From 5921e9770a465e72dd137006b7787f510b9c50f6 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 1 Apr 2022 20:43:24 +0200 Subject: [PATCH] Composer: Support all audio formats with cover images (#1793) --- package-lock.json | 257 ++++++++++++++++-- package.json | 2 + src/api/gramjs/apiBuilders/messages.ts | 38 +-- src/api/gramjs/methods/messages.ts | 18 +- src/api/types/misc.ts | 5 + src/components/common/Audio.tsx | 8 +- .../middle/composer/AttachmentModal.tsx | 3 +- .../composer/helpers/buildAttachment.ts | 19 +- src/config.ts | 12 + src/hooks/useAsync.ts | 4 +- src/hooks/useMessageMediaMetadata.ts | 19 +- src/util/audio.ts | 22 ++ webpack.config.js | 1 + 13 files changed, 348 insertions(+), 60 deletions(-) create mode 100644 src/util/audio.ts diff --git a/package-lock.json b/package-lock.json index b9a249a5c..fb684a90a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,10 +16,12 @@ "emoji-data-ios": "git+https://github.com/korenskoy/emoji-data-ios#54443d1938ec1c157e74d2a95e9103dcb3f5c6dd", "events": "^3.3.0", "idb-keyval": "^6.1.0", + "music-metadata-browser": "^2.5.5", "opus-recorder": "github:Ajaxy/opus-recorder", "os-browserify": "^0.3.0", "pako": "^2.0.4", "path-browserify": "^1.0.1", + "process": "^0.11.10", "qr-code-styling": "github:zubiden/qr-code-styling#10f7cf3", "websocket": "^1.0.34" }, @@ -90,7 +92,7 @@ }, "engines": { "node": "^16.13", - "npm": "^8.1" + "npm": "^8.5.2" }, "optionalDependencies": { "fsevents": "2.1.2" @@ -2816,6 +2818,11 @@ "node": ">=8" } }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "dev": true, @@ -4233,7 +4240,6 @@ }, "node_modules/base64-js": { "version": "1.5.1", - "dev": true, "funding": [ { "type": "github", @@ -4406,7 +4412,6 @@ }, "node_modules/buffer": { "version": "6.0.3", - "dev": true, "funding": [ { "type": "github", @@ -4938,7 +4943,6 @@ }, "node_modules/content-type": { "version": "1.0.4", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -5431,7 +5435,6 @@ }, "node_modules/debug": { "version": "4.3.3", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -6963,6 +6966,22 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-type": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.3.tgz", + "integrity": "sha512-uVsl7iFhHSOY4bEONLlTK47iAHtNsFHWP5YE4xJfZ4rnX7S1Q3wce09XgqSC7E/xh8Ncv/be1lNoyprlUH/x6A==", + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/fill-range": { "version": "7.0.1", "dev": true, @@ -7752,7 +7771,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "dev": true, "funding": [ { "type": "github", @@ -7850,7 +7868,6 @@ }, "node_modules/inherits": { "version": "2.0.4", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -10974,7 +10991,6 @@ }, "node_modules/ms": { "version": "2.1.2", - "dev": true, "license": "MIT" }, "node_modules/multicast-dns": { @@ -10994,6 +11010,51 @@ "dev": true, "license": "MIT" }, + "node_modules/music-metadata": { + "version": "7.12.2", + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-7.12.2.tgz", + "integrity": "sha512-KO1L6q30b6HfGlDQk1VAdrZqCKi4Gy7pN7eZOZ0YZQkhF/KCLHxKCjKKli9ao9kIBC/9s+uXHvjW3bDIBWuGew==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "content-type": "^1.0.4", + "debug": "^4.3.3", + "file-type": "16.5.3", + "media-typer": "^1.1.0", + "strtok3": "^6.2.4", + "token-types": "^4.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/music-metadata-browser": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/music-metadata-browser/-/music-metadata-browser-2.5.5.tgz", + "integrity": "sha512-38A/q1fz7LOIDxpi2fAzPGMNZQ0YyQUfErizK/rbWRIKC7E4N2BQpqCHq38nHlb7+Iv/wEHgwVoIwbUAXtphEA==", + "dependencies": { + "buffer": "^6.0.3", + "debug": "^4.3.3", + "music-metadata": "^7.12.0", + "readable-stream": "^3.6.0", + "readable-web-to-node-stream": "^3.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/music-metadata/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/nanoid": { "version": "3.3.1", "dev": true, @@ -11545,6 +11606,18 @@ "node": ">=8" } }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/picocolors": { "version": "1.0.0", "dev": true, @@ -12431,6 +12504,14 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "dev": true, @@ -12753,7 +12834,6 @@ }, "node_modules/readable-stream": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -12764,6 +12844,21 @@ "node": ">= 6" } }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/readdirp": { "version": "3.6.0", "dev": true, @@ -13735,7 +13830,6 @@ }, "node_modules/string_decoder": { "version": "1.3.0", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -13743,7 +13837,6 @@ }, "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.2.1", - "dev": true, "funding": [ { "type": "github", @@ -13922,6 +14015,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/style-loader": { "version": "3.3.1", "dev": true, @@ -14877,6 +14986,22 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.0.tgz", + "integrity": "sha512-P0rrp4wUpefLncNamWIef62J0v0kQR/GfDVji9WKY7GDCWy5YbVSrKUTam07iWPZQGy0zWNOfstYTykMmPNR7w==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/totalist": { "version": "1.1.0", "dev": true, @@ -15211,7 +15336,6 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/utila": { @@ -17892,6 +18016,11 @@ } } }, + "@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + }, "@tootallnate/once": { "version": "1.1.2", "dev": true @@ -18858,8 +18987,7 @@ "dev": true }, "base64-js": { - "version": "1.5.1", - "dev": true + "version": "1.5.1" }, "batch": { "version": "0.6.1", @@ -18972,7 +19100,6 @@ }, "buffer": { "version": "6.0.3", - "dev": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -19304,8 +19431,7 @@ } }, "content-type": { - "version": "1.0.4", - "dev": true + "version": "1.0.4" }, "convert-source-map": { "version": "1.8.0", @@ -19612,7 +19738,6 @@ }, "debug": { "version": "4.3.3", - "dev": true, "requires": { "ms": "2.1.2" } @@ -20620,6 +20745,16 @@ "flat-cache": "^3.0.4" } }, + "file-type": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.3.tgz", + "integrity": "sha512-uVsl7iFhHSOY4bEONLlTK47iAHtNsFHWP5YE4xJfZ4rnX7S1Q3wce09XgqSC7E/xh8Ncv/be1lNoyprlUH/x6A==", + "requires": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + } + }, "fill-range": { "version": "7.0.1", "dev": true, @@ -21102,8 +21237,7 @@ } }, "ieee754": { - "version": "1.2.1", - "dev": true + "version": "1.2.1" }, "ignore": { "version": "5.2.0", @@ -21150,8 +21284,7 @@ } }, "inherits": { - "version": "2.0.4", - "dev": true + "version": "2.0.4" }, "ini": { "version": "1.3.8", @@ -23109,8 +23242,7 @@ "dev": true }, "ms": { - "version": "2.1.2", - "dev": true + "version": "2.1.2" }, "multicast-dns": { "version": "6.2.3", @@ -23124,6 +23256,39 @@ "version": "1.1.0", "dev": true }, + "music-metadata": { + "version": "7.12.2", + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-7.12.2.tgz", + "integrity": "sha512-KO1L6q30b6HfGlDQk1VAdrZqCKi4Gy7pN7eZOZ0YZQkhF/KCLHxKCjKKli9ao9kIBC/9s+uXHvjW3bDIBWuGew==", + "requires": { + "@tokenizer/token": "^0.3.0", + "content-type": "^1.0.4", + "debug": "^4.3.3", + "file-type": "16.5.3", + "media-typer": "^1.1.0", + "strtok3": "^6.2.4", + "token-types": "^4.2.0" + }, + "dependencies": { + "media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" + } + } + }, + "music-metadata-browser": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/music-metadata-browser/-/music-metadata-browser-2.5.5.tgz", + "integrity": "sha512-38A/q1fz7LOIDxpi2fAzPGMNZQ0YyQUfErizK/rbWRIKC7E4N2BQpqCHq38nHlb7+Iv/wEHgwVoIwbUAXtphEA==", + "requires": { + "buffer": "^6.0.3", + "debug": "^4.3.3", + "music-metadata": "^7.12.0", + "readable-stream": "^3.6.0", + "readable-web-to-node-stream": "^3.0.2" + } + }, "nanoid": { "version": "3.3.1", "dev": true @@ -23465,6 +23630,11 @@ "version": "4.0.0", "dev": true }, + "peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==" + }, "picocolors": { "version": "1.0.0", "dev": true @@ -23981,6 +24151,11 @@ } } }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, "process-nextick-args": { "version": "2.0.1", "dev": true @@ -24190,13 +24365,20 @@ }, "readable-stream": { "version": "3.6.0", - "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, + "readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "requires": { + "readable-stream": "^3.6.0" + } + }, "readdirp": { "version": "3.6.0", "dev": true, @@ -24830,14 +25012,12 @@ }, "string_decoder": { "version": "1.3.0", - "dev": true, "requires": { "safe-buffer": "~5.2.0" }, "dependencies": { "safe-buffer": { - "version": "5.2.1", - "dev": true + "version": "5.2.1" } } }, @@ -24939,6 +25119,15 @@ "version": "3.1.1", "dev": true }, + "strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "requires": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + } + }, "style-loader": { "version": "3.3.1", "dev": true, @@ -25564,6 +25753,15 @@ "version": "1.0.1", "dev": true }, + "token-types": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.0.tgz", + "integrity": "sha512-P0rrp4wUpefLncNamWIef62J0v0kQR/GfDVji9WKY7GDCWy5YbVSrKUTam07iWPZQGy0zWNOfstYTykMmPNR7w==", + "requires": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + } + }, "totalist": { "version": "1.1.0", "dev": true @@ -25759,8 +25957,7 @@ } }, "util-deprecate": { - "version": "1.0.2", - "dev": true + "version": "1.0.2" }, "utila": { "version": "0.4.0", diff --git a/package.json b/package.json index 637d7abd1..859c20460 100644 --- a/package.json +++ b/package.json @@ -106,10 +106,12 @@ "emoji-data-ios": "git+https://github.com/korenskoy/emoji-data-ios#54443d1938ec1c157e74d2a95e9103dcb3f5c6dd", "events": "^3.3.0", "idb-keyval": "^6.1.0", + "music-metadata-browser": "^2.5.5", "opus-recorder": "github:Ajaxy/opus-recorder", "os-browserify": "^0.3.0", "pako": "^2.0.4", "path-browserify": "^1.0.1", + "process": "^0.11.10", "qr-code-styling": "github:zubiden/qr-code-styling#10f7cf3", "websocket": "^1.0.34" }, diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 9eab5170e..a4b2aa386 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -36,6 +36,7 @@ import { LOCAL_MESSAGE_ID_BASE, SERVICE_NOTIFICATIONS_USER_ID, SPONSORED_MESSAGE_CACHE_MS, + SUPPORTED_AUDIO_CONTENT_TYPES, SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES, VIDEO_MOV_TYPE, @@ -1079,9 +1080,8 @@ function buildUploadingMedia( } = attachment; if (attachment.quick) { - const { width, height, duration } = attachment.quick; - - if (mimeType.startsWith('image/')) { + if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType)) { + const { width, height } = attachment.quick; return { photo: { id: LOCAL_MEDIA_UPLOADING_TEMP_ID, @@ -1090,7 +1090,9 @@ function buildUploadingMedia( blobUrl, }, }; - } else { + } + if (SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) { + const { width, height, duration } = attachment.quick; return { video: { id: LOCAL_MEDIA_UPLOADING_TEMP_ID, @@ -1105,7 +1107,8 @@ function buildUploadingMedia( }, }; } - } else if (attachment.voice) { + } + if (attachment.voice) { const { duration, waveform } = attachment.voice; const { data: inputWaveform } = interpolateArray(waveform, INPUT_WAVEFORM_LENGTH); return { @@ -1115,26 +1118,29 @@ function buildUploadingMedia( waveform: inputWaveform, }, }; - } else if (mimeType.startsWith('audio/')) { + } + if (SUPPORTED_AUDIO_CONTENT_TYPES.has(mimeType)) { + const { duration, performer, title } = attachment.audio || {}; return { audio: { id: LOCAL_MEDIA_UPLOADING_TEMP_ID, mimeType, fileName, size, - duration: 200, // Arbitrary - }, - }; - } else { - return { - document: { - mimeType, - fileName, - size, - ...(previewBlobUrl && { previewBlobUrl }), + duration: duration || 0, + title, + performer, }, }; } + return { + document: { + mimeType, + fileName, + size, + ...(previewBlobUrl && { previewBlobUrl }), + }, + }; } function buildNewPoll(poll: ApiNewPoll, localId: number) { diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 0ffb1f111..00a90faf3 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -534,10 +534,9 @@ export async function rescheduleMessage({ async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment, onProgress: ApiOnProgress) { const { - filename, blobUrl, mimeType, quick, voice, + filename, blobUrl, mimeType, quick, voice, audio, previewBlobUrl, } = attachment; - const file = await fetchFile(blobUrl, filename); const patchedOnProgress: ApiOnProgress = (progress) => { if (onProgress.isCanceled) { patchedOnProgress.isCanceled = true; @@ -545,8 +544,13 @@ async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment, onProgress(progress, localMessage.id); } }; + + const file = await fetchFile(blobUrl, filename); const inputFile = await uploadFile(file, patchedOnProgress); + const thumbFile = previewBlobUrl && await fetchFile(previewBlobUrl, filename); + const thumb = thumbFile ? await uploadFile(thumbFile) : undefined; + const attributes: GramJs.TypeDocumentAttribute[] = [new GramJs.DocumentAttributeFilename({ fileName: filename })]; if (quick) { if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType)) { @@ -566,6 +570,15 @@ async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment, } } + if (audio) { + const { duration, title, performer } = audio; + attributes.push(new GramJs.DocumentAttributeAudio({ + duration, + title, + performer, + })); + } + if (voice) { const { duration, waveform } = voice; const { data: inputWaveform } = interpolateArray(waveform, INPUT_WAVEFORM_LENGTH); @@ -580,6 +593,7 @@ async function uploadMedia(localMessage: ApiMessage, attachment: ApiAttachment, file: inputFile, mimeType, attributes, + thumb, }); } diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index b7bb43366..96965e9e8 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -33,6 +33,11 @@ export interface ApiAttachment { duration: number; waveform: number[]; }; + audio?: { + duration: number; + title?: string; + performer?: string; + }; previewBlobUrl?: string; } diff --git a/src/components/common/Audio.tsx b/src/components/common/Audio.tsx index a7a9591d9..d960335bd 100644 --- a/src/components/common/Audio.tsx +++ b/src/components/common/Audio.tsx @@ -407,8 +407,12 @@ function renderAudio( {!showSeekline && !showProgress && (
{formatMediaDuration(duration)} - - {renderText(performer || 'Unknown')} + {performer && ( + <> + + {renderText(performer)} + + )} {date && ( <> diff --git a/src/components/middle/composer/AttachmentModal.tsx b/src/components/middle/composer/AttachmentModal.tsx index 41ab657bb..f279aab88 100644 --- a/src/components/middle/composer/AttachmentModal.tsx +++ b/src/components/middle/composer/AttachmentModal.tsx @@ -7,6 +7,7 @@ import { ApiAttachment, ApiChatMember } from '../../../api/types'; import { CONTENT_TYPES_WITH_PREVIEW, EDITABLE_INPUT_MODAL_ID, + SUPPORTED_AUDIO_CONTENT_TYPES, SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES, } from '../../../config'; @@ -185,7 +186,7 @@ const AttachmentModal: FC = ({ const areAllPhotos = renderingAttachments.every((a) => SUPPORTED_IMAGE_CONTENT_TYPES.has(a.mimeType)); const areAllVideos = renderingAttachments.every((a) => SUPPORTED_VIDEO_CONTENT_TYPES.has(a.mimeType)); - const areAllAudios = renderingAttachments.every((a) => a.mimeType.startsWith('audio/')); + const areAllAudios = renderingAttachments.every((a) => SUPPORTED_AUDIO_CONTENT_TYPES.has(a.mimeType)); let title = ''; if (areAllPhotos) { diff --git a/src/components/middle/composer/helpers/buildAttachment.ts b/src/components/middle/composer/helpers/buildAttachment.ts index 13862f47e..e290658cd 100644 --- a/src/components/middle/composer/helpers/buildAttachment.ts +++ b/src/components/middle/composer/helpers/buildAttachment.ts @@ -1,5 +1,10 @@ import { ApiAttachment } from '../../../../api/types'; -import { SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES } from '../../../../config'; +import { + SUPPORTED_AUDIO_CONTENT_TYPES, + SUPPORTED_IMAGE_CONTENT_TYPES, + SUPPORTED_VIDEO_CONTENT_TYPES, +} from '../../../../config'; +import { parseAudioMetadata } from '../../../../util/audio'; import { preloadImage, preloadVideo, @@ -17,6 +22,7 @@ export default async function buildAttachment( const blobUrl = URL.createObjectURL(blob); const { type: mimeType, size } = blob; let quick; + let audio; let previewBlobUrl; if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType)) { @@ -44,6 +50,16 @@ export default async function buildAttachment( quick = { width, height, duration }; previewBlobUrl = await createPosterForVideo(blobUrl); + } else if (SUPPORTED_AUDIO_CONTENT_TYPES.has(mimeType)) { + const { + duration, title, performer, coverUrl, + } = await parseAudioMetadata(blobUrl); + audio = { + duration: duration || 0, + title, + performer, + }; + previewBlobUrl = coverUrl; } return { @@ -52,6 +68,7 @@ export default async function buildAttachment( mimeType, size, quick, + audio, previewBlobUrl, ...options, }; diff --git a/src/config.ts b/src/config.ts index 6fc3b72d0..573d733ec 100644 --- a/src/config.ts +++ b/src/config.ts @@ -148,6 +148,18 @@ export const SUPPORTED_VIDEO_CONTENT_TYPES = new Set([ 'video/mp4', // video/quicktime added dynamically in environment.ts ]); +export const SUPPORTED_AUDIO_CONTENT_TYPES = new Set([ + 'audio/mp3', + 'audio/ogg', + 'audio/wav', + 'audio/mpeg', + 'audio/flac', + 'audio/aac', + 'audio/m4a', + 'audio/mp4', + 'audio/x-m4a', +]); + export const CONTENT_TYPES_WITH_PREVIEW = new Set([ ...SUPPORTED_IMAGE_CONTENT_TYPES, ...SUPPORTED_VIDEO_CONTENT_TYPES, diff --git a/src/hooks/useAsync.ts b/src/hooks/useAsync.ts index a4b9e43f3..6088a05e9 100644 --- a/src/hooks/useAsync.ts +++ b/src/hooks/useAsync.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from '../lib/teact/teact'; -export const useAsync = (fn: () => Promise, deps: any[], defaultValue?: T) => { +const useAsync = (fn: () => Promise, deps: any[], defaultValue?: T) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(); const [result, setResult] = useState(defaultValue); @@ -23,3 +23,5 @@ export const useAsync = (fn: () => Promise, deps: any[], defaultValue?: T) }, deps); return { isLoading, error, result }; }; + +export default useAsync; diff --git a/src/hooks/useMessageMediaMetadata.ts b/src/hooks/useMessageMediaMetadata.ts index 2f104551c..e0cfe77a2 100644 --- a/src/hooks/useMessageMediaMetadata.ts +++ b/src/hooks/useMessageMediaMetadata.ts @@ -4,16 +4,17 @@ import { ApiAudio, ApiChat, ApiMessage, ApiUser, ApiVoice, } from '../api/types'; -import useMedia from './useMedia'; -import { useAsync } from './useAsync'; import { getAudioHasCover, getChatAvatarHash, getChatTitle, getMessageContent, getMessageMediaHash, getSenderTitle, } from '../global/helpers'; -import { getTranslation } from '../util/langProvider'; import { buildMediaMetadata } from '../util/mediaSession'; import { scaleImage, resizeImage } from '../util/imageResize'; import { AVATAR_FULL_DIMENSIONS } from '../components/common/helpers/mediaDimensions'; +import useLang from './useLang'; +import useMedia from './useMedia'; +import useAsync from './useAsync'; + import telegramLogoPath from '../assets/telegram-logo-filled.svg'; const LOGO_DIMENSIONS = { width: 200, height: 200 }; @@ -23,10 +24,12 @@ const MINIMAL_SIZE = 115; // spec says 100, but on Chrome 93 it's not showing const useMessageMediaMetadata = ( message: ApiMessage, sender?: ApiUser | ApiChat, chat?: ApiChat, ): MediaMetadata | undefined => { + const lang = useLang(); + const { audio, voice } = getMessageContent(message); const title = audio ? (audio.title || audio.fileName) : voice ? 'Voice message' : ''; - const artist = (audio && audio.performer) || (sender && getSenderTitle(getTranslation, sender)); - const album = (chat && getChatTitle(getTranslation, chat)) || 'Telegram'; + const artist = audio?.performer || (sender && getSenderTitle(lang, sender)); + const album = (chat && getChatTitle(lang, chat)) || 'Telegram'; const audioCoverHash = (audio && getAudioHasCover(audio) && getMessageMediaHash(message, 'pictogram')); const avatarHash = sender && getChatAvatarHash(sender, 'big'); @@ -36,7 +39,9 @@ const useMessageMediaMetadata = ( const size = useMemo(() => { return getCoverSize(audio, voice, media); }, [audio, media, voice]); - const { result: url } = useAsync(() => makeGoodArtwork(media, size), [media, size], telegramLogoPath); + const { result: url } = useAsync(() => ( + makeGoodArtwork(media, size) + ), [media, size], telegramLogoPath); return useMemo(() => { return buildMediaMetadata({ title, @@ -61,7 +66,7 @@ function makeGoodArtwork(url?: string, size?: { width: number; height: number }) function getCoverSize(audio?: ApiAudio, voice?: ApiVoice, url?: string) { if (!url) return LOGO_DIMENSIONS; if (audio) { - if (!audio.thumbnailSizes || audio.thumbnailSizes.length === 0) return undefined; + if (!audio.thumbnailSizes?.length) return undefined; const preferred = audio.thumbnailSizes.find((size) => size.type === 'm'); return preferred || audio.thumbnailSizes[0]; // Sometimes `m` is not present } diff --git a/src/util/audio.ts b/src/util/audio.ts new file mode 100644 index 000000000..bad44283e --- /dev/null +++ b/src/util/audio.ts @@ -0,0 +1,22 @@ +type AudioMetadata = { + title?: string; + performer?: string; + duration?: number; + coverUrl?: string; +}; + +export async function parseAudioMetadata(url: string): Promise { + const { fetchFromUrl, selectCover } = await import('music-metadata-browser'); + const metadata = await fetchFromUrl(url); + const { common: { title, artist, picture }, format: { duration } } = metadata; + + const cover = selectCover(picture); + const coverUrl = cover ? `data:${cover.format};base64,${cover.data.toString('base64')}` : undefined; + + return { + title, + performer: artist, + duration, + coverUrl, + }; +} diff --git a/webpack.config.js b/webpack.config.js index 5361c0985..d57171872 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -133,6 +133,7 @@ module.exports = (env = {}, argv = {}) => { }), new ProvidePlugin({ Buffer: ['buffer', 'Buffer'], + process: 'process/browser', }), ...(argv.mode === 'production' ? [ new BundleAnalyzerPlugin({