From c8ce9a7faea441d45e287cfab5749b2525b675d2 Mon Sep 17 00:00:00 2001 From: tolriq Date: Tue, 11 Nov 2025 10:55:32 +0100 Subject: [PATCH 1/2] Use streaminfo block for FLAC files. Some files do not have the sample size in each frame headers, in that case, ffmpeg needs the streaminfo data to properly decode them. --- .../decoder/ffmpeg/FfmpegAudioDecoder.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegAudioDecoder.java b/libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegAudioDecoder.java index d558c639009..67d20c5361b 100644 --- a/libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegAudioDecoder.java +++ b/libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegAudioDecoder.java @@ -190,12 +190,57 @@ private static byte[] getExtraData(String mimeType, List initializationD return getAlacExtraData(initializationData); case MimeTypes.AUDIO_VORBIS: return getVorbisExtraData(initializationData); + case MimeTypes.AUDIO_FLAC: + return getFlacExtraData(initializationData); default: // Other codecs do not require extra data. return null; } } + @Nullable + private static byte[] getFlacExtraData(List initializationData) { + for (int i = 0; i < initializationData.size(); i++) { + byte[] out = extractFlacStreamInfo(initializationData.get(i)); + if (out != null) { + return out; + } + } + return null; + } + + @Nullable + private static byte[] extractFlacStreamInfo(byte[] data) { + final int STREAMINFO_LEN = 34; + int off = 0; + + if (data.length >= 4 + && data[0] == (byte) 'f' + && data[1] == (byte) 'L' + && data[2] == (byte) 'a' + && data[3] == (byte) 'C') { + off = 4; + } + + if (data.length - off == STREAMINFO_LEN) { + byte[] out = new byte[STREAMINFO_LEN]; + System.arraycopy(data, off, out, 0, STREAMINFO_LEN); + return out; + } + + if (data.length >= off + 4) { + int type = data[off] & 0x7F; + int len = ((data[off + 1] & 0xFF) << 16) | ((data[off + 2] & 0xFF) << 8) | (data[off + 3] & 0xFF); + if (type == 0 && len == STREAMINFO_LEN && data.length >= off + 4 + STREAMINFO_LEN) { + byte[] out = new byte[STREAMINFO_LEN]; + System.arraycopy(data, off + 4, out, 0, STREAMINFO_LEN); + return out; + } + } + + return null; + } + private static byte[] getAlacExtraData(List initializationData) { // FFmpeg's ALAC decoder expects an ALAC atom, which contains the ALAC "magic cookie", as extra // data. initializationData[0] contains only the magic cookie, and so we need to package it into From d71c5063a1786f119ee52bcfc2965f2094a659eb Mon Sep 17 00:00:00 2001 From: Rohit Singh Date: Tue, 11 Nov 2025 14:36:55 +0000 Subject: [PATCH 2/2] Refactor based on code conventio ns --- RELEASENOTES.md | 4 + .../decoder/ffmpeg/FfmpegAudioDecoder.java | 114 +++++++++++------- 2 files changed, 75 insertions(+), 43 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8593feba5ca..1c3f6307e31 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -57,6 +57,10 @@ * Smooth Streaming extension: * RTSP extension: * Decoder extensions (FFmpeg, VP9, AV1, etc.): + * FFmpeg extension: Fix an issue that prevented some FLAC files from + playing by ensuring the `STREAMINFO` block is correctly parsed and + passed to the decoder + ([#2887](https://github.com/androidx/media/issues/2887)). * MIDI extension: * Leanback extension: * Cast extension: diff --git a/libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegAudioDecoder.java b/libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegAudioDecoder.java index 67d20c5361b..5b692180c06 100644 --- a/libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegAudioDecoder.java +++ b/libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegAudioDecoder.java @@ -39,6 +39,12 @@ private static final int AUDIO_DECODER_ERROR_INVALID_DATA = -1; private static final int AUDIO_DECODER_ERROR_OTHER = -2; + // FLAC parsing constants + private static final byte[] flacStreamMarker = {'f', 'L', 'a', 'C'}; + private static final int FLAC_METADATA_TYPE_STREAM_INFO = 0; + private static final int FLAC_METADATA_BLOCK_HEADER_SIZE = 4; + private static final int FLAC_STREAM_INFO_DATA_SIZE = 34; + private final String codecName; @Nullable private final byte[] extraData; private final @C.PcmEncoding int encoding; @@ -198,49 +204,6 @@ private static byte[] getExtraData(String mimeType, List initializationD } } - @Nullable - private static byte[] getFlacExtraData(List initializationData) { - for (int i = 0; i < initializationData.size(); i++) { - byte[] out = extractFlacStreamInfo(initializationData.get(i)); - if (out != null) { - return out; - } - } - return null; - } - - @Nullable - private static byte[] extractFlacStreamInfo(byte[] data) { - final int STREAMINFO_LEN = 34; - int off = 0; - - if (data.length >= 4 - && data[0] == (byte) 'f' - && data[1] == (byte) 'L' - && data[2] == (byte) 'a' - && data[3] == (byte) 'C') { - off = 4; - } - - if (data.length - off == STREAMINFO_LEN) { - byte[] out = new byte[STREAMINFO_LEN]; - System.arraycopy(data, off, out, 0, STREAMINFO_LEN); - return out; - } - - if (data.length >= off + 4) { - int type = data[off] & 0x7F; - int len = ((data[off + 1] & 0xFF) << 16) | ((data[off + 2] & 0xFF) << 8) | (data[off + 3] & 0xFF); - if (type == 0 && len == STREAMINFO_LEN && data.length >= off + 4 + STREAMINFO_LEN) { - byte[] out = new byte[STREAMINFO_LEN]; - System.arraycopy(data, off + 4, out, 0, STREAMINFO_LEN); - return out; - } - } - - return null; - } - private static byte[] getAlacExtraData(List initializationData) { // FFmpeg's ALAC decoder expects an ALAC atom, which contains the ALAC "magic cookie", as extra // data. initializationData[0] contains only the magic cookie, and so we need to package it into @@ -272,6 +235,71 @@ private static byte[] getVorbisExtraData(List initializationData) { return extraData; } + @Nullable + private static byte[] getFlacExtraData(List initializationData) { + for (int i = 0; i < initializationData.size(); i++) { + byte[] out = extractFlacStreamInfo(initializationData.get(i)); + if (out != null) { + return out; + } + } + return null; + } + + @Nullable + private static byte[] extractFlacStreamInfo(byte[] data) { + int offset = 0; + if (arrayStartsWith(data, flacStreamMarker)) { + offset = flacStreamMarker.length; + } + + + if (data.length - offset == FLAC_STREAM_INFO_DATA_SIZE) { + byte[] streamInfo = new byte[FLAC_STREAM_INFO_DATA_SIZE]; + System.arraycopy(data, offset, streamInfo, 0, FLAC_STREAM_INFO_DATA_SIZE); + return streamInfo; + } + + if (data.length >= offset + FLAC_METADATA_BLOCK_HEADER_SIZE) { + int type = data[offset] & 0x7F; + int length = + ((data[offset + 1] & 0xFF) << 16) + | ((data[offset + 2] & 0xFF) << 8) + | (data[offset + 3] & 0xFF); + + if (type == FLAC_METADATA_TYPE_STREAM_INFO + && length == FLAC_STREAM_INFO_DATA_SIZE + && data.length + >= offset + + FLAC_METADATA_BLOCK_HEADER_SIZE + + FLAC_STREAM_INFO_DATA_SIZE) { + byte[] streamInfo = new byte[FLAC_STREAM_INFO_DATA_SIZE]; + System.arraycopy( + data, + offset + FLAC_METADATA_BLOCK_HEADER_SIZE, + streamInfo, + 0, + FLAC_STREAM_INFO_DATA_SIZE); + return streamInfo; + } + } + + return null; + } + + private static boolean arrayStartsWith(byte[] data, byte[] prefix) { + if (data.length < prefix.length) { + return false; + } + for (int i = 0; i < prefix.length; i++) { + if (data[i] != prefix[i]) { + return false; + } + } + return true; + } + + private native long ffmpegInitialize( String codecName, @Nullable byte[] extraData,