Skip to content

Commit

Permalink
Video support
Browse files Browse the repository at this point in the history
  • Loading branch information
RemcoSimonides committed Aug 11, 2024
1 parent a9433ee commit c338ded
Show file tree
Hide file tree
Showing 22 changed files with 203 additions and 67 deletions.
25 changes: 13 additions & 12 deletions apps/journal/package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
{
"name": "journal",
"devDependencies": {
"@capacitor-community/fcm": "^5.0.2",
"@capacitor-firebase/authentication": "^5.2.0",
"@capacitor/app": "^5.0.6",
"@capacitor/camera": "^5.0.7",
"@capacitor/cli": "^5.0.5",
"@capacitor/clipboard": "^5.0.6",
"@capacitor/ios": "5.0.5",
"@capacitor/keyboard": "^5.0.6",
"@capacitor/push-notifications": "^5.1.0",
"@capacitor/share": "^5.0.6",
"@capacitor/splash-screen": "^5.0.6",
"@capawesome/capacitor-app-update": "^5.0.1",
"@capacitor-community/fcm": "^5.0.3",
"@capacitor-firebase/authentication": "^6.0.0",
"@capacitor/app": "^6.0.0",
"@capacitor/camera": "^6.0.0",
"@capacitor/cli": "^6.0.0",
"@capacitor/clipboard": "^6.0.0",
"@capacitor/ios": "6.0.0",
"@capacitor/keyboard": "^6.0.0",
"@capacitor/push-notifications": "^6.0.0",
"@capacitor/share": "^6.0.0",
"@capacitor/splash-screen": "^6.0.0",
"@capawesome/capacitor-app-update": "^6.0.0",
"@capawesome/capacitor-file-picker": "^6.0.1",
"@capacitor/filesystem": "^6.0.0",
"send-intent": "^6.0.0"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<swiper-container space-between="16" slides-per-view="auto" #swiper>
@for (control of form.controls; track control; let index = $index) {
<swiper-slide (click)="openPopover($event, index)">
<img [src]="control.value.preview" />
@if (control.value.type === 'video') {
<video [src]="control.value.preview" (loadedmetadata)="checkDuration($event, index)"></video>
} @else {
<img [src]="control.value.preview" />
}
</swiper-slide>
}
<swiper-slide>
Expand All @@ -11,7 +15,7 @@
@case ('drop') {
<article class="drop-zone" (click)="selectImages()">
<ion-icon name="images-outline" />
<span>Add Photo</span>
<span>Add Photo or Video</span>
</article>
}
<!-- Hovering -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
}
}

img {
img, video {
height: 100%;
border-radius: 12px;
}
Expand Down Expand Up @@ -52,7 +52,7 @@
align-items: center;
margin-bottom: 1em;

img {
img, video {
width: 100px;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, ChangeDetectorRef, Com
import { CommonModule } from '@angular/common'
import { FormArray } from '@angular/forms'
import { SafeUrl } from '@angular/platform-browser'
import { IonIcon, PopoverController, ToastController } from '@ionic/angular/standalone'
import { AlertController, IonIcon, PopoverController, ToastController } from '@ionic/angular/standalone'
import { addIcons } from 'ionicons'
import { imagesOutline } from 'ionicons/icons'
import { SwiperContainer } from 'swiper/swiper-element'

import { BehaviorSubject, Subscription } from 'rxjs'

import { Camera, GalleryPhoto } from '@capacitor/camera'
import { FilePicker } from '@capawesome/capacitor-file-picker'

import { captureException, captureMessage } from '@sentry/capacitor'
import { EditMediaForm } from '@strive/media/forms/media.form'
import { delay } from '@strive/utils/helpers'
import { ImageOptionsPopoverComponent } from './popover/options.component'
import { VideoPlayerComponent } from '../video-player/video-player.component'

type CropStep = 'drop' | 'hovering'

Expand All @@ -27,15 +29,17 @@ type CropStep = 'drop' | 'hovering'
imports: [
CommonModule,
ImageOptionsPopoverComponent,
IonIcon
IonIcon,
VideoPlayerComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class ImagesSelectorComponent implements OnInit, OnDestroy {

step = new BehaviorSubject<CropStep>('drop')

accept = ['.jpg', '.jpeg', '.png', '.webp']
accept = ['image/*','video/*']
maxVideoLength = 10
previewUrl$ = new BehaviorSubject<string | SafeUrl>('')

sub?: Subscription
Expand All @@ -46,6 +50,7 @@ export class ImagesSelectorComponent implements OnInit, OnDestroy {
@ViewChild('swiper') swiper?: ElementRef<SwiperContainer>;

constructor(
private alertCtrl: AlertController,
private cdr: ChangeDetectorRef,
private popoverCtrl: PopoverController,
private toast: ToastController
Expand Down Expand Up @@ -86,15 +91,15 @@ export class ImagesSelectorComponent implements OnInit, OnDestroy {

async selectImages() {
try {
const images = await Camera.pickImages({ quality: 100 })
for (const image of images.photos) {
const file = await getFileFromGalleryPhoto(image)
if (file) {
this.filesSelected(file)
const { files } = await FilePicker.pickMedia()

for (const file of files) {
if (file.blob instanceof File) {
this.filesSelected(file.blob)
} else {
this.toast.create({ message: 'Something went wrong', duration: 3000 })
captureMessage('Unsupported file type chosen')
captureException(image)
captureException(file)
}
}
} catch (err) {
Expand All @@ -111,13 +116,14 @@ export class ImagesSelectorComponent implements OnInit, OnDestroy {
const files = isFileList(file) ? Array.from(file) : [file]

for (const file of files) {
if (file.type?.split('/')[0] !== 'image') {
const type = file.type?.split('/')[0]
if (type !== 'image' && type !== 'video') {
this.toast.create({ message: 'Unsupported file type', duration: 3000 }).then(toast => toast.present())
return
}

const preview = URL.createObjectURL(file)
const mediaForm = new EditMediaForm({ id: '', preview, file })
const mediaForm = new EditMediaForm({ id: '', preview, file, type })
this.form.push(mediaForm)
this.form.markAsDirty()
}
Expand All @@ -137,9 +143,11 @@ export class ImagesSelectorComponent implements OnInit, OnDestroy {
}

async openPopover(event: Event, index: number) {
const ctrl = this.form.at(index)
const type = ctrl.type.value
const popover = await this.popoverCtrl.create({
component: ImageOptionsPopoverComponent,
componentProps: { form: this.form, index },
componentProps: { type },
event
})

Expand All @@ -153,23 +161,19 @@ export class ImagesSelectorComponent implements OnInit, OnDestroy {

popover.present()
}

checkDuration(event: any, index: number) {
const duration = event.target.duration
if (duration > this.maxVideoLength) {
this.form.removeAt(index)
this.alertCtrl.create({
subHeader: `Video cannot be longer than ${this.maxVideoLength} seconds`,
buttons: [{ text: 'Ok', role: 'cancel' }]
}).then(alert => alert.present())
}
}
}

function isFileList(file: FileList | File): file is FileList {
return (file as FileList).item !== undefined
}


async function getFileFromGalleryPhoto(photo: GalleryPhoto): Promise<File | undefined> {
try {
const response = await fetch(photo.webPath);
const blob = await response.blob();
const name = photo.webPath.split('/').pop();
const fileName = `${name}.${photo.format}`
const file = new File([blob], fileName, { type: blob.type });
return file;
} catch (error) {
console.error("Error fetching blob data:", error);
return;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { IonList, IonItem, PopoverController } from '@ionic/angular/standalone'
import { MediaType } from '@strive/model'

@Component({
standalone: true,
Expand All @@ -10,12 +11,15 @@ import { IonList, IonItem, PopoverController } from '@ionic/angular/standalone'
selector: 'strive-image-options-popover',
template: `
<ion-list lines="none">
<ion-item button (click)="remove()">Remove image</ion-item>
<ion-item button (click)="remove()">Remove {{ type }}</ion-item>
</ion-list>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ImageOptionsPopoverComponent {

@Input() type: MediaType = 'image'

constructor(
private popoverCtrl: PopoverController
) { }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<video
#player
controls
crossorigin="anonymous"
[src]="storagePath | videoUrl"
autoplay
></video>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
video {
width: 100%;
max-width: 1500px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { AfterViewInit, Component, ElementRef, Input, ViewChild, ViewEncapsulation } from '@angular/core'
import { MediaRefPipe, VideoUrlPipe } from '@strive/media/pipes/media.pipe';
// import Hls from 'hls.js'

@Component({
standalone: true,
selector: 'media-video-player',
templateUrl: './video-player.component.html',
styleUrls: ['./video-player.component.scss'],
encapsulation: ViewEncapsulation.None,
imports: [
MediaRefPipe,
VideoUrlPipe
]
})
export class VideoPlayerComponent implements AfterViewInit {
@ViewChild('player', { static: true }) player: ElementRef<HTMLVideoElement> = {} as ElementRef<HTMLVideoElement>;

@Input() storagePath = ''

ngAfterViewInit() {
// Vidoe API only available on IMGIX enterprice plan: https://www.imgix.com/pricing
// if (!this.storagePath) return

// const video = this.player.nativeElement
// const src = `https://${environment.firebase.options.projectId}.imgix.video/${encodeURI(this.storagePath)}?fm=hls`

// if (video.canPlayType('application/vnd.apple.mpegurl')) {
// video.src = src
// } else if (Hls.isSupported()) {
// const hls = new Hls()
// hls.loadSource(src)
// hls.attachMedia(video)
// } else {
// console.error("This is a legacy browser that doesn't support Media Source Extensions")
// }
}
}
9 changes: 7 additions & 2 deletions libs/media/src/lib/forms/media.form.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { AbstractControl, FormControl, FormGroup } from '@angular/forms'
import { Media } from '@strive/model'
import { Media, MediaType } from '@strive/model'
import { getImgIxResourceUrl } from '../directives/imgix-helpers'

interface EditMedia {
export interface EditMedia {
id: string
preview: string
delete?: boolean
file?: File
type?: MediaType
}

export function mediaToEditMedia(media: Media): EditMedia {
Expand All @@ -21,6 +22,7 @@ export function mediaToEditMedia(media: Media): EditMedia {
return {
id: media.id ?? '',
preview: getPreview(),
type: media.fileType,
delete: false
}
}
Expand All @@ -30,6 +32,7 @@ export function createEditMedia(params: Partial<EditMedia> = {}): EditMedia {
id: params.id ?? '',
preview: params.preview ?? '',
file: params.file ?? undefined,
type: params.type ?? undefined,
delete: params.delete ?? false
}
}
Expand All @@ -41,6 +44,7 @@ function createEditMediaFormControl(params: Partial<EditMedia> = {}) {
delete: new FormControl<boolean>(editMedia.delete ?? false, { nonNullable: true }),
preview: new FormControl<string>(editMedia.preview, { nonNullable: true }),
file: new FormControl<File | undefined>(editMedia.file, { nonNullable: true }),
type: new FormControl<MediaType | undefined>(editMedia.type, { nonNullable: true }),
id: new FormControl<string>(editMedia.id ?? '', { nonNullable: true })
}
}
Expand All @@ -55,5 +59,6 @@ export class EditMediaForm extends FormGroup<MediaFormControl> {
get delete() { return this.get('delete') as AbstractControl<boolean> }
get preview() { return this.get('preview') as AbstractControl<string> }
get file() { return this.get('file') as AbstractControl<File | undefined> }
get type() { return this.get('type') as AbstractControl<MediaType | undefined> }
get id() { return this.get('id') as AbstractControl<string> }
}
5 changes: 4 additions & 1 deletion libs/media/src/lib/media.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ export class MediaService extends FireSubCollection<Media> {
}

async upload(file: File, storagePath: string, goalId: string) {
const start = file.type.split('/')[0]
const fileType = start === 'image' ? 'image' : 'video'

const media = createMedia({
fileName: file.name,
fileType: 'image',
fileType,
storagePath
})
const mediaId = await this.add(media, { params: { goalId }})
Expand Down
10 changes: 9 additions & 1 deletion libs/media/src/lib/pipes/media.pipe.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core'
import { Media } from '@strive/model'

import { environment } from '@env'

@Pipe({ name: 'mediaRef', standalone: true })
export class MediaRefPipe implements PipeTransform {
Expand All @@ -9,3 +9,11 @@ export class MediaRefPipe implements PipeTransform {
return `${media.storagePath}/${media.id}`
}
}

@Pipe({ name: 'videoUrl', standalone: true })
export class VideoUrlPipe implements PipeTransform {
transform(storagePath: string) {
if (!storagePath) return ''
return `https://${environment.firebase.options.projectId}.imgix.net/${encodeURI(storagePath)}?fm=hls`
}
}
2 changes: 1 addition & 1 deletion libs/model/src/lib/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const mediaTypes = [
'video',
'youtube'
] as const;
type MediaType = typeof mediaTypes[number];
export type MediaType = typeof mediaTypes[number];

type MediaUploadStatus = 'uploading' | 'uploaded' | 'error';

Expand Down
Loading

0 comments on commit c338ded

Please sign in to comment.