diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index eaed88b7..fbba3e2b 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -97,7 +97,7 @@ _scrcpy() { return ;; --audio-codec) - COMPREPLY=($(compgen -W 'opus aac raw' -- "$cur")) + COMPREPLY=($(compgen -W 'opus aac flac raw' -- "$cur")) return ;; --video-source) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 4b1e5868..75d8a1a6 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -11,7 +11,7 @@ arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--audio-bit-rate=[Encode the audio at the given bit-rate]' '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]' - '--audio-codec=[Select the audio codec]:codec:(opus aac raw)' + '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-encoder=[Use a specific MediaCodec audio encoder]' '--audio-source=[Select the audio source]:source:(output mic)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 2901d014..4d784a97 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -35,7 +35,7 @@ Default is 50. .TP .BI "\-\-audio\-codec " name -Select an audio codec (opus, aac or raw). +Select an audio codec (opus, aac, flac or raw). Default is opus. diff --git a/app/src/cli.c b/app/src/cli.c index 462465fa..1e716d6d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -152,7 +152,7 @@ static const struct sc_option options[] = { .longopt_id = OPT_AUDIO_CODEC, .longopt = "audio-codec", .argdesc = "name", - .text = "Select an audio codec (opus, aac or raw).\n" + .text = "Select an audio codec (opus, aac, flac or raw).\n" "Default is opus.", }, { @@ -1626,6 +1626,9 @@ get_record_format(const char *name) { if (!strcmp(name, "aac")) { return SC_RECORD_FORMAT_AAC; } + if (!strcmp(name, "flac")) { + return SC_RECORD_FORMAT_FLAC; + } return 0; } @@ -1695,11 +1698,15 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) { *codec = SC_CODEC_AAC; return true; } + if (!strcmp(optarg, "flac")) { + *codec = SC_CODEC_FLAC; + return true; + } if (!strcmp(optarg, "raw")) { *codec = SC_CODEC_RAW; return true; } - LOGE("Unsupported audio codec: %s (expected opus, aac or raw)", optarg); + LOGE("Unsupported audio codec: %s (expected opus, aac, flac or raw)", optarg); return false; } @@ -2376,6 +2383,16 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], "(try with --audio-codec=aac)"); return false; } + if (opts->record_format == SC_RECORD_FORMAT_FLAC + && opts->audio_codec != SC_CODEC_FLAC) { + LOGE("Recording to FLAC file requires a FLAC audio stream " + "(try with --audio-codec=flac)"); + return false; + } + } + + if (opts->audio_codec == SC_CODEC_FLAC && opts->audio_bit_rate) { + LOGW("--audio-bit-rate is ignored for FLAC audio codec"); } if (opts->audio_codec == SC_CODEC_RAW) { diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 943f72b6..c9ee8f3c 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -25,7 +25,8 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) { #define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII #define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII #define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII -#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII" +#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac" in ASCII +#define SC_CODEC_ID_FLAC UINT32_C(0x666c6163) // "flac" in ASCII #define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII switch (codec_id) { case SC_CODEC_ID_H264: @@ -43,6 +44,8 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) { return AV_CODEC_ID_OPUS; case SC_CODEC_ID_AAC: return AV_CODEC_ID_AAC; + case SC_CODEC_ID_FLAC: + return AV_CODEC_ID_FLAC; case SC_CODEC_ID_RAW: return AV_CODEC_ID_PCM_S16LE; default: @@ -207,6 +210,11 @@ run_demuxer(void *data) { codec_ctx->channels = 2; #endif codec_ctx->sample_rate = 48000; + + if (raw_codec_id == SC_CODEC_ID_FLAC) { + // The sample_fmt is not set by the FLAC decoder + codec_ctx->sample_fmt = AV_SAMPLE_FMT_S16; + } } if (avcodec_open2(codec_ctx, codec, NULL) < 0) { diff --git a/app/src/options.h b/app/src/options.h index 18b437d8..91433894 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -25,6 +25,7 @@ enum sc_record_format { SC_RECORD_FORMAT_MKA, SC_RECORD_FORMAT_OPUS, SC_RECORD_FORMAT_AAC, + SC_RECORD_FORMAT_FLAC, }; static inline bool @@ -32,7 +33,8 @@ sc_record_format_is_audio_only(enum sc_record_format fmt) { return fmt == SC_RECORD_FORMAT_M4A || fmt == SC_RECORD_FORMAT_MKA || fmt == SC_RECORD_FORMAT_OPUS - || fmt == SC_RECORD_FORMAT_AAC; + || fmt == SC_RECORD_FORMAT_AAC + || fmt == SC_RECORD_FORMAT_FLAC; } enum sc_codec { @@ -41,6 +43,7 @@ enum sc_codec { SC_CODEC_AV1, SC_CODEC_OPUS, SC_CODEC_AAC, + SC_CODEC_FLAC, SC_CODEC_RAW, }; diff --git a/app/src/recorder.c b/app/src/recorder.c index 23c8b497..d13b122a 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -69,6 +69,8 @@ sc_recorder_get_format_name(enum sc_record_format format) { return "matroska"; case SC_RECORD_FORMAT_OPUS: return "opus"; + case SC_RECORD_FORMAT_FLAC: + return "flac"; default: return NULL; } diff --git a/app/src/server.c b/app/src/server.c index 2b3439da..d4726c2a 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -178,6 +178,8 @@ sc_server_get_codec_name(enum sc_codec codec) { return "opus"; case SC_CODEC_AAC: return "aac"; + case SC_CODEC_FLAC: + return "flac"; case SC_CODEC_RAW: return "raw"; default: diff --git a/doc/audio.md b/doc/audio.md index cb6cde95..ecae4468 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -62,12 +62,13 @@ scrcpy --audio-source=mic --no-video --no-playback --record=file.opus ## Codec -The audio codec can be selected. The possible values are `opus` (default), `aac` -and `raw` (uncompressed PCM 16-bit LE): +The audio codec can be selected. The possible values are `opus` (default), +`aac`, `flac` and `raw` (uncompressed PCM 16-bit LE): ```bash scrcpy --audio-codec=opus # default scrcpy --audio-codec=aac +scrcpy --audio-codec=flac scrcpy --audio-codec=raw ``` @@ -80,7 +81,14 @@ then your device has no Opus encoder: try `scrcpy --audio-codec=aac`. For advanced usage, to pass arbitrary parameters to the [`MediaFormat`], check `--audio-codec-options` in the manpage or in `scrcpy --help`. +For example, to change the [FLAC compression level]: + +```bash +scrcpy --audio-codec=flac --audio-codec-options=flac-compression-level=8 +``` + [`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat +[FLAC compression level]: https://developer.android.com/reference/android/media/MediaFormat#KEY_FLAC_COMPRESSION_LEVEL ## Encoder diff --git a/doc/recording.md b/doc/recording.md index 76a7efd6..bcdc7633 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -18,7 +18,8 @@ To record only the audio: ```bash scrcpy --no-video --record=file.opus scrcpy --no-video --audio-codec=aac --record=file.aac -# .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac +scrcpy --no-video --audio-codec=flac --record=file.flac +# .m4a/.mp4 and .mka/.mkv are also supported for opus, aac and flac ``` Timestamps are captured on the device, so [packet delay variation] does not diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java b/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java index 1f3b07a0..b4ea3680 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java @@ -5,6 +5,7 @@ import android.media.MediaFormat; public enum AudioCodec implements Codec { OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS), AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC), + FLAC(0x66_6c_61_63, "flac", MediaFormat.MIMETYPE_AUDIO_FLAC), RAW(0x00_72_61_77, "raw", MediaFormat.MIMETYPE_AUDIO_RAW); private final int id; // 4-byte ASCII representation of the name diff --git a/server/src/main/java/com/genymobile/scrcpy/Streamer.java b/server/src/main/java/com/genymobile/scrcpy/Streamer.java index c3f1c6ee..8b6c9dcc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Streamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/Streamer.java @@ -5,6 +5,7 @@ import android.media.MediaCodec; import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.Arrays; public final class Streamer { @@ -29,6 +30,7 @@ public final class Streamer { public Codec getCodec() { return codec; } + public void writeAudioHeader() throws IOException { if (sendCodecMeta) { ByteBuffer buffer = ByteBuffer.allocate(4); @@ -61,8 +63,12 @@ public final class Streamer { } public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException { - if (config && codec == AudioCodec.OPUS) { - fixOpusConfigPacket(buffer); + if (config) { + if (codec == AudioCodec.OPUS) { + fixOpusConfigPacket(buffer); + } else if (codec == AudioCodec.FLAC) { + fixFlacConfigPacket(buffer); + } } if (sendFrameMeta) { @@ -140,4 +146,41 @@ public final class Streamer { // Set the buffer to point to the OPUS header slice buffer.limit(buffer.position() + size); } + + private static void fixFlacConfigPacket(ByteBuffer buffer) throws IOException { + // 00000000 66 4c 61 43 00 00 00 22 |fLaC..." | + // -------------- BELOW IS THE PART WE MUST PUT AS EXTRADATA ------------------- + // 00000000 10 00 10 00 00 00 00 00 | ........| + // 00000010 00 00 0b b8 02 f0 00 00 00 00 00 00 00 00 00 00 |................| + // 00000020 00 00 00 00 00 00 00 00 00 00 |.......... | + // ------------------------------------------------------------------------------ + // 00000020 84 00 00 28 20 00 | ...( .| + // 00000030 00 00 72 65 66 65 72 65 6e 63 65 20 6c 69 62 46 |..reference libF| + // 00000040 4c 41 43 20 31 2e 33 2e 32 20 32 30 32 32 31 30 |LAC 1.3.2 202210| + // 00000050 32 32 00 00 00 00 |22....| + // + // + + if (buffer.remaining() < 8) { + throw new IOException("Not enough data in FLAC config packet"); + } + + final byte[] flacHeaderId = {'f', 'L', 'a', 'C'}; + byte[] idBuffer = new byte[4]; + buffer.get(idBuffer); + if (!Arrays.equals(idBuffer, flacHeaderId)) { + throw new IOException("FLAC header not found"); + } + + // The size is in big-endian + buffer.order(ByteOrder.BIG_ENDIAN); + + int size = buffer.getInt(); + if (buffer.remaining() < size) { + throw new IOException("Not enough data in FLAC header (invalid size: " + size + ")"); + } + + // Set the buffer to point to the FLAC header slice + buffer.limit(buffer.position() + size); + } }