From c5ad5b30b4bddb2e65f4f9c29efedff0f5b1cae4 Mon Sep 17 00:00:00 2001 From: Sean Wu Date: Wed, 12 May 2021 14:27:24 +0800 Subject: [PATCH 1/2] Install `capacitor-blob-writer` with Android. --- android/app/capacitor.build.gradle | 2 +- android/app/src/main/AndroidManifest.xml | 3 ++- .../numbersprotocol/capturelite/MainActivity.java | 4 +++- .../src/main/res/xml/network_security_config.xml | 6 ++++++ android/capacitor.settings.gradle | 3 +++ ios/App/Podfile | 2 +- package-lock.json | 15 +++++++++++++++ package.json | 1 + 8 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 android/app/src/main/res/xml/network_security_config.xml diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 837cd452a..ed5f7e515 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -9,7 +9,7 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { - + implementation project(':capacitor-blob-writer') } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0097cac1d..1d2c7a31c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -28,7 +28,8 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" + android:networkSecurityConfig="@xml/network_security_config"> diff --git a/android/app/src/main/java/io/numbersprotocol/capturelite/MainActivity.java b/android/app/src/main/java/io/numbersprotocol/capturelite/MainActivity.java index 49f471042..e855bcb48 100644 --- a/android/app/src/main/java/io/numbersprotocol/capturelite/MainActivity.java +++ b/android/app/src/main/java/io/numbersprotocol/capturelite/MainActivity.java @@ -5,6 +5,8 @@ import com.getcapacitor.BridgeActivity; import com.getcapacitor.Plugin; +import com.equimaps.capacitorblobwriter.BlobWriter; + import java.util.ArrayList; public class MainActivity extends BridgeActivity { @@ -15,7 +17,7 @@ public void onCreate(Bundle savedInstanceState) { // Initializes the Bridge this.init(savedInstanceState, new ArrayList>() {{ // Additional plugins you've installed go here - // Ex: add(TotallyAwesomePlugin.class); + add(BlobWriter.class); }}); } } diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 000000000..6489d8564 --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,6 @@ + + + + localhost + + \ No newline at end of file diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 9a5fa872e..f8953f834 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -1,3 +1,6 @@ // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN include ':capacitor-android' project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') + +include ':capacitor-blob-writer' +project(':capacitor-blob-writer').projectDir = new File('../node_modules/capacitor-blob-writer/android') diff --git a/ios/App/Podfile b/ios/App/Podfile index 58615b9fd..6870dc834 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -10,7 +10,7 @@ def capacitor_pods # Automatic Capacitor Pod dependencies, do not delete pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' - + pod 'CapacitorBlobWriter', :path => '../../node_modules/capacitor-blob-writer' # Do not delete end diff --git a/package-lock.json b/package-lock.json index a3d8c601e..a721ec7ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@ngx-formly/material": "^5.10.15", "@ngx-formly/schematics": "^5.10.15", "async-mutex": "^0.3.1", + "capacitor-blob-writer": "^0.2.1", "compressorjs": "^1.0.7", "immutable": "^4.0.0-rc.12", "lodash-es": "^4.17.21", @@ -5035,6 +5036,14 @@ "integrity": "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==", "dev": true }, + "node_modules/capacitor-blob-writer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/capacitor-blob-writer/-/capacitor-blob-writer-0.2.1.tgz", + "integrity": "sha512-fkad3aJA5/j4WbexCxevtE+SfpBi1gAiaFVCpl+wjlXjTDpRTDjP1n4jWlPrs6K0uT3lMtjJpo4rFZsMcvhrqQ==", + "peerDependencies": { + "@capacitor/core": "^2.0.0" + } + }, "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -26821,6 +26830,12 @@ "integrity": "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==", "dev": true }, + "capacitor-blob-writer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/capacitor-blob-writer/-/capacitor-blob-writer-0.2.1.tgz", + "integrity": "sha512-fkad3aJA5/j4WbexCxevtE+SfpBi1gAiaFVCpl+wjlXjTDpRTDjP1n4jWlPrs6K0uT3lMtjJpo4rFZsMcvhrqQ==", + "requires": {} + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", diff --git a/package.json b/package.json index 8ccfa1cd1..ded01e003 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@ngx-formly/material": "^5.10.15", "@ngx-formly/schematics": "^5.10.15", "async-mutex": "^0.3.1", + "capacitor-blob-writer": "^0.2.1", "compressorjs": "^1.0.7", "immutable": "^4.0.0-rc.12", "lodash-es": "^4.17.21", From 67adc2bdb590bd971b31f630acd0832eca9174ce Mon Sep 17 00:00:00 2001 From: Sean Wu Date: Wed, 12 May 2021 16:38:14 +0800 Subject: [PATCH 2/2] Support large media asset. - Use `capacitor-blob-writer` to write large file into native filesystem. - Use `HttpClient` to read large file into Angular. - Avoid using `Capacitor.Filesystem.readFile` and `Capacitor.Filesystem.writeFile` if possible. --- .../media/media-store/media-store.service.ts | 64 +++++++++++++------ src/app/shared/shared-testing.module.ts | 2 - 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/app/shared/media/media-store/media-store.service.ts b/src/app/shared/media/media-store/media-store.service.ts index 4b19850ae..e6ed9a33c 100644 --- a/src/app/shared/media/media-store/media-store.service.ts +++ b/src/app/shared/media/media-store/media-store.service.ts @@ -1,3 +1,4 @@ +import { HttpClient } from '@angular/common/http'; import { Inject, Injectable } from '@angular/core'; import { Capacitor, @@ -5,12 +6,13 @@ import { FilesystemPlugin, } from '@capacitor/core'; import { Mutex } from 'async-mutex'; +import { writeFile } from 'capacitor-blob-writer'; import Compressor from 'compressorjs'; import { defer, merge } from 'rxjs'; import { concatMap, map } from 'rxjs/operators'; import { sha256WithBase64 } from '../../../utils/crypto/crypto'; import { base64ToBlob, blobToBase64 } from '../../../utils/encoding/encoding'; -import { MimeType, toExtension } from '../../../utils/mime-type'; +import { fromExtension, MimeType, toExtension } from '../../../utils/mime-type'; import { toDataUrl } from '../../../utils/url'; import { FILESYSTEM_PLUGIN } from '../../capacitor-plugins/capacitor-plugins.module'; import { Database } from '../../database/database.service'; @@ -38,7 +40,8 @@ export class MediaStore { constructor( @Inject(FILESYSTEM_PLUGIN) private readonly filesystemPlugin: FilesystemPlugin, - private readonly database: Database + private readonly database: Database, + private readonly httpClient: HttpClient ) {} private async initialize() { @@ -61,7 +64,18 @@ export class MediaStore { }); } - async read(index: string) { + async read(index: string): Promise { + await this.initialize(); + const extension = await this.getExtension(index); + if (!extension) throw new Error(`Cannot get extension of ${index}.`); + const url = await this.getUrl(index, fromExtension(extension)); + const blob = await this.httpClient + .get(url, { responseType: 'blob' }) + .toPromise(); + return blobToBase64(blob); + } + + async readWithFileSystem(index: string) { await this.initialize(); const extension = await this.getExtension(index); const result = await this.filesystemPlugin.readFile({ @@ -82,9 +96,7 @@ export class MediaStore { return this._write(index, base64, mimeType); } const exists = await this.exists(index); - if (exists) { - return index; - } + if (exists) return index; return this._write(index, base64, mimeType); } @@ -92,12 +104,22 @@ export class MediaStore { await this.initialize(); return this.mutex.runExclusive(async () => { const mediaExtension = await this.setMediaExtension(index, mimeType); - await this.filesystemPlugin.writeFile({ - directory: this.directory, - path: `${this.rootDir}/${index}.${mediaExtension.extension}`, - data: base64, - recursive: true, - }); + if (Capacitor.isNative) { + const blob = await base64ToBlob(base64, mimeType); + await writeFile({ + directory: this.directory, + path: `${this.rootDir}/${index}.${mediaExtension.extension}`, + data: blob, + recursive: true, + }); + } else { + await this.filesystemPlugin.writeFile({ + directory: this.directory, + path: `${this.rootDir}/${index}.${mediaExtension.extension}`, + data: base64, + recursive: true, + }); + } return index; }); } @@ -158,11 +180,13 @@ export class MediaStore { private async makeThumbnail(index: string, mimeType: MimeType) { const thumbnailSize = 100; - const blob = await base64ToBlob(await this.read(index), mimeType); const thumbnailBlob = mimeType.startsWith('video') - ? await makeVideoThumbnail({ video: blob, width: thumbnailSize }) + ? await makeVideoThumbnail({ + videoUrl: await this.getUrl(index, mimeType), + width: thumbnailSize, + }) : await makeImageThumbnail({ - image: blob, + image: await base64ToBlob(await this.read(index), mimeType), width: thumbnailSize, }); return blobToBase64(thumbnailBlob); @@ -224,7 +248,7 @@ export class MediaStore { if (Capacitor.isNative) return Capacitor.convertFileSrc(await this.getUri(index)); return URL.createObjectURL( - await base64ToBlob(await this.read(index), mimeType) + await base64ToBlob(await this.readWithFileSystem(index), mimeType) ); } @@ -317,11 +341,11 @@ async function makeImageThumbnail({ } async function makeVideoThumbnail({ - video, + videoUrl, width, quality = 0.6, }: { - video: Blob; + videoUrl: string; width: number; quality?: number; }) { @@ -349,13 +373,13 @@ async function makeVideoThumbnail({ resolve(makeImageThumbnail({ image: blob, width, quality })); else reject(TypeError('canvas.toBlob is null.')); }, - video.type, + 'image/jpeg', quality ); } }); videoElement.preload = 'auto'; - videoElement.src = URL.createObjectURL(video); + videoElement.src = videoUrl; videoElement.load(); }); } diff --git a/src/app/shared/shared-testing.module.ts b/src/app/shared/shared-testing.module.ts index e420b92e3..4366b3ec4 100644 --- a/src/app/shared/shared-testing.module.ts +++ b/src/app/shared/shared-testing.module.ts @@ -1,4 +1,3 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; import { NgModule } from '@angular/core'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; @@ -11,7 +10,6 @@ import { SharedModule } from './shared.module'; @NgModule({ imports: [ SharedModule, - HttpClientTestingModule, RouterTestingModule, BrowserAnimationsModule, getTranslocoTestingModule(),