| @@ -107,8 +107,9 @@ | ||
| 107 | 107 | getHeicConversionAdvice: () => getHeicConversionAdvice, |
| 108 | 108 | getHeicUnsupportedMessage: () => getHeicUnsupportedMessage, |
| 109 | 109 | isClientSideMediaSupported: () => isClientSideMediaSupported, |
| 110 | 110 | isHeicCanvasSupported: () => isHeicCanvasSupported, |
| 111 | + isHeicFile: () => isHeicFile, | |
| 111 | 112 | store: () => store |
| 112 | 113 | }); |
| 113 | 114 | |
| 114 | 115 | // packages/upload-media/build-module/store/index.mjs |
| @@ -259,9 +260,20 @@ | ||
| 259 | 260 | ...item, |
| 260 | 261 | status: ItemStatus.Processing, |
| 261 | 262 | error: void 0, |
| 262 | 263 | retryCount: (item.retryCount ?? 0) + 1, |
| 263 | - abortController: new AbortController() | |
| 264 | + abortController: new AbortController(), | |
| 265 | + /* | |
| 266 | + * The failed operation is still recorded on | |
| 267 | + * the item: nothing finishes it when it | |
| 268 | + * fails, and while the item waits out the | |
| 269 | + * backoff that is what keeps it out of the | |
| 270 | + * concurrency pools. Clear it now that the | |
| 271 | + * item is about to run again, so processItem | |
| 272 | + * does not mistake it for an operation still | |
| 273 | + * in flight and skip the retry. | |
| 274 | + */ | |
| 275 | + currentOperation: void 0 | |
| 264 | 276 | } : item |
| 265 | 277 | ) |
| 266 | 278 | }; |
| 267 | 279 | case Type.ScheduleRetry: |
| @@ -665,8 +677,56 @@ | ||
| 665 | 677 | } |
| 666 | 678 | } |
| 667 | 679 | return false; |
| 668 | 680 | } |
| 681 | + var FILE_TYPE_BOX_BYTES = 64; | |
| 682 | + var HEIC_BRANDS = [ | |
| 683 | + "heic", | |
| 684 | + "heix", | |
| 685 | + "heim", | |
| 686 | + "heis", | |
| 687 | + "hevc", | |
| 688 | + "hevx", | |
| 689 | + "hevm", | |
| 690 | + "hevs", | |
| 691 | + "mif1", | |
| 692 | + "msf1" | |
| 693 | + ]; | |
| 694 | + var AVIF_BRANDS = ["avif", "avis"]; | |
| 695 | + function isHeicBuffer(buffer) { | |
| 696 | + const view = new Uint8Array(buffer); | |
| 697 | + const readBrand = (offset) => String.fromCharCode(...view.subarray(offset, offset + 4)); | |
| 698 | + if (view.length < 12 || readBrand(4) !== "ftyp") { | |
| 699 | + return false; | |
| 700 | + } | |
| 701 | + const boxEnd = Math.min( | |
| 702 | + view.length, | |
| 703 | + new DataView(buffer).getUint32(0) | |
| 704 | + ); | |
| 705 | + const brands = [readBrand(8)]; | |
| 706 | + for (let offset = 16; offset + 4 <= boxEnd; offset += 4) { | |
| 707 | + brands.push(readBrand(offset)); | |
| 708 | + } | |
| 709 | + if (brands.some((brand) => AVIF_BRANDS.includes(brand))) { | |
| 710 | + return false; | |
| 711 | + } | |
| 712 | + return brands.some((brand) => HEIC_BRANDS.includes(brand)); | |
| 713 | + } | |
| 714 | + async function isHeicFile(file) { | |
| 715 | + if (HEIC_MIME_TYPES.includes(file.type)) { | |
| 716 | + return true; | |
| 717 | + } | |
| 718 | + if (file.type && !file.type.startsWith("image/")) { | |
| 719 | + return false; | |
| 720 | + } | |
| 721 | + try { | |
| 722 | + return isHeicBuffer( | |
| 723 | + await file.slice(0, FILE_TYPE_BOX_BYTES).arrayBuffer() | |
| 724 | + ); | |
| 725 | + } catch { | |
| 726 | + return false; | |
| 727 | + } | |
| 728 | + } | |
| 669 | 729 | |
| 670 | 730 | // packages/upload-media/build-module/store/utils/index.mjs |
| 671 | 731 | var vipsModulePromise; |
| 672 | 732 | var vipsModule; |
| @@ -2073,8 +2133,9 @@ | ||
| 2073 | 2133 | return `${cause} ${getHeicConversionAdvice()}`; |
| 2074 | 2134 | } |
| 2075 | 2135 | |
| 2076 | 2136 | // packages/upload-media/build-module/canvas-utils.mjs |
| 2137 | + var IMAGE_DECODER_TIMEOUT = 15e3; | |
| 2077 | 2138 | var HeicUnsupportedError = class extends Error { |
| 2078 | 2139 | }; |
| 2079 | 2140 | async function canvasConvertToJpeg(file, quality = 0.82) { |
| 2080 | 2141 | const baseName = getFileBasename(file.name); |
| @@ -2105,10 +2166,36 @@ | ||
| 2105 | 2166 | const decoder = new ImageDecoder({ |
| 2106 | 2167 | type: file.type, |
| 2107 | 2168 | data: file.stream() |
| 2108 | 2169 | }); |
| 2170 | + let videoFrame; | |
| 2171 | + let timeoutId; | |
| 2172 | + let timedOut = false; | |
| 2109 | 2173 | try { |
| 2110 | - const { image: videoFrame } = await decoder.decode(); | |
| 2174 | + const decoded = await Promise.race([ | |
| 2175 | + decoder.decode(), | |
| 2176 | + new Promise((_resolve, reject) => { | |
| 2177 | + timeoutId = setTimeout(() => { | |
| 2178 | + timedOut = true; | |
| 2179 | + decoder.close(); | |
| 2180 | + reject( | |
| 2181 | + new Error("ImageDecoder decode timed out") | |
| 2182 | + ); | |
| 2183 | + }, IMAGE_DECODER_TIMEOUT); | |
| 2184 | + }) | |
| 2185 | + ]); | |
| 2186 | + videoFrame = decoded.image; | |
| 2187 | + } catch (error) { | |
| 2188 | + if (!timedOut) { | |
| 2189 | + throw error; | |
| 2190 | + } | |
| 2191 | + } finally { | |
| 2192 | + clearTimeout(timeoutId); | |
| 2193 | + if (!timedOut) { | |
| 2194 | + decoder.close(); | |
| 2195 | + } | |
| 2196 | + } | |
| 2197 | + if (videoFrame) { | |
| 2111 | 2198 | try { |
| 2112 | 2199 | const canvas = new OffscreenCanvas( |
| 2113 | 2200 | videoFrame.displayWidth, |
| 2114 | 2201 | videoFrame.displayHeight |
| @@ -2127,10 +2214,8 @@ | ||
| 2127 | 2214 | }); |
| 2128 | 2215 | } finally { |
| 2129 | 2216 | videoFrame.close(); |
| 2130 | 2217 | } |
| 2131 | - } finally { | |
| 2132 | - decoder.close(); | |
| 2133 | 2218 | } |
| 2134 | 2219 | } |
| 2135 | 2220 | } |
| 2136 | 2221 | let heicData; |
| @@ -2554,8 +2639,11 @@ | ||
| 2554 | 2639 | const item = select2.getItem(id); |
| 2555 | 2640 | if (!item) { |
| 2556 | 2641 | return; |
| 2557 | 2642 | } |
| 2643 | + if (item.currentOperation) { | |
| 2644 | + return; | |
| 2645 | + } | |
| 2558 | 2646 | const { |
| 2559 | 2647 | attachment, |
| 2560 | 2648 | onChange, |
| 2561 | 2649 | onSuccess, |
| @@ -2833,13 +2921,16 @@ | ||
| 2833 | 2921 | } |
| 2834 | 2922 | } |
| 2835 | 2923 | } |
| 2836 | 2924 | let heicJpeg = null; |
| 2837 | - const isImage = file.type.startsWith("image/"); | |
| 2838 | - const isVipsSupported = CLIENT_SIDE_SUPPORTED_MIME_TYPES.includes( | |
| 2839 | - file.type | |
| 2840 | - ); | |
| 2841 | - const isHeic = HEIC_MIME_TYPES.includes(file.type); | |
| 2925 | + const isHeic = await isHeicFile(file); | |
| 2926 | + const isMisnamedHeic = isHeic && !HEIC_MIME_TYPES.includes(file.type); | |
| 2927 | + const isImage = file.type.startsWith("image/") || isHeic; | |
| 2928 | + const isVipsSupported = !isHeic && CLIENT_SIDE_SUPPORTED_MIME_TYPES.includes(file.type); | |
| 2929 | + const heicFile = isMisnamedHeic ? new File([file], `${getFileBasename(file.name)}.heic`, { | |
| 2930 | + type: HEIC_MIME_TYPES[0], | |
| 2931 | + lastModified: file.lastModified | |
| 2932 | + }) : file; | |
| 2842 | 2933 | let tooLargeForClient = false; |
| 2843 | 2934 | if (isImage && isVipsSupported) { |
| 2844 | 2935 | const dimensions = await getImageDimensions(file); |
| 2845 | 2936 | if (dimensions && exceedsClientProcessingMemory(dimensions)) { |
| @@ -2845,9 +2936,9 @@ | ||
| 2845 | 2936 | if (dimensions && exceedsClientProcessingMemory(dimensions)) { |
| 2846 | 2937 | tooLargeForClient = true; |
| 2847 | 2938 | } |
| 2848 | 2939 | } |
| 2849 | - if (file.type === "image/jpeg" && !tooLargeForClient) { | |
| 2940 | + if (file.type === "image/jpeg" && !isHeic && !tooLargeForClient) { | |
| 2850 | 2941 | operations.push(OperationType.DetectUltraHdr); |
| 2851 | 2942 | } |
| 2852 | 2943 | if (isImage && isVipsSupported && !tooLargeForClient) { |
| 2853 | 2944 | operations.push( |
| @@ -2857,9 +2948,9 @@ | ||
| 2857 | 2948 | ); |
| 2858 | 2949 | } else if (isImage && isHeic) { |
| 2859 | 2950 | try { |
| 2860 | 2951 | heicJpeg = await canvasConvertToJpeg( |
| 2861 | - file, | |
| 2952 | + heicFile, | |
| 2862 | 2953 | settings.imageQuality ?? DEFAULT_OUTPUT_QUALITY |
| 2863 | 2954 | ); |
| 2864 | 2955 | } catch (error) { |
| 2865 | 2956 | const unsupported = error instanceof HeicUnsupportedError; |
| @@ -2894,9 +2985,9 @@ | ||
| 2894 | 2985 | const vipsAvailable = isClientSideMediaSupported(); |
| 2895 | 2986 | updates = { |
| 2896 | 2987 | file: heicJpeg, |
| 2897 | 2988 | sourceFile: heicJpeg, |
| 2898 | - originalHeicFile: item.file, | |
| 2989 | + originalHeicFile: heicFile, | |
| 2899 | 2990 | additionalData: { |
| 2900 | 2991 | ...item.additionalData, |
| 2901 | 2992 | generate_sub_sizes: !vipsAvailable, |
| 2902 | 2993 | convert_format: true |