diff --git a/lib/types/media-types.js b/lib/types/media-types.js --- a/lib/types/media-types.js +++ b/lib/types/media-types.js @@ -216,6 +216,13 @@ +orientation: ?number, }; +export type GenerateThumbhashMediaMissionStep = { + +step: 'generate_thumbhash', + +success: boolean, + +exceptionMessage: ?string, + +thumbHash: ?string, +}; + export type EncryptFileMediaMissionStep = | { +step: 'read_plaintext_file', @@ -452,6 +459,7 @@ | CopyFileMediaMissionStep | EncryptFileMediaMissionStep | GetOrientationMediaMissionStep + | GenerateThumbhashMediaMissionStep | { +step: 'preload_image', +success: boolean, diff --git a/native/media/encryption-utils.js b/native/media/encryption-utils.js --- a/native/media/encryption-utils.js +++ b/native/media/encryption-utils.js @@ -181,6 +181,12 @@ } if (preprocessedMedia.mediaType === 'photo') { + const thumbHashResult = preprocessedMedia.thumbHash + ? encryptBase64( + preprocessedMedia.thumbHash, + hexToUintArray(encryptionResult.encryptionKey), + ) + : null; return { steps, result: { @@ -188,6 +194,7 @@ mediaType: 'encrypted_photo', uploadURI: encryptionResult.uri, blobHash: encryptionResult.sha256Hash, + thumbHash: thumbHashResult?.base64, encryptionKey: encryptionResult.encryptionKey, shouldDisposePath: pathFromURI(encryptionResult.uri), }, @@ -203,6 +210,13 @@ return { steps, result: thumbnailEncryptionResult }; } + const thumbHashResult = preprocessedMedia.thumbHash + ? encryptBase64( + preprocessedMedia.thumbHash, + hexToUintArray(thumbnailEncryptionResult.encryptionKey), + ) + : null; + return { steps, result: { @@ -210,6 +224,7 @@ mediaType: 'encrypted_video', uploadURI: encryptionResult.uri, blobHash: encryptionResult.sha256Hash, + thumbHash: thumbHashResult?.base64, encryptionKey: encryptionResult.encryptionKey, uploadThumbnailURI: thumbnailEncryptionResult.uri, thumbnailBlobHash: thumbnailEncryptionResult.sha256Hash, diff --git a/native/media/image-utils.js b/native/media/image-utils.js --- a/native/media/image-utils.js +++ b/native/media/image-utils.js @@ -10,6 +10,8 @@ } from 'lib/types/media-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; +import { generateThumbhashStep } from './media-utils.js'; + type ProcessImageInfo = { uri: string, dimensions: Dimensions, @@ -22,6 +24,7 @@ uri: string, mime: string, dimensions: Dimensions, + thumbHash: ?string, }; async function processImage(input: ProcessImageInfo): Promise<{ steps: $ReadOnlyArray, @@ -38,9 +41,12 @@ inputOrientation: orientation, }); if (plan.action === 'none') { + const thumbhashStep = await generateThumbhashStep(uri); + steps.push(thumbhashStep); + const { thumbHash } = thumbhashStep; return { steps, - result: { success: true, uri, dimensions, mime }, + result: { success: true, uri, dimensions, mime, thumbHash }, }; } const { targetMIME, compressionRatio, fitInside } = plan; @@ -99,7 +105,14 @@ }; } - return { steps, result: { success: true, uri, dimensions, mime } }; + const thumbhashStep = await generateThumbhashStep(uri); + steps.push(thumbhashStep); + const { thumbHash } = thumbhashStep; + + return { + steps, + result: { success: true, uri, dimensions, mime, thumbHash }, + }; } export { processImage }; diff --git a/native/media/media-utils.js b/native/media/media-utils.js --- a/native/media/media-utils.js +++ b/native/media/media-utils.js @@ -9,12 +9,15 @@ MediaMissionStep, MediaMissionFailure, NativeMediaSelection, + GenerateThumbhashMediaMissionStep, } from 'lib/types/media-types.js'; +import { getMessageForException } from 'lib/utils/errors.js'; import { fetchFileInfo } from './file-utils.js'; import { processImage } from './image-utils.js'; import { saveMedia } from './save-media.js'; import { processVideo } from './video-utils.js'; +import { generateThumbHash } from '../utils/thumbhash-module.js'; type MediaProcessConfig = { +hasWiFi: boolean, @@ -29,6 +32,7 @@ +filename: string, +mime: string, +dimensions: Dimensions, + +thumbHash: ?string, }; export type MediaResult = | { +mediaType: 'photo', ...SharedMediaResult } @@ -88,7 +92,8 @@ mediaType = null, mime = null, loop = false, - resultReturned = false; + resultReturned = false, + thumbHash = null; const returnResult = (failure?: MediaMissionFailure) => { invariant( !resultReturned, @@ -118,6 +123,7 @@ mediaType, dimensions, loop, + thumbHash, }); } else { sendResult({ @@ -128,6 +134,7 @@ mime, mediaType, dimensions, + thumbHash, }); } }; @@ -208,6 +215,7 @@ mime, dimensions, loop, + thumbHash, } = videoResult); } else if (mediaType === 'photo') { const { steps: imageSteps, result: imageResult } = await processImage({ @@ -221,7 +229,7 @@ if (!imageResult.success) { return await finish(imageResult); } - ({ uri: uploadURI, mime, dimensions } = imageResult); + ({ uri: uploadURI, mime, dimensions, thumbHash } = imageResult); } else { invariant(false, `unknown mediaType ${mediaType}`); } @@ -264,4 +272,22 @@ }); } -export { processMedia, getDimensions }; +async function generateThumbhashStep( + uri: string, +): Promise { + let thumbHash, exceptionMessage; + try { + thumbHash = await generateThumbHash(uri); + } catch (err) { + exceptionMessage = getMessageForException(err); + } + + return { + step: 'generate_thumbhash', + success: !!thumbHash && !exceptionMessage, + exceptionMessage, + thumbHash, + }; +} + +export { processMedia, getDimensions, generateThumbhashStep }; diff --git a/native/media/video-utils.js b/native/media/video-utils.js --- a/native/media/video-utils.js +++ b/native/media/video-utils.js @@ -19,6 +19,7 @@ import { ffmpeg } from './ffmpeg.js'; import { temporaryDirectoryPath } from './file-utils.js'; +import { generateThumbhashStep } from './media-utils.js'; // These are some numbers I sorta kinda made up // We should try to calculate them on a per-device basis @@ -48,6 +49,7 @@ +mime: string, +dimensions: Dimensions, +loop: boolean, + +thumbHash: ?string, }; async function processVideo( input: ProcessVideoInfo, @@ -104,12 +106,17 @@ result: { success: false, reason: 'video_generate_thumbnail_failed' }, }; } + const thumbnailURI = `file://${plan.thumbnailPath}`; + const thumbhashStep = await generateThumbhashStep(thumbnailURI); + steps.push(thumbhashStep); + const { thumbHash } = thumbhashStep; return { steps, result: { success: true, uri: input.uri, - thumbnailURI: `file://${plan.thumbnailPath}`, + thumbnailURI, + thumbHash, mime: 'video/mp4', dimensions: input.dimensions, loop: false, @@ -165,12 +172,17 @@ mediaConfig[input.mime].videoConfig && mediaConfig[input.mime].videoConfig.loop ); + const thumbnailURI = `file://${plan.thumbnailPath}`; + const thumbhashStep = await generateThumbhashStep(thumbnailURI); + steps.push(thumbhashStep); + const { thumbHash } = thumbhashStep; return { steps, result: { success: true, uri: `file://${plan.outputPath}`, - thumbnailURI: `file://${plan.thumbnailPath}`, + thumbnailURI, + thumbHash, mime: 'video/mp4', dimensions, loop,