diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index 482ba6ba..1c542da4 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -1789,6 +1789,9 @@ class FFmpegService { imageData.length; final buffer = ByteData(blockSize); + // The header and payload share one allocation; artwork can be several + // megabytes, so copying this whole buffer doubles the block's memory. + final blockBytes = buffer.buffer.asUint8List(); var offset = 0; buffer.setUint32(offset, 3, Endian.big); @@ -1797,8 +1800,6 @@ class FFmpegService { buffer.setUint32(offset, mimeBytes.length, Endian.big); offset += 4; - final blockBytes = Uint8List(blockSize); - blockBytes.setRange(0, offset, buffer.buffer.asUint8List()); blockBytes.setRange(offset, offset + mimeBytes.length, mimeBytes); offset += mimeBytes.length; diff --git a/test/ffmpeg_opus_metadata_test.dart b/test/ffmpeg_opus_metadata_test.dart index b055f913..410e13d8 100644 --- a/test/ffmpeg_opus_metadata_test.dart +++ b/test/ffmpeg_opus_metadata_test.dart @@ -82,6 +82,77 @@ void main() { return hash; } + for (final cover in [ + ( + fileName: 'cover.png', + mimeType: 'image/png', + signature: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], + length: 1024 * 1024, + ), + ( + fileName: 'cover.jpeg', + mimeType: 'image/jpeg', + signature: [0xff, 0xd8], + length: 256, + ), + ( + fileName: 'cover.image', + mimeType: 'image/png', + signature: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], + length: 512, + ), + ]) { + test( + 'Opus picture block preserves every byte of ${cover.fileName}', + () async { + final imageBytes = List.generate( + cover.length, + (index) => index & 0xff, + ); + imageBytes.setRange(0, cover.signature.length, cover.signature); + final coverFile = File('${directory.path}/${cover.fileName}'); + await coverFile.writeAsBytes(imageBytes); + final opusPath = '${directory.path}/track.opus'; + final audioBytes = [0x4f, 0x67, 0x67, 0x53]; + await File(opusPath).writeAsBytes(audioBytes); + List? pictureBytes; + + expect( + await FFmpegService.embedMetadataToOpus( + opusPath: opusPath, + coverPath: coverFile.path, + execute: (arguments) async { + const prefix = 'METADATA_BLOCK_PICTURE='; + final argument = arguments.singleWhere( + (argument) => argument.startsWith(prefix), + ); + pictureBytes = base64Decode(argument.substring(prefix.length)); + await File( + arguments[arguments.length - 2], + ).writeAsBytes(audioBytes); + return FFmpegResult(success: true, returnCode: 0, output: ''); + }, + ), + opusPath, + ); + + final mimeBytes = utf8.encode(cover.mimeType); + expect(pictureBytes, [ + 0, 0, 0, 3, // Front cover. + 0, 0, 0, mimeBytes.length, + ...mimeBytes, + ...List.filled(20, 0), // Empty description and image dimensions. + (cover.length >> 24) & 0xff, + (cover.length >> 16) & 0xff, + (cover.length >> 8) & 0xff, + cover.length & 0xff, + ...imageBytes, + ]); + expect(await File(opusPath).readAsBytes(), audioBytes); + }, + ); + } + for (final scenario in [ ( name: 'ISRC only',