Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support video recording. #680

Merged
merged 6 commits into from
May 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/.idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion android/.idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions android/.idea/runConfigurations.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,15 @@ export class CaptureDetailsPage {
readonly assetSrc$ = this.proof$.pipe(
switchMap(async proof => {
const [index, meta] = Object.entries(proof.indexedAssets)[0];
if (!(await this.imageStore.exists(index)) && proof.diaBackendAssetId) {
const imageBlob = await this.diaBackendAssetRepository
if (!(await this.mediaStore.exists(index)) && proof.diaBackendAssetId) {
const mediaBlob = await this.diaBackendAssetRepository
.downloadFile$({ id: proof.diaBackendAssetId, field: 'asset_file' })
.pipe(
first(),
catchError((err: unknown) => this.errorService.toastError$(err))
)
.toPromise();
await proof.setAssets({ [await blobToBase64(imageBlob)]: meta });
await proof.setAssets({ [await blobToBase64(mediaBlob)]: meta });
}
return proof.getFirstAssetUrl();
})
Expand Down Expand Up @@ -106,7 +106,7 @@ export class CaptureDetailsPage {
private readonly proofRepository: ProofRepository,
private readonly diaBackendAuthService: DiaBackendAuthService,
private readonly diaBackendAssetRepository: DiaBackendAssetRepository,
private readonly imageStore: MediaStore,
private readonly mediaStore: MediaStore,
private readonly shareService: ShareService,
private readonly errorService: ErrorService,
private readonly actionSheetController: ActionSheetController
Expand Down
44 changes: 41 additions & 3 deletions src/app/features/home/home.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ChangeDetectorRef, Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { Plugins } from '@capacitor/core';
import { ActionSheetController } from '@ionic/angular';
import { TranslocoService } from '@ngneat/transloco';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { defer, EMPTY, iif, of } from 'rxjs';
Expand All @@ -14,7 +15,11 @@ import {
tap,
} from 'rxjs/operators';
import { ErrorService } from '../../shared/modules/error/error.service';
import { CaptureService } from '../../shared/services/capture/capture.service';
import { CameraService } from '../../shared/services/camera/camera.service';
import {
CaptureService,
Media,
} from '../../shared/services/capture/capture.service';
import { ConfirmAlert } from '../../shared/services/confirm-alert/confirm-alert.service';
import { DiaBackendAssetRepository } from '../../shared/services/dia-backend/asset/dia-backend-asset-repository.service';
import { DiaBackendAuthService } from '../../shared/services/dia-backend/auth/dia-backend-auth.service';
Expand Down Expand Up @@ -61,7 +66,9 @@ export class HomePage {
private readonly dialog: MatDialog,
private readonly translocoService: TranslocoService,
private readonly migrationService: MigrationService,
private readonly errorService: ErrorService
private readonly errorService: ErrorService,
private readonly cameraService: CameraService,
private readonly actionSheetController: ActionSheetController
) {
this.downloadExpiredPostCaptures();
}
Expand Down Expand Up @@ -128,9 +135,10 @@ export class HomePage {
return defer(() => {
const captureIndex = 2;
this.selectedTabIndex = captureIndex;
return this.captureService.capture();
return this.presentCaptureActions$();
})
.pipe(
concatMap(media => this.captureService.capture(media)),
catchError((err: unknown) => {
if (err !== 'User cancelled photos app')
return this.errorService.toastError$(err);
Expand All @@ -141,6 +149,36 @@ export class HomePage {
.subscribe();
}

private presentCaptureActions$() {
return this.translocoService
.selectTranslateObject({
takePicture: null,
recordVideo: null,
})
.pipe(
first(),
concatMap(
([takePicture, recordVideo]) =>
new Promise<Media>(resolve =>
this.actionSheetController
.create({
buttons: [
{
text: takePicture,
handler: () => resolve(this.cameraService.takePhoto()),
},
{
text: recordVideo,
handler: () => resolve(this.cameraService.recordVideo()),
},
],
})
.then(sheet => sheet.present())
)
)
);
}

// eslint-disable-next-line class-methods-use-this
async openCaptureClub() {
return Browser.open({
Expand Down
4 changes: 2 additions & 2 deletions src/app/features/profile/profile.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class ProfilePage {
constructor(
private readonly database: Database,
private readonly preferenceManager: PreferenceManager,
private readonly imageStore: MediaStore,
private readonly mediaStore: MediaStore,
private readonly blockingActionService: BlockingActionService,
private readonly errorService: ErrorService,
private readonly translocoService: TranslocoService,
Expand Down Expand Up @@ -85,7 +85,7 @@ export class ProfilePage {
}

logout() {
const action$ = defer(() => this.imageStore.clear()).pipe(
const action$ = defer(() => this.mediaStore.clear()).pipe(
concatMapTo(defer(() => this.database.clear())),
concatMapTo(defer(() => this.preferenceManager.clear())),
concatMapTo(defer(reloadApp)),
Expand Down
44 changes: 37 additions & 7 deletions src/app/shared/services/camera/camera.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@ import {
CameraSource,
} from '@capacitor/core';
import { Subject } from 'rxjs';
import { blobToBase64 } from '../../../utils/encoding/encoding';
import { fromExtension, MimeType } from '../../../utils/mime-type';
import {
APP_PLUGIN,
CAMERA_PLUGIN,
} from '../../core/capacitor-plugins/capacitor-plugins.module';
import { Media } from '../capture/capture.service';

@Injectable({
providedIn: 'root',
})
export class CameraService {
private readonly killedCapturedPhotoEvent$ = new Subject<Photo>();
private readonly killedCapturedPhotoEvent$ = new Subject<Media>();
readonly restoreKilledCaptureEvent$ = this.killedCapturedPhotoEvent$.asObservable();

constructor(
Expand All @@ -38,7 +40,7 @@ export class CameraService {
});
}

async takePhoto(): Promise<Photo> {
async takePhoto(): Promise<Media> {
const cameraPhoto = await this.cameraPlugin.getPhoto({
resultType: CameraResultType.Base64,
source: CameraSource.Camera,
Expand All @@ -47,17 +49,45 @@ export class CameraService {
});
return cameraPhotoToPhoto(cameraPhoto);
}
}

export interface Photo {
readonly mimeType: MimeType;
readonly base64: string;
// eslint-disable-next-line class-methods-use-this
async recordVideo(): Promise<Media> {
return new Promise<Media>((resolve, reject) => {
const inputElement = document.createElement('input');
inputElement.accept = 'video/*';
inputElement.type = 'file';
inputElement.setAttribute('capture', 'environment');
// Safari/Webkit quirk: input element must be attached to body in order to work
document.body.appendChild(inputElement);
inputElement.onchange = event => {
document.body.removeChild(inputElement);
const file = (event.target as HTMLInputElement | null)?.files?.item(0);
if (
!file ||
(file.type !== 'video/mp4' && file.type !== 'video/quicktime')
)
reject(new VideoRecordError(`File type: ${file?.type}`));
else
blobToBase64(file).then(base64 =>
resolve({
base64,
mimeType: file.type as MimeType,
})
);
};
inputElement.click();
});
}
}

function cameraPhotoToPhoto(cameraPhoto: CameraPhoto): Photo {
function cameraPhotoToPhoto(cameraPhoto: CameraPhoto): Media {
return {
mimeType: fromExtension(cameraPhoto.format),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
base64: cameraPhoto.base64String!,
};
}

export class VideoRecordError extends Error {
readonly name = 'VideoRecordError';
}
17 changes: 10 additions & 7 deletions src/app/shared/services/capture/capture.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { CameraService, Photo } from '../camera/camera.service';
import { MimeType } from '../../../utils/mime-type';
import { CollectorService } from '../collector/collector.service';
import { MediaStore } from '../media-store/media-store.service';
import { getOldProof } from '../repositories/proof/old-proof-adapter';
Expand All @@ -17,17 +17,15 @@ export class CaptureService {
readonly collectingOldProofHashes$ = this._collectingOldProofHashes$;

constructor(
private readonly cameraService: CameraService,
private readonly proofRepository: ProofRepository,
private readonly imageStore: MediaStore,
private readonly mediaStore: MediaStore,
private readonly collectorService: CollectorService
) {}

async capture(restoredPhoto?: Photo) {
const photo = restoredPhoto ?? (await this.cameraService.takePhoto());
async capture(source: Media) {
const proof = await Proof.from(
this.imageStore,
{ [photo.base64]: { mimeType: photo.mimeType } },
this.mediaStore,
{ [source.base64]: { mimeType: source.mimeType } },
{ timestamp: Date.now(), providers: {} },
{}
);
Expand All @@ -52,3 +50,8 @@ export class CaptureService {
);
}
}

export interface Media {
readonly mimeType: MimeType;
readonly base64: string;
}
4 changes: 2 additions & 2 deletions src/app/shared/services/collector/collector.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ export class CollectorService {
private readonly factsProviders = new Set<FactsProvider>();
private readonly signatureProviders = new Set<SignatureProvider>();

constructor(private readonly imageStore: MediaStore) {}
constructor(private readonly mediaStore: MediaStore) {}

async run(assets: Assets, capturedTimestamp: number) {
const truth = await this.collectTruth(assets, capturedTimestamp);
const signatures = await this.signTargets({ assets, truth });
const proof = await Proof.from(this.imageStore, assets, truth, signatures);
const proof = await Proof.from(this.mediaStore, assets, truth, signatures);
proof.isCollected = true;
return proof;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
export class DiaBackendAssetDownloadingService {
constructor(
private readonly assetRepository: DiaBackendAssetRepository,
private readonly imageStore: MediaStore,
private readonly mediaStore: MediaStore,
private readonly proofRepository: ProofRepository
) {}

Expand All @@ -37,7 +37,7 @@ export class DiaBackendAssetDownloadingService {
.downloadFile$({ id: diaBackendAsset.id, field: 'asset_file_thumbnail' })
.pipe(first())
.toPromise();
return this.imageStore.storeThumbnail(
return this.mediaStore.storeThumbnail(
diaBackendAsset.proof_hash,
await blobToBase64(thumbnailBlob),
diaBackendAsset.information.proof.mimeType
Expand All @@ -52,7 +52,7 @@ export class DiaBackendAssetDownloadingService {
return;
}
const proof = new Proof(
this.imageStore,
this.mediaStore,
getTruth({
proof: diaBackendAsset.information.proof,
information: diaBackendAsset.information.information,
Expand Down
18 changes: 11 additions & 7 deletions src/app/shared/services/media-store/media-store.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,22 +127,24 @@ export class MediaStore {
}

getThumbnailUrl$(index: string, mimeType: MimeType) {
const isVideo = mimeType.startsWith('video');
const thumbnailMimeType = isVideo ? 'image/jpeg' : mimeType;
return defer(() => this.getThumbnail(index)).pipe(
concatMap(thumbnail => {
if (thumbnail) {
return defer(() => this.read(thumbnail.thumbnailIndex)).pipe(
map(base64 => toDataUrl(base64, mimeType))
map(base64 => toDataUrl(base64, thumbnailMimeType))
);
}
if (mimeType.startsWith('video')) {
if (isVideo) {
return defer(() => this.setThumbnail(index, mimeType)).pipe(
map(base64 => toDataUrl(base64, mimeType))
map(base64 => toDataUrl(base64, thumbnailMimeType))
);
}
return merge(
defer(() => this.getUrl(index, mimeType)),
defer(() => this.setThumbnail(index, mimeType)).pipe(
map(base64 => toDataUrl(base64, mimeType))
defer(() => this.getUrl(index, thumbnailMimeType)),
defer(() => this.setThumbnail(index, thumbnailMimeType)).pipe(
map(base64 => toDataUrl(base64, thumbnailMimeType))
)
);
})
Expand Down Expand Up @@ -221,7 +223,9 @@ export class MediaStore {
async getUrl(index: string, mimeType: MimeType) {
if (Capacitor.isNative)
return Capacitor.convertFileSrc(await this.getUri(index));
return toDataUrl(await this.read(index), mimeType);
return URL.createObjectURL(
await base64ToBlob(await this.read(index), mimeType)
);
}

private async getExtension(index: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ import {

describe('old-proof-adapter', () => {
let proof: Proof;
let imageStore: MediaStore;
let mediaStore: MediaStore;

beforeEach(async () => {
TestBed.configureTestingModule({
imports: [SharedTestingModule],
});
imageStore = TestBed.inject(MediaStore);
proof = await Proof.from(imageStore, ASSETS, TRUTH, SIGNATURES);
mediaStore = TestBed.inject(MediaStore);
proof = await Proof.from(mediaStore, ASSETS, TRUTH, SIGNATURES);
});

it('should convert Proof to OldProof', () => {
Expand Down Expand Up @@ -70,7 +70,7 @@ describe('old-proof-adapter', () => {
it('should convert SortedProofInformation with raw Blob to Proof', async () => {
const blob = await base64ToBlob(ASSET1_BASE64, ASSET1_MIMETYPE);
const convertedProof = await getProof(
imageStore,
mediaStore,
blob,
SORTED_PROOF_INFORMATION,
OLD_SIGNATURES
Expand Down
Loading