From d706c5df3991a35b5f98058bfd2f640bcef05ea7 Mon Sep 17 00:00:00 2001 From: Ivan Gorinov Date: Wed, 10 Oct 2018 22:12:36 -0700 Subject: [PATCH 01/16] Enable video output file, with pts set by server --- app/src/decoder.c | 101 +++++++++++++++++- app/src/decoder.h | 9 +- app/src/main.c | 11 +- app/src/scrcpy.c | 4 +- app/src/scrcpy.h | 1 + gradle.properties | 1 + .../com/genymobile/scrcpy/ScreenEncoder.java | 9 ++ 7 files changed, 126 insertions(+), 10 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index d26d4cf8..10c6619c 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -1,6 +1,7 @@ #include "decoder.h" #include +#include #include #include #include @@ -14,9 +15,50 @@ #define BUFSIZE 0x10000 +static AVRational us = {1, 1000000}; + +static inline uint64_t from_be(uint8_t *b, int size) +{ + uint64_t x = 0; + int i; + + for (i = 0; i < size; i += 1) { + x <<= 8; + x |= b[i]; + } + + return x; +} + +#define HEADER_SIZE 16 + static int read_packet(void *opaque, uint8_t *buf, int buf_size) { struct decoder *decoder = opaque; - return net_recv(decoder->video_socket, buf, buf_size); + uint8_t header[HEADER_SIZE]; + int remaining; + int ret; + + remaining = decoder->remaining; + if (remaining == 0) { + ret = net_recv(decoder->video_socket, header, HEADER_SIZE); + if (ret <= 0) + return ret; + + decoder->pts = from_be(header, 8); + remaining = from_be(header + 12, 4); + } + + if (buf_size > remaining) + buf_size = remaining; + + ret = net_recv(decoder->video_socket, buf, buf_size); + if (ret <= 0) + return ret; + + remaining -= ret; + decoder->remaining = remaining; + + return ret; } // set the decoded frame as ready for rendering, and notify @@ -40,6 +82,7 @@ static void notify_stopped(void) { static int run_decoder(void *data) { struct decoder *decoder = data; + int ret; AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); if (!codec) { @@ -86,16 +129,55 @@ static int run_decoder(void *data) { goto run_finally_free_avio_ctx; } + AVStream *outstream = NULL; + AVFormatContext *output_ctx = NULL; + if (decoder->out_filename) { + avformat_alloc_output_context2(&output_ctx, NULL, NULL, decoder->out_filename); + if (!output_ctx) { + LOGE("Could not allocate output format context"); + goto run_finally_free_avio_ctx; + } else { + outstream = avformat_new_stream(output_ctx, codec); + if (!outstream) { + LOGE("Could not allocate output stream"); + goto run_finally_free_output_ctx; + } + outstream->codec = avcodec_alloc_context3(codec); + outstream->codec->pix_fmt = AV_PIX_FMT_YUV420P; + outstream->codec->width = decoder->frame_size.width; + outstream->codec->height = decoder->frame_size.height; + outstream->time_base = (AVRational) {1, 60}; + outstream->codec->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + ret = avio_open(&output_ctx->pb, decoder->out_filename, AVIO_FLAG_WRITE); + if (ret < 0) { + LOGE("Failed to open output file"); + goto run_finally_free_output_ctx; + } + ret = avformat_write_header(output_ctx, NULL); + if (ret < 0) { + LOGE("Error writing output header"); + avio_closep(&output_ctx->pb); + goto run_finally_free_output_ctx; + } + } + } + AVPacket packet; av_init_packet(&packet); packet.data = NULL; packet.size = 0; while (!av_read_frame(format_ctx, &packet)) { + + if (output_ctx) { + packet.pts = decoder->pts; + av_packet_rescale_ts(&packet, us, outstream->time_base); + ret = av_write_frame(output_ctx, &packet); + } + // the new decoding/encoding API has been introduced by: // #if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 37, 0) - int ret; if ((ret = avcodec_send_packet(codec_ctx, &packet)) < 0) { LOGE("Could not send video packet: %d", ret); goto run_quit; @@ -125,6 +207,7 @@ static int run_decoder(void *data) { packet.data += len; } #endif + av_packet_unref(&packet); if (avio_ctx->eof_reached) { @@ -135,7 +218,14 @@ static int run_decoder(void *data) { LOGD("End of frames"); run_quit: + if (output_ctx) { + ret = av_write_trailer(output_ctx); + avio_closep(&output_ctx->pb); + } avformat_close_input(&format_ctx); +run_finally_free_output_ctx: + if (output_ctx) + avformat_free_context(output_ctx); run_finally_free_avio_ctx: av_freep(&avio_ctx); run_finally_free_format_ctx: @@ -149,20 +239,21 @@ run_end: return 0; } -void decoder_init(struct decoder *decoder, struct frames *frames, socket_t video_socket) { +void decoder_init(struct decoder *decoder, struct frames *frames, socket_t video_socket, struct size frame_size) { decoder->frames = frames; decoder->video_socket = video_socket; + decoder->frame_size = frame_size; } -SDL_bool decoder_start(struct decoder *decoder) { +SDL_bool decoder_start(struct decoder *decoder, const char *out_filename) { LOGD("Starting decoder thread"); + decoder->out_filename = out_filename; decoder->thread = SDL_CreateThread(run_decoder, "video_decoder", decoder); if (!decoder->thread) { LOGC("Could not start decoder thread"); return SDL_FALSE; } - return SDL_TRUE; } diff --git a/app/src/decoder.h b/app/src/decoder.h index 87346114..1f70c2bb 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -4,19 +4,24 @@ #include #include +#include "common.h" #include "net.h" struct frames; struct decoder { + uint64_t pts; struct frames *frames; socket_t video_socket; SDL_Thread *thread; SDL_mutex *mutex; + const char *out_filename; + struct size frame_size; + int remaining; }; -void decoder_init(struct decoder *decoder, struct frames *frames, socket_t video_socket); -SDL_bool decoder_start(struct decoder *decoder); +void decoder_init(struct decoder *decoder, struct frames *frames, socket_t video_socket, struct size frame_size); +SDL_bool decoder_start(struct decoder *decoder, const char *out_filename); void decoder_stop(struct decoder *decoder); void decoder_join(struct decoder *decoder); diff --git a/app/src/main.c b/app/src/main.c index e1d6782e..11129138 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -11,6 +11,7 @@ struct args { const char *serial; const char *crop; + const char *out_filename; SDL_bool fullscreen; SDL_bool help; SDL_bool version; @@ -49,6 +50,9 @@ static void usage(const char *arg0) { " is preserved.\n" " Default is %d%s.\n" "\n" + " -o, --output-file\n" + " Write video output to file.\n" + "\n" " -p, --port port\n" " Set the TCP port the client listens on.\n" " Default is %d.\n" @@ -207,6 +211,7 @@ static SDL_bool parse_args(struct args *args, int argc, char *argv[]) { {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, {"max-size", required_argument, NULL, 'm'}, + {"output-file", required_argument, NULL, 'o'}, {"port", required_argument, NULL, 'p'}, {"serial", required_argument, NULL, 's'}, {"show-touches", no_argument, NULL, 't'}, @@ -214,7 +219,7 @@ static SDL_bool parse_args(struct args *args, int argc, char *argv[]) { {NULL, 0, NULL, 0 }, }; int c; - while ((c = getopt_long(argc, argv, "b:c:fhm:p:s:tv", long_options, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "b:c:fhm:o:p:s:tv", long_options, NULL)) != -1) { switch (c) { case 'b': if (!parse_bit_rate(optarg, &args->bit_rate)) { @@ -235,6 +240,9 @@ static SDL_bool parse_args(struct args *args, int argc, char *argv[]) { return SDL_FALSE; } break; + case 'o': + args->out_filename = optarg; + break; case 'p': if (!parse_port(optarg, &args->port)) { return SDL_FALSE; @@ -310,6 +318,7 @@ int main(int argc, char *argv[]) { .serial = args.serial, .crop = args.crop, .port = args.port, + .out_filename = args.out_filename, .max_size = args.max_size, .bit_rate = args.bit_rate, .show_touches = args.show_touches, diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 54a7f993..2158c4f1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -193,11 +193,11 @@ SDL_bool scrcpy(const struct scrcpy_options *options) { goto finally_destroy_frames; } - decoder_init(&decoder, &frames, device_socket); + decoder_init(&decoder, &frames, device_socket, frame_size); // now we consumed the header values, the socket receives the video stream // start the decoder - if (!decoder_start(&decoder)) { + if (!decoder_start(&decoder, options->out_filename)) { ret = SDL_FALSE; server_stop(&server); goto finally_destroy_file_handler; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index f64d4c02..4716c587 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -6,6 +6,7 @@ struct scrcpy_options { const char *serial; const char *crop; + const char *out_filename; Uint16 port; Uint16 max_size; Uint32 bit_rate; diff --git a/gradle.properties b/gradle.properties index aac7c9b4..89196d13 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,3 +15,4 @@ org.gradle.jvmargs=-Xmx1536m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true + diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 636bbb00..c5cabfdb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.SurfaceControl; import android.graphics.Rect; +import android.media.MediaMuxer; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; @@ -80,6 +81,8 @@ public class ScreenEncoder implements Device.RotationListener { private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException { boolean eof = false; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + ByteBuffer bBuffer = ByteBuffer.allocate(16); + while (!consumeRotationChange() && !eof) { int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; @@ -90,6 +93,12 @@ public class ScreenEncoder implements Device.RotationListener { } if (outputBufferId >= 0) { ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); + bBuffer.position(0); + bBuffer.putLong(bufferInfo.presentationTimeUs); + bBuffer.putInt(bufferInfo.flags); + bBuffer.putInt(codecBuffer.remaining()); + bBuffer.position(0); + IO.writeFully(fd, bBuffer); IO.writeFully(fd, codecBuffer); } } finally { From 27686e93615e31bd77f4fab3912d9a98a146aa9d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 9 Nov 2018 12:21:17 +0100 Subject: [PATCH 02/16] Add recorder Implement recording in a separate "class". --- app/meson.build | 1 + app/src/decoder.c | 70 +++++++++++---------------------- app/src/decoder.h | 8 ++-- app/src/recorder.c | 98 ++++++++++++++++++++++++++++++++++++++++++++++ app/src/recorder.h | 24 ++++++++++++ app/src/scrcpy.c | 22 +++++++++-- 6 files changed, 168 insertions(+), 55 deletions(-) create mode 100644 app/src/recorder.c create mode 100644 app/src/recorder.h diff --git a/app/meson.build b/app/meson.build index 3e309cdf..07122a24 100644 --- a/app/meson.build +++ b/app/meson.build @@ -12,6 +12,7 @@ src = [ 'src/input_manager.c', 'src/lock_util.c', 'src/net.c', + 'src/recorder.c', 'src/scrcpy.c', 'src/screen.c', 'src/server.c', diff --git a/app/src/decoder.c b/app/src/decoder.c index 10c6619c..6543acb4 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -12,11 +12,10 @@ #include "frames.h" #include "lock_util.h" #include "log.h" +#include "recorder.h" #define BUFSIZE 0x10000 -static AVRational us = {1, 1000000}; - static inline uint64_t from_be(uint8_t *b, int size) { uint64_t x = 0; @@ -40,6 +39,7 @@ static int read_packet(void *opaque, uint8_t *buf, int buf_size) { remaining = decoder->remaining; if (remaining == 0) { + // FIXME what if only part of the header is available? ret = net_recv(decoder->video_socket, header, HEADER_SIZE); if (ret <= 0) return ret; @@ -82,7 +82,6 @@ static void notify_stopped(void) { static int run_decoder(void *data) { struct decoder *decoder = data; - int ret; AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); if (!codec) { @@ -129,37 +128,10 @@ static int run_decoder(void *data) { goto run_finally_free_avio_ctx; } - AVStream *outstream = NULL; - AVFormatContext *output_ctx = NULL; - if (decoder->out_filename) { - avformat_alloc_output_context2(&output_ctx, NULL, NULL, decoder->out_filename); - if (!output_ctx) { - LOGE("Could not allocate output format context"); - goto run_finally_free_avio_ctx; - } else { - outstream = avformat_new_stream(output_ctx, codec); - if (!outstream) { - LOGE("Could not allocate output stream"); - goto run_finally_free_output_ctx; - } - outstream->codec = avcodec_alloc_context3(codec); - outstream->codec->pix_fmt = AV_PIX_FMT_YUV420P; - outstream->codec->width = decoder->frame_size.width; - outstream->codec->height = decoder->frame_size.height; - outstream->time_base = (AVRational) {1, 60}; - outstream->codec->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; - ret = avio_open(&output_ctx->pb, decoder->out_filename, AVIO_FLAG_WRITE); - if (ret < 0) { - LOGE("Failed to open output file"); - goto run_finally_free_output_ctx; - } - ret = avformat_write_header(output_ctx, NULL); - if (ret < 0) { - LOGE("Error writing output header"); - avio_closep(&output_ctx->pb); - goto run_finally_free_output_ctx; - } - } + if (decoder->recorder && + !recorder_open(decoder->recorder, codec)) { + LOGE("Could not open recorder"); + goto run_finally_close_input; } AVPacket packet; @@ -168,16 +140,21 @@ static int run_decoder(void *data) { packet.size = 0; while (!av_read_frame(format_ctx, &packet)) { - - if (output_ctx) { + if (decoder->recorder) { packet.pts = decoder->pts; - av_packet_rescale_ts(&packet, us, outstream->time_base); - ret = av_write_frame(output_ctx, &packet); + // no need to rescale with av_packet_rescale_ts(), the timestamps + // are in microseconds both in input and output + if (!recorder_write(decoder->recorder, &packet)) { + LOGE("Could not write frame to output file"); + av_packet_unref(&packet); + goto run_quit; + } } // the new decoding/encoding API has been introduced by: // #if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 37, 0) + int ret; if ((ret = avcodec_send_packet(codec_ctx, &packet)) < 0) { LOGE("Could not send video packet: %d", ret); goto run_quit; @@ -218,14 +195,11 @@ static int run_decoder(void *data) { LOGD("End of frames"); run_quit: - if (output_ctx) { - ret = av_write_trailer(output_ctx); - avio_closep(&output_ctx->pb); + if (decoder->recorder) { + recorder_close(decoder->recorder); } +run_finally_close_input: avformat_close_input(&format_ctx); -run_finally_free_output_ctx: - if (output_ctx) - avformat_free_context(output_ctx); run_finally_free_avio_ctx: av_freep(&avio_ctx); run_finally_free_format_ctx: @@ -239,16 +213,16 @@ run_end: return 0; } -void decoder_init(struct decoder *decoder, struct frames *frames, socket_t video_socket, struct size frame_size) { +void decoder_init(struct decoder *decoder, struct frames *frames, + socket_t video_socket, struct recorder *recorder) { decoder->frames = frames; decoder->video_socket = video_socket; - decoder->frame_size = frame_size; + decoder->recorder = recorder; } -SDL_bool decoder_start(struct decoder *decoder, const char *out_filename) { +SDL_bool decoder_start(struct decoder *decoder) { LOGD("Starting decoder thread"); - decoder->out_filename = out_filename; decoder->thread = SDL_CreateThread(run_decoder, "video_decoder", decoder); if (!decoder->thread) { LOGC("Could not start decoder thread"); diff --git a/app/src/decoder.h b/app/src/decoder.h index 1f70c2bb..9c673973 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -15,13 +15,13 @@ struct decoder { socket_t video_socket; SDL_Thread *thread; SDL_mutex *mutex; - const char *out_filename; - struct size frame_size; + struct recorder *recorder; int remaining; }; -void decoder_init(struct decoder *decoder, struct frames *frames, socket_t video_socket, struct size frame_size); -SDL_bool decoder_start(struct decoder *decoder, const char *out_filename); +void decoder_init(struct decoder *decoder, struct frames *frames, + socket_t video_socket, struct recorder *recoder); +SDL_bool decoder_start(struct decoder *decoder); void decoder_stop(struct decoder *decoder); void decoder_join(struct decoder *decoder); diff --git a/app/src/recorder.c b/app/src/recorder.c new file mode 100644 index 00000000..5b9bae6d --- /dev/null +++ b/app/src/recorder.c @@ -0,0 +1,98 @@ +#include "recorder.h" + +#include + +#include "config.h" +#include "log.h" + +static const AVOutputFormat *find_mp4_muxer(void) { + void *opaque = NULL; + const AVOutputFormat *oformat; + do { + oformat = av_muxer_iterate(&opaque); + // until null or with name "mp4" + } while (oformat && strcmp(oformat->name, "mp4")); + return oformat; +} + +SDL_bool recorder_init(struct recorder *recorder, const char *filename, + struct size declared_frame_size) { + recorder->filename = SDL_strdup(filename); + if (!recorder->filename) { + LOGE("Cannot strdup filename"); + return SDL_FALSE; + } + + recorder->declared_frame_size = declared_frame_size; + + return SDL_TRUE; +} + +void recorder_destroy(struct recorder *recorder) { + SDL_free(recorder->filename); +} + +SDL_bool recorder_open(struct recorder *recorder, AVCodec *input_codec) { + const AVOutputFormat *mp4 = find_mp4_muxer(); + if (!mp4) { + LOGE("Could not find mp4 muxer"); + return SDL_FALSE; + } + + recorder->ctx = avformat_alloc_context(); + if (!recorder->ctx) { + LOGE("Could not allocate output context"); + return SDL_FALSE; + } + + // contrary to the deprecated API (av_oformat_next()), av_muxer_iterate() + // returns (on purpose) a pointer-to-const, but AVFormatContext.oformat + // still expects a pointer-to-non-const (it has not be updated accordingly) + // + recorder->ctx->oformat = (AVOutputFormat *) mp4; + + AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec); + if (!ostream) { + avformat_free_context(recorder->ctx); + return SDL_FALSE; + } + + ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + ostream->codecpar->codec_id = input_codec->id; + ostream->codecpar->format = AV_PIX_FMT_YUV420P; + ostream->codecpar->width = recorder->declared_frame_size.width; + ostream->codecpar->height = recorder->declared_frame_size.height; + ostream->time_base = (AVRational) {1, 1000000}; // timestamps in us + + int ret = avio_open(&recorder->ctx->pb, recorder->filename, + AVIO_FLAG_WRITE); + if (ret < 0) { + LOGE("Failed to open output file: %s", recorder->filename); + // ostream will be cleaned up during context cleaning + avformat_free_context(recorder->ctx); + return SDL_FALSE; + } + + ret = avformat_write_header(recorder->ctx, NULL); + if (ret < 0) { + LOGE("Failed to write header to %s", recorder->filename); + avio_closep(&recorder->ctx->pb); + avformat_free_context(recorder->ctx); + return SDL_FALSE; + } + + return SDL_TRUE; +} + +void recorder_close(struct recorder *recorder) { + int ret = av_write_trailer(recorder->ctx); + if (ret < 0) { + LOGE("Failed to write trailer to %s", recorder->filename); + } + avio_close(recorder->ctx->pb); + avformat_free_context(recorder->ctx); +} + +SDL_bool recorder_write(struct recorder *recorder, AVPacket *packet) { + return av_write_frame(recorder->ctx, packet) >= 0; +} diff --git a/app/src/recorder.h b/app/src/recorder.h new file mode 100644 index 00000000..9cb17fe0 --- /dev/null +++ b/app/src/recorder.h @@ -0,0 +1,24 @@ +#ifndef RECORDER_H +#define RECORDER_H + +#include +#include + +#include "common.h" + +struct recorder { + char *filename; + AVFormatContext *ctx; + struct size declared_frame_size; +}; + +SDL_bool recorder_init(struct recorder *recoder, const char *filename, + struct size declared_frame_size); +void recorder_destroy(struct recorder *recorder); + +SDL_bool recorder_open(struct recorder *recorder, AVCodec *input_codec); +void recorder_close(struct recorder *recorder); + +SDL_bool recorder_write(struct recorder *recorder, AVPacket *packet); + +#endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 2158c4f1..1f10b676 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -20,6 +20,7 @@ #include "log.h" #include "lock_util.h" #include "net.h" +#include "recorder.h" #include "screen.h" #include "server.h" #include "tiny_xpm.h" @@ -30,6 +31,7 @@ static struct frames frames; static struct decoder decoder; static struct controller controller; static struct file_handler file_handler; +static struct recorder recorder; static struct input_manager input_manager = { .controller = &controller, @@ -193,14 +195,24 @@ SDL_bool scrcpy(const struct scrcpy_options *options) { goto finally_destroy_frames; } - decoder_init(&decoder, &frames, device_socket, frame_size); + struct recorder *rec = NULL; + if (options->out_filename) { + if (!recorder_init(&recorder, options->out_filename, frame_size)) { + ret = SDL_FALSE; + server_stop(&server); + goto finally_destroy_file_handler; + } + rec = &recorder; + } + + decoder_init(&decoder, &frames, device_socket, rec); // now we consumed the header values, the socket receives the video stream // start the decoder - if (!decoder_start(&decoder, options->out_filename)) { + if (!decoder_start(&decoder)) { ret = SDL_FALSE; server_stop(&server); - goto finally_destroy_file_handler; + goto finally_destroy_recorder; } if (!controller_init(&controller, device_socket)) { @@ -246,6 +258,10 @@ finally_destroy_file_handler: file_handler_stop(&file_handler); file_handler_join(&file_handler); file_handler_destroy(&file_handler); +finally_destroy_recorder: + if (options->out_filename) { + recorder_destroy(&recorder); + } finally_destroy_frames: frames_destroy(&frames); finally_destroy_server: From 2cd99e72051fd91c6616743add7126c1c799f0e6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 9 Nov 2018 16:09:15 +0100 Subject: [PATCH 03/16] Only set valid PTS/DTS When the PTS is valid, set both PTS and DTS to avoid FFmpeg warnings. Since configuration packets have no PTS, do not record these packets. --- app/src/decoder.c | 23 +++++++++++++------ app/src/decoder.h | 3 ++- .../com/genymobile/scrcpy/ScreenEncoder.java | 18 ++++++++++++--- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 6543acb4..681a5609 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -15,6 +15,7 @@ #include "recorder.h" #define BUFSIZE 0x10000 +#define MEDIA_CODEC_FLAG_CONFIG 2 // MediaCodec.BUFFER_FLAG_CODEC_CONFIG static inline uint64_t from_be(uint8_t *b, int size) { @@ -45,6 +46,7 @@ static int read_packet(void *opaque, uint8_t *buf, int buf_size) { return ret; decoder->pts = from_be(header, 8); + decoder->buffer_info_flags = from_be(header + 8, 4); remaining = from_be(header + 12, 4); } @@ -141,13 +143,20 @@ static int run_decoder(void *data) { while (!av_read_frame(format_ctx, &packet)) { if (decoder->recorder) { - packet.pts = decoder->pts; - // no need to rescale with av_packet_rescale_ts(), the timestamps - // are in microseconds both in input and output - if (!recorder_write(decoder->recorder, &packet)) { - LOGE("Could not write frame to output file"); - av_packet_unref(&packet); - goto run_quit; + // do not record configuration packets + // (they contain no media data and have no PTS/DTS) + // FIXME do not use MediaCodec specific flags + if (!(decoder->buffer_info_flags & MEDIA_CODEC_FLAG_CONFIG)) { + packet.pts = decoder->pts; + packet.dts = decoder->pts; + + // no need to rescale with av_packet_rescale_ts(), the timestamps + // are in microseconds both in input and output + if (!recorder_write(decoder->recorder, &packet)) { + LOGE("Could not write frame to output file"); + av_packet_unref(&packet); + goto run_quit; + } } } diff --git a/app/src/decoder.h b/app/src/decoder.h index 9c673973..fa1200b4 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -10,12 +10,13 @@ struct frames; struct decoder { - uint64_t pts; struct frames *frames; socket_t video_socket; SDL_Thread *thread; SDL_mutex *mutex; struct recorder *recorder; + uint64_t pts; + uint32_t buffer_info_flags; int remaining; }; diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index c5cabfdb..325febbb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -29,6 +29,7 @@ public class ScreenEncoder implements Device.RotationListener { private int bitRate; private int frameRate; private int iFrameInterval; + private long ptsOrigin; public ScreenEncoder(int bitRate, int frameRate, int iFrameInterval) { this.bitRate = bitRate; @@ -93,11 +94,22 @@ public class ScreenEncoder implements Device.RotationListener { } if (outputBufferId >= 0) { ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); - bBuffer.position(0); - bBuffer.putLong(bufferInfo.presentationTimeUs); + bBuffer.clear(); + + long pts; + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + pts = 0; // non-media data packet + } else { + if (ptsOrigin == 0) { + ptsOrigin = bufferInfo.presentationTimeUs; + } + pts = bufferInfo.presentationTimeUs - ptsOrigin; + } + + bBuffer.putLong(pts); bBuffer.putInt(bufferInfo.flags); bBuffer.putInt(codecBuffer.remaining()); - bBuffer.position(0); + bBuffer.flip(); IO.writeFully(fd, bBuffer); IO.writeFully(fd, codecBuffer); } From 61db5758613f26dd5295b023fb1e3fad67a48b60 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 9 Nov 2018 19:28:28 +0100 Subject: [PATCH 04/16] Decode and push frame before recording Handle display before recording, to reduce latency. --- app/src/decoder.c | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 681a5609..ad7fede8 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -142,24 +142,6 @@ static int run_decoder(void *data) { packet.size = 0; while (!av_read_frame(format_ctx, &packet)) { - if (decoder->recorder) { - // do not record configuration packets - // (they contain no media data and have no PTS/DTS) - // FIXME do not use MediaCodec specific flags - if (!(decoder->buffer_info_flags & MEDIA_CODEC_FLAG_CONFIG)) { - packet.pts = decoder->pts; - packet.dts = decoder->pts; - - // no need to rescale with av_packet_rescale_ts(), the timestamps - // are in microseconds both in input and output - if (!recorder_write(decoder->recorder, &packet)) { - LOGE("Could not write frame to output file"); - av_packet_unref(&packet); - goto run_quit; - } - } - } - // the new decoding/encoding API has been introduced by: // #if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 37, 0) @@ -194,6 +176,24 @@ static int run_decoder(void *data) { } #endif + if (decoder->recorder) { + // do not record configuration packets + // (they contain no media data and have no PTS/DTS) + // FIXME do not use MediaCodec specific flags + if (!(decoder->buffer_info_flags & MEDIA_CODEC_FLAG_CONFIG)) { + packet.pts = decoder->pts; + packet.dts = decoder->pts; + + // no need to rescale with av_packet_rescale_ts(), the timestamps + // are in microseconds both in input and output + if (!recorder_write(decoder->recorder, &packet)) { + LOGE("Could not write frame to output file"); + av_packet_unref(&packet); + goto run_quit; + } + } + } + av_packet_unref(&packet); if (avio_ctx->eof_reached) { From 27e8a9a79d7ba06181daa497456f5a11261655e3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 9 Nov 2018 21:22:41 +0100 Subject: [PATCH 05/16] Assign PTS to the right frame The PTS was read from the socket and set as the current one even before the frame was consumed, so it could be assigned to the previous frame "in advance". Store the PTS for the current frame and the last PTS read from the packet header of the next frame in separate fields. As a side-effect, this fixes the warning on quit: > Application provided invalid, non monotonically increasing dts to > muxer in stream 0: 17164020 >= 17164020 --- app/src/decoder.c | 6 +++++- app/src/decoder.h | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index ad7fede8..83583cc3 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -40,12 +40,16 @@ static int read_packet(void *opaque, uint8_t *buf, int buf_size) { remaining = decoder->remaining; if (remaining == 0) { + // the previous PTS read is now for the current frame + decoder->pts = decoder->next_pts; + // FIXME what if only part of the header is available? ret = net_recv(decoder->video_socket, header, HEADER_SIZE); if (ret <= 0) return ret; - decoder->pts = from_be(header, 8); + // read the PTS for the next frame + decoder->next_pts = from_be(header, 8); decoder->buffer_info_flags = from_be(header + 8, 4); remaining = from_be(header + 12, 4); } diff --git a/app/src/decoder.h b/app/src/decoder.h index fa1200b4..b502b625 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -15,6 +15,7 @@ struct decoder { SDL_Thread *thread; SDL_mutex *mutex; struct recorder *recorder; + uint64_t next_pts; uint64_t pts; uint32_t buffer_info_flags; int remaining; From 475912a39c9caedda545972d405f88494ce12aa8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Nov 2018 00:35:53 +0100 Subject: [PATCH 06/16] Do not transmit MediaCodec flags Since PTS handling has been fixed, the recorder do not associate a PTS to a wrong frame anymore, so PTS of "configuration packets" (which never produce a frame), are never read by the recorder. Therefore, there is no need to ignore them explicitly, so we can remove the MediaCodec flags completely. --- app/src/decoder.c | 27 +++++++------------ app/src/decoder.h | 1 - .../com/genymobile/scrcpy/ScreenEncoder.java | 3 +-- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 83583cc3..d03ec3e6 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -15,7 +15,6 @@ #include "recorder.h" #define BUFSIZE 0x10000 -#define MEDIA_CODEC_FLAG_CONFIG 2 // MediaCodec.BUFFER_FLAG_CODEC_CONFIG static inline uint64_t from_be(uint8_t *b, int size) { @@ -30,7 +29,7 @@ static inline uint64_t from_be(uint8_t *b, int size) return x; } -#define HEADER_SIZE 16 +#define HEADER_SIZE 12 static int read_packet(void *opaque, uint8_t *buf, int buf_size) { struct decoder *decoder = opaque; @@ -50,8 +49,7 @@ static int read_packet(void *opaque, uint8_t *buf, int buf_size) { // read the PTS for the next frame decoder->next_pts = from_be(header, 8); - decoder->buffer_info_flags = from_be(header + 8, 4); - remaining = from_be(header + 12, 4); + remaining = from_be(header + 8, 4); } if (buf_size > remaining) @@ -181,20 +179,15 @@ static int run_decoder(void *data) { #endif if (decoder->recorder) { - // do not record configuration packets - // (they contain no media data and have no PTS/DTS) - // FIXME do not use MediaCodec specific flags - if (!(decoder->buffer_info_flags & MEDIA_CODEC_FLAG_CONFIG)) { - packet.pts = decoder->pts; - packet.dts = decoder->pts; + packet.pts = decoder->pts; + packet.dts = decoder->pts; - // no need to rescale with av_packet_rescale_ts(), the timestamps - // are in microseconds both in input and output - if (!recorder_write(decoder->recorder, &packet)) { - LOGE("Could not write frame to output file"); - av_packet_unref(&packet); - goto run_quit; - } + // no need to rescale with av_packet_rescale_ts(), the timestamps + // are in microseconds both in input and output + if (!recorder_write(decoder->recorder, &packet)) { + LOGE("Could not write frame to output file"); + av_packet_unref(&packet); + goto run_quit; } } diff --git a/app/src/decoder.h b/app/src/decoder.h index b502b625..235afba0 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -17,7 +17,6 @@ struct decoder { struct recorder *recorder; uint64_t next_pts; uint64_t pts; - uint32_t buffer_info_flags; int remaining; }; diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 325febbb..a76ce7a9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -82,7 +82,7 @@ public class ScreenEncoder implements Device.RotationListener { private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException { boolean eof = false; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - ByteBuffer bBuffer = ByteBuffer.allocate(16); + ByteBuffer bBuffer = ByteBuffer.allocate(12); while (!consumeRotationChange() && !eof) { int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); @@ -107,7 +107,6 @@ public class ScreenEncoder implements Device.RotationListener { } bBuffer.putLong(pts); - bBuffer.putInt(bufferInfo.flags); bBuffer.putInt(codecBuffer.remaining()); bBuffer.flip(); IO.writeFully(fd, bBuffer); From d0e090e1f92f943c082c03fcb156db9052c0cc01 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 10 Nov 2018 14:01:57 +0100 Subject: [PATCH 07/16] Reenable custom SDL signal handlers This partially reverts commit f00c6c5b1321860eb6d590928448778ec8f43278. On Ctrl+C, we need to execute cleanup code. For instance, if recording is enabled, we need to write MP4 file trailer on exit. Custom SDL signal handlers were disabled because it leaded to process hanging on Ctrl+C during network calls on initialization, but now it seems to work correctly, the network calls return immediately on signal. --- app/src/scrcpy.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 1f10b676..766c26f2 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -155,10 +155,6 @@ SDL_bool scrcpy(const struct scrcpy_options *options) { SDL_bool ret = SDL_TRUE; - if (!SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1")) { - LOGW("Cannot request to keep default signal handlers"); - } - if (!sdl_init_and_configure()) { ret = SDL_FALSE; goto finally_destroy_server; From e361b49b4acb9c66a60776d8d7bce6b5a399e157 Mon Sep 17 00:00:00 2001 From: yuchenlin Date: Sat, 10 Nov 2018 19:00:30 +0800 Subject: [PATCH 08/16] recorder: use av_oformat_next to support older FFmpeg Signed-off-by: yuchenlin --- app/src/recorder.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 5b9bae6d..84e1abe3 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -6,10 +6,16 @@ #include "log.h" static const AVOutputFormat *find_mp4_muxer(void) { +#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 9, 100) void *opaque = NULL; - const AVOutputFormat *oformat; +#endif + const AVOutputFormat *oformat = NULL; do { +#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 9, 100) oformat = av_muxer_iterate(&opaque); +#else + oformat = av_oformat_next(oformat); +#endif // until null or with name "mp4" } while (oformat && strcmp(oformat->name, "mp4")); return oformat; From b98eb7d0fafd347704a6826d5996c59dd12ee301 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 10 Nov 2018 23:49:19 +0100 Subject: [PATCH 09/16] Support AVStream.codec for old FFmpeg versions AVStream.codec has been deprecated in favor of AVStream.codecpar. Due to the FFmpeg/Libav split, this happened in two separate versions: - 57.33.100 for FFmpeg - 57.5.0 for Libav --- app/src/recorder.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/recorder.c b/app/src/recorder.c index 84e1abe3..2e846e91 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -63,11 +63,25 @@ SDL_bool recorder_open(struct recorder *recorder, AVCodec *input_codec) { return SDL_FALSE; } +// In ffmpeg/doc/APIchanges: +// 2016-04-11 - 6f69f7a / 9200514 - lavf 57.33.100 / 57.5.0 - avformat.h +// Add AVStream.codecpar, deprecate AVStream.codec. +#if (LIBAVFORMAT_VERSION_MICRO >= 100 /* FFmpeg */ && \ + LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 33, 100)) \ + || (LIBAVFORMAT_VERSION_MICRO < 100 && /* Libav */ \ + LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 5, 0)) ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; ostream->codecpar->codec_id = input_codec->id; ostream->codecpar->format = AV_PIX_FMT_YUV420P; ostream->codecpar->width = recorder->declared_frame_size.width; ostream->codecpar->height = recorder->declared_frame_size.height; +#else + ostream->codec->codec_type = AVMEDIA_TYPE_VIDEO; + ostream->codec->codec_id = input_codec->id; + ostream->codec->pix_fmt = AV_PIX_FMT_YUV420P; + ostream->codec->width = recorder->declared_frame_size.width; + ostream->codec->height = recorder->declared_frame_size.height; +#endif ostream->time_base = (AVRational) {1, 1000000}; // timestamps in us int ret = avio_open(&recorder->ctx->pb, recorder->filename, From ebe998cf784a98402716f782551edb7099b333fd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Nov 2018 00:58:29 +0100 Subject: [PATCH 10/16] Move buffer reader functions to buffer_util.h --- app/src/buffer_util.h | 10 ++++++++++ app/src/decoder.c | 18 +++--------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/app/src/buffer_util.h b/app/src/buffer_util.h index 8b2ee279..cfb3fa12 100644 --- a/app/src/buffer_util.h +++ b/app/src/buffer_util.h @@ -15,4 +15,14 @@ static inline void buffer_write32be(Uint8 *buf, Uint32 value) { buf[3] = value; } +static inline Uint32 buffer_read32be(Uint8 *buf) { + return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; +} + +static inline Uint64 buffer_read64be(Uint8 *buf) { + Uint32 msb = buffer_read32be(buf); + Uint32 lsb = buffer_read32be(&buf[4]); + return ((Uint64) msb << 32) | lsb; +} + #endif diff --git a/app/src/decoder.c b/app/src/decoder.c index d03ec3e6..309ddce2 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -8,6 +8,7 @@ #include #include "config.h" +#include "buffer_util.h" #include "events.h" #include "frames.h" #include "lock_util.h" @@ -16,19 +17,6 @@ #define BUFSIZE 0x10000 -static inline uint64_t from_be(uint8_t *b, int size) -{ - uint64_t x = 0; - int i; - - for (i = 0; i < size; i += 1) { - x <<= 8; - x |= b[i]; - } - - return x; -} - #define HEADER_SIZE 12 static int read_packet(void *opaque, uint8_t *buf, int buf_size) { @@ -48,8 +36,8 @@ static int read_packet(void *opaque, uint8_t *buf, int buf_size) { return ret; // read the PTS for the next frame - decoder->next_pts = from_be(header, 8); - remaining = from_be(header + 8, 4); + decoder->next_pts = buffer_read64be(header); + remaining = buffer_read32be(&header[8]); } if (buf_size > remaining) From e562837c0b5ed77ea7f556ea9365cdeda789cd68 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Nov 2018 21:26:48 +0100 Subject: [PATCH 11/16] Avoid partial header reads Use net_recv_all() to avoid partial reads for the "meta" header (this would break the whole stream). --- app/src/decoder.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 309ddce2..655ed9eb 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -30,11 +31,13 @@ static int read_packet(void *opaque, uint8_t *buf, int buf_size) { // the previous PTS read is now for the current frame decoder->pts = decoder->next_pts; - // FIXME what if only part of the header is available? - ret = net_recv(decoder->video_socket, header, HEADER_SIZE); + ret = net_recv_all(decoder->video_socket, header, HEADER_SIZE); if (ret <= 0) return ret; + // no partial read (net_recv_all()) + SDL_assert_release(ret == HEADER_SIZE); + // read the PTS for the next frame decoder->next_pts = buffer_read64be(header); remaining = buffer_read32be(&header[8]); From 70579dc7095582390e0a3297b04c61ff321eedd0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Nov 2018 12:36:08 +0100 Subject: [PATCH 12/16] Wrap receiver state into separate struct For readability, wrap the state of the receiver in a separate struct receiver_state. --- app/src/decoder.c | 58 ++++++++++++++++++++++++++++++----------------- app/src/decoder.h | 8 ++++--- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 655ed9eb..5d945cba 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -22,36 +22,49 @@ static int read_packet(void *opaque, uint8_t *buf, int buf_size) { struct decoder *decoder = opaque; - uint8_t header[HEADER_SIZE]; - int remaining; - int ret; + struct receiver_state *state = &decoder->receiver_state; - remaining = decoder->remaining; - if (remaining == 0) { - // the previous PTS read is now for the current frame - decoder->pts = decoder->next_pts; + // The video stream contains raw packets, without time information. When we + // record, we retrieve the timestamps separately, from a "meta" header + // added by the server before each raw packet. + // + // The "meta" header length is 12 bytes: + // [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ... + // <-------------> <-----> <-----------------------------... + // PTS packet raw packet + // size + // + // It is followed by bytes containing the packet/frame. - ret = net_recv_all(decoder->video_socket, header, HEADER_SIZE); - if (ret <= 0) + if (!state->remaining) { + // the next PTS is now for the current frame + state->pts = state->next_pts; + +#define HEADER_SIZE 12 + uint8_t header[HEADER_SIZE]; + ssize_t ret = net_recv_all(decoder->video_socket, header, HEADER_SIZE); + if (ret <= 0) { return ret; - + } // no partial read (net_recv_all()) SDL_assert_release(ret == HEADER_SIZE); - // read the PTS for the next frame - decoder->next_pts = buffer_read64be(header); - remaining = buffer_read32be(&header[8]); + state->next_pts = buffer_read64be(header); + state->remaining = buffer_read32be(&header[8]); } - if (buf_size > remaining) - buf_size = remaining; + SDL_assert(state->remaining); - ret = net_recv(decoder->video_socket, buf, buf_size); - if (ret <= 0) + if (buf_size > state->remaining) + buf_size = state->remaining; + + ssize_t ret = net_recv(decoder->video_socket, buf, buf_size); + if (ret <= 0) { return ret; + } - remaining -= ret; - decoder->remaining = remaining; + SDL_assert(state->remaining >= ret); + state->remaining -= ret; return ret; } @@ -107,6 +120,9 @@ static int run_decoder(void *data) { goto run_finally_free_format_ctx; } + // initialize the receiver state + decoder->receiver_state.remaining = 0; + AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, decoder, read_packet, NULL, NULL); if (!avio_ctx) { LOGC("Could not allocate avio context"); @@ -170,8 +186,8 @@ static int run_decoder(void *data) { #endif if (decoder->recorder) { - packet.pts = decoder->pts; - packet.dts = decoder->pts; + packet.pts = decoder->receiver_state.pts; + packet.dts = decoder->receiver_state.pts; // no need to rescale with av_packet_rescale_ts(), the timestamps // are in microseconds both in input and output diff --git a/app/src/decoder.h b/app/src/decoder.h index 235afba0..2aeb1824 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -15,9 +15,11 @@ struct decoder { SDL_Thread *thread; SDL_mutex *mutex; struct recorder *recorder; - uint64_t next_pts; - uint64_t pts; - int remaining; + struct receiver_state { + uint64_t next_pts; + uint64_t pts; + size_t remaining; // remaining bytes to receive for the current frame + } receiver_state; }; void decoder_init(struct decoder *decoder, struct frames *frames, From 22bf0c19d6caecb972290e71a922e0653339abaf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Nov 2018 14:03:29 +0100 Subject: [PATCH 13/16] Rename --output-file to --record To record the screen to a local file: scrcpy --record file.mp4 --- app/src/main.c | 21 +++++++++++---------- app/src/scrcpy.c | 6 +++--- app/src/scrcpy.h | 2 +- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index 11129138..65887295 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -11,7 +11,7 @@ struct args { const char *serial; const char *crop; - const char *out_filename; + const char *record_filename; SDL_bool fullscreen; SDL_bool help; SDL_bool version; @@ -50,13 +50,13 @@ static void usage(const char *arg0) { " is preserved.\n" " Default is %d%s.\n" "\n" - " -o, --output-file\n" - " Write video output to file.\n" - "\n" " -p, --port port\n" " Set the TCP port the client listens on.\n" " Default is %d.\n" "\n" + " -r, --record file.mp4\n" + " Record screen to file.\n" + "\n" " -s, --serial\n" " The device serial number. Mandatory only if several devices\n" " are connected to adb.\n" @@ -211,15 +211,15 @@ static SDL_bool parse_args(struct args *args, int argc, char *argv[]) { {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, {"max-size", required_argument, NULL, 'm'}, - {"output-file", required_argument, NULL, 'o'}, {"port", required_argument, NULL, 'p'}, + {"record", required_argument, NULL, 'r'}, {"serial", required_argument, NULL, 's'}, {"show-touches", no_argument, NULL, 't'}, {"version", no_argument, NULL, 'v'}, {NULL, 0, NULL, 0 }, }; int c; - while ((c = getopt_long(argc, argv, "b:c:fhm:o:p:s:tv", long_options, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "b:c:fhm:p:r:s:tv", long_options, NULL)) != -1) { switch (c) { case 'b': if (!parse_bit_rate(optarg, &args->bit_rate)) { @@ -240,14 +240,14 @@ static SDL_bool parse_args(struct args *args, int argc, char *argv[]) { return SDL_FALSE; } break; - case 'o': - args->out_filename = optarg; - break; case 'p': if (!parse_port(optarg, &args->port)) { return SDL_FALSE; } break; + case 'r': + args->record_filename = optarg; + break; case 's': args->serial = optarg; break; @@ -281,6 +281,7 @@ int main(int argc, char *argv[]) { struct args args = { .serial = NULL, .crop = NULL, + .record_filename = NULL, .help = SDL_FALSE, .version = SDL_FALSE, .show_touches = SDL_FALSE, @@ -318,7 +319,7 @@ int main(int argc, char *argv[]) { .serial = args.serial, .crop = args.crop, .port = args.port, - .out_filename = args.out_filename, + .record_filename = args.record_filename, .max_size = args.max_size, .bit_rate = args.bit_rate, .show_touches = args.show_touches, diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 766c26f2..7f6df90c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -192,8 +192,8 @@ SDL_bool scrcpy(const struct scrcpy_options *options) { } struct recorder *rec = NULL; - if (options->out_filename) { - if (!recorder_init(&recorder, options->out_filename, frame_size)) { + if (options->record_filename) { + if (!recorder_init(&recorder, options->record_filename, frame_size)) { ret = SDL_FALSE; server_stop(&server); goto finally_destroy_file_handler; @@ -255,7 +255,7 @@ finally_destroy_file_handler: file_handler_join(&file_handler); file_handler_destroy(&file_handler); finally_destroy_recorder: - if (options->out_filename) { + if (options->record_filename) { recorder_destroy(&recorder); } finally_destroy_frames: diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 4716c587..89945e6c 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -6,7 +6,7 @@ struct scrcpy_options { const char *serial; const char *crop; - const char *out_filename; + const char *record_filename; Uint16 port; Uint16 max_size; Uint32 bit_rate; From 345f8858d386d53a0af75b5d9a942717d53adfcf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Nov 2018 14:41:54 +0100 Subject: [PATCH 14/16] Send frame meta only if recording is enabled The client needs the PTS for each frame only if recording is enabled. Otherwise, the PTS are not necessary, and the protocol is more straighforward. --- app/src/decoder.c | 13 +++++- app/src/scrcpy.c | 4 +- app/src/server.c | 17 ++++--- app/src/server.h | 7 ++- .../java/com/genymobile/scrcpy/Options.java | 9 ++++ .../com/genymobile/scrcpy/ScreenEncoder.java | 45 ++++++++++++------- .../java/com/genymobile/scrcpy/Server.java | 9 +++- 7 files changed, 75 insertions(+), 29 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 5d945cba..4fbc12bf 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -20,7 +20,7 @@ #define HEADER_SIZE 12 -static int read_packet(void *opaque, uint8_t *buf, int buf_size) { +static int read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) { struct decoder *decoder = opaque; struct receiver_state *state = &decoder->receiver_state; @@ -69,6 +69,11 @@ static int read_packet(void *opaque, uint8_t *buf, int buf_size) { return ret; } +static int read_raw_packet(void *opaque, uint8_t *buf, int buf_size) { + struct decoder *decoder = opaque; + return net_recv(decoder->video_socket, buf, buf_size); +} + // set the decoded frame as ready for rendering, and notify static void push_frame(struct decoder *decoder) { SDL_bool previous_frame_consumed = frames_offer_decoded_frame(decoder->frames); @@ -123,7 +128,11 @@ static int run_decoder(void *data) { // initialize the receiver state decoder->receiver_state.remaining = 0; - AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, decoder, read_packet, NULL, NULL); + // if recording is enabled, a "header" is sent between raw packets + int (*read_packet)(void *, uint8_t *, int) = + decoder->recorder ? read_packet_with_meta : read_raw_packet; + AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, decoder, + read_packet, NULL, NULL); if (!avio_ctx) { LOGC("Could not allocate avio context"); // avformat_open_input takes ownership of 'buffer' diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 7f6df90c..0e9bcba0 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -140,8 +140,10 @@ static void wait_show_touches(process_t process) { } SDL_bool scrcpy(const struct scrcpy_options *options) { + SDL_bool send_frame_meta = !!options->record_filename; if (!server_start(&server, options->serial, options->port, - options->max_size, options->bit_rate, options->crop)) { + options->max_size, options->bit_rate, options->crop, + send_frame_meta)) { return SDL_FALSE; } diff --git a/app/src/server.c b/app/src/server.c index 91eac7b7..3ad21511 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -78,7 +78,8 @@ static SDL_bool disable_tunnel(struct server *server) { static process_t execute_server(const char *serial, Uint16 max_size, Uint32 bit_rate, - const char *crop, SDL_bool tunnel_forward) { + SDL_bool tunnel_forward, const char *crop, + SDL_bool send_frame_meta) { char max_size_string[6]; char bit_rate_string[11]; sprintf(max_size_string, "%"PRIu16, max_size); @@ -92,7 +93,8 @@ static process_t execute_server(const char *serial, max_size_string, bit_rate_string, tunnel_forward ? "true" : "false", - crop ? crop : "", + crop ? crop : "''", + send_frame_meta ? "true" : "false", }; return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0])); } @@ -148,8 +150,9 @@ void server_init(struct server *server) { *server = (struct server) SERVER_INITIALIZER; } -SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port, - Uint16 max_size, Uint32 bit_rate, const char *crop) { +SDL_bool server_start(struct server *server, const char *serial, + Uint16 local_port, Uint16 max_size, Uint32 bit_rate, + const char *crop, SDL_bool send_frame_meta) { server->local_port = local_port; if (serial) { @@ -190,8 +193,10 @@ SDL_bool server_start(struct server *server, const char *serial, Uint16 local_po } // server will connect to our server socket - server->process = execute_server(serial, max_size, bit_rate, crop, - server->tunnel_forward); + server->process = execute_server(serial, max_size, bit_rate, + server->tunnel_forward, crop, + send_frame_meta); + if (server->process == PROCESS_NONE) { if (!server->tunnel_forward) { close_socket(&server->server_socket); diff --git a/app/src/server.h b/app/src/server.h index 2bc3f41f..19594c03 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -12,6 +12,7 @@ struct server { Uint16 local_port; SDL_bool tunnel_enabled; SDL_bool tunnel_forward; // use "adb forward" instead of "adb reverse" + SDL_bool send_frame_meta; // request frame PTS to be able to record properly SDL_bool server_copied_to_device; }; @@ -23,6 +24,7 @@ struct server { .local_port = 0, \ .tunnel_enabled = SDL_FALSE, \ .tunnel_forward = SDL_FALSE, \ + .send_frame_meta = SDL_FALSE, \ .server_copied_to_device = SDL_FALSE, \ } @@ -30,8 +32,9 @@ struct server { void server_init(struct server *server); // push, enable tunnel et start the server -SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port, - Uint16 max_size, Uint32 bit_rate, const char *crop); +SDL_bool server_start(struct server *server, const char *serial, + Uint16 local_port, Uint16 max_size, Uint32 bit_rate, + const char *crop, SDL_bool send_frame_meta); // block until the communication with the server is established socket_t server_connect_to(struct server *server); diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 93df896a..851c7ed6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -7,6 +7,7 @@ public class Options { private int bitRate; private boolean tunnelForward; private Rect crop; + private boolean sendFrameMeta; // send PTS so that the client may record properly public int getMaxSize() { return maxSize; @@ -39,4 +40,12 @@ public class Options { public void setCrop(Rect crop) { this.crop = crop; } + + public boolean getSendFrameMeta() { + return sendFrameMeta; + } + + public void setSendFrameMeta(boolean sendFrameMeta) { + this.sendFrameMeta = sendFrameMeta; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index a76ce7a9..be4a42eb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -25,20 +25,23 @@ public class ScreenEncoder implements Device.RotationListener { private static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000; private final AtomicBoolean rotationChanged = new AtomicBoolean(); + private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); private int bitRate; private int frameRate; private int iFrameInterval; + private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(int bitRate, int frameRate, int iFrameInterval) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int frameRate, int iFrameInterval) { + this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.frameRate = frameRate; this.iFrameInterval = iFrameInterval; } - public ScreenEncoder(int bitRate) { - this(bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL); + public ScreenEncoder(boolean sendFrameMeta, int bitRate) { + this(sendFrameMeta, bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL); } @Override @@ -82,7 +85,7 @@ public class ScreenEncoder implements Device.RotationListener { private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException { boolean eof = false; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - ByteBuffer bBuffer = ByteBuffer.allocate(12); + while (!consumeRotationChange() && !eof) { int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); @@ -94,22 +97,11 @@ public class ScreenEncoder implements Device.RotationListener { } if (outputBufferId >= 0) { ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); - bBuffer.clear(); - long pts; - if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { - pts = 0; // non-media data packet - } else { - if (ptsOrigin == 0) { - ptsOrigin = bufferInfo.presentationTimeUs; - } - pts = bufferInfo.presentationTimeUs - ptsOrigin; + if (sendFrameMeta) { + writeFrameMeta(fd, bufferInfo, codecBuffer.remaining()); } - bBuffer.putLong(pts); - bBuffer.putInt(codecBuffer.remaining()); - bBuffer.flip(); - IO.writeFully(fd, bBuffer); IO.writeFully(fd, codecBuffer); } } finally { @@ -122,6 +114,25 @@ public class ScreenEncoder implements Device.RotationListener { return !eof; } + private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException { + headerBuffer.clear(); + + long pts; + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + pts = 0; // non-media data packet + } else { + if (ptsOrigin == 0) { + ptsOrigin = bufferInfo.presentationTimeUs; + } + pts = bufferInfo.presentationTimeUs - ptsOrigin; + } + + headerBuffer.putLong(pts); + headerBuffer.putInt(packetSize); + headerBuffer.flip(); + IO.writeFully(fd, headerBuffer); + } + private static MediaCodec createCodec() throws IOException { return MediaCodec.createEncoderByType("video/avc"); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index b218e83d..db15fb52 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy; import android.graphics.Rect; import java.io.IOException; +import java.util.Arrays; public final class Server { @@ -14,7 +15,7 @@ public final class Server { final Device device = new Device(options); boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { - ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate()); + ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate()); // asynchronous startEventController(device, connection); @@ -71,6 +72,12 @@ public final class Server { Rect crop = parseCrop(args[3]); options.setCrop(crop); + if (args.length < 5) { + return options; + } + boolean sendFrameMeta = Boolean.parseBoolean(args[4]); + options.setSendFrameMeta(sendFrameMeta); + return options; } From 60afb46c8d4c3db6bb4d82bf00bc8741fa390713 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Nov 2018 15:27:52 +0100 Subject: [PATCH 15/16] Store queue of PTS for pending frames Several frames may be read by read_packet() before they are consumed (returned by av_read_frame()), so we need to store the PTS of frames in order, so that the right PTS is assigned to the right frame. --- app/src/decoder.c | 59 ++++++++++++++++++++++++++++++++++++++++++----- app/src/decoder.h | 9 ++++++-- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 4fbc12bf..bc164210 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -20,6 +20,46 @@ #define HEADER_SIZE 12 +static struct frame_meta *frame_meta_new(uint64_t pts) { + struct frame_meta *meta = malloc(sizeof(*meta)); + if (!meta) { + return meta; + } + meta->pts = pts; + meta->next = NULL; + return meta; +} + +static void frame_meta_delete(struct frame_meta *frame_meta) { + free(frame_meta); +} + +static SDL_bool receiver_state_push_meta(struct receiver_state *state, + uint64_t pts) { + struct frame_meta *frame_meta = frame_meta_new(pts); + if (!frame_meta) { + return SDL_FALSE; + } + + // append to the list + // (iterate to find the last item, in practice the list should be tiny) + struct frame_meta **p = &state->frame_meta_queue; + while (*p) { + p = &(*p)->next; + } + *p = frame_meta; + return SDL_TRUE; +} + +static uint64_t receiver_state_take_meta(struct receiver_state *state) { + struct frame_meta *frame_meta = state->frame_meta_queue; // first item + SDL_assert(frame_meta); // must not be empty + uint64_t pts = frame_meta->pts; + state->frame_meta_queue = frame_meta->next; // remove the item + frame_meta_delete(frame_meta); + return pts; +} + static int read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) { struct decoder *decoder = opaque; struct receiver_state *state = &decoder->receiver_state; @@ -37,9 +77,6 @@ static int read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) { // It is followed by bytes containing the packet/frame. if (!state->remaining) { - // the next PTS is now for the current frame - state->pts = state->next_pts; - #define HEADER_SIZE 12 uint8_t header[HEADER_SIZE]; ssize_t ret = net_recv_all(decoder->video_socket, header, HEADER_SIZE); @@ -49,8 +86,14 @@ static int read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) { // no partial read (net_recv_all()) SDL_assert_release(ret == HEADER_SIZE); - state->next_pts = buffer_read64be(header); + uint64_t pts = buffer_read64be(header); state->remaining = buffer_read32be(&header[8]); + + if (!receiver_state_push_meta(state, pts)) { + LOGE("Could not store PTS for recording"); + // we cannot save the PTS, the recording would be broken + return -1; + } } SDL_assert(state->remaining); @@ -126,6 +169,7 @@ static int run_decoder(void *data) { } // initialize the receiver state + decoder->receiver_state.frame_meta_queue = NULL; decoder->receiver_state.remaining = 0; // if recording is enabled, a "header" is sent between raw packets @@ -195,8 +239,11 @@ static int run_decoder(void *data) { #endif if (decoder->recorder) { - packet.pts = decoder->receiver_state.pts; - packet.dts = decoder->receiver_state.pts; + // we retrieve the PTS in order they were received, so they will + // be assigned to the correct frame + uint64_t pts = receiver_state_take_meta(&decoder->receiver_state); + packet.pts = pts; + packet.dts = pts; // no need to rescale with av_packet_rescale_ts(), the timestamps // are in microseconds both in input and output diff --git a/app/src/decoder.h b/app/src/decoder.h index 2aeb1824..610de000 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -9,6 +9,11 @@ struct frames; +struct frame_meta { + uint64_t pts; + struct frame_meta *next; +}; + struct decoder { struct frames *frames; socket_t video_socket; @@ -16,8 +21,8 @@ struct decoder { SDL_mutex *mutex; struct recorder *recorder; struct receiver_state { - uint64_t next_pts; - uint64_t pts; + // meta (in order) for frames not consumed yet + struct frame_meta *frame_meta_queue; size_t remaining; // remaining bytes to receive for the current frame } receiver_state; }; From 22ff03f2f793dafe6ff86f4c101f8f1d1555a551 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Nov 2018 15:38:06 +0100 Subject: [PATCH 16/16] Do not queue invalid PTS Configuration packets produced by MediaCodec have no valid PTS, and do not produce frame. Do not queue their (invalid) PTS not to break the matching between frames and their PTS. --- app/src/decoder.c | 3 ++- server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index bc164210..7e1e041d 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -19,6 +19,7 @@ #define BUFSIZE 0x10000 #define HEADER_SIZE 12 +#define NO_PTS UINT64_C(-1) static struct frame_meta *frame_meta_new(uint64_t pts) { struct frame_meta *meta = malloc(sizeof(*meta)); @@ -89,7 +90,7 @@ static int read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) { uint64_t pts = buffer_read64be(header); state->remaining = buffer_read32be(&header[8]); - if (!receiver_state_push_meta(state, pts)) { + if (pts != NO_PTS && !receiver_state_push_meta(state, pts)) { LOGE("Could not store PTS for recording"); // we cannot save the PTS, the recording would be broken return -1; diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index be4a42eb..0419dc52 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -23,6 +23,7 @@ public class ScreenEncoder implements Device.RotationListener { private static final int REPEAT_FRAME_DELAY = 6; // repeat after 6 frames private static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000; + private static final int NO_PTS = -1; private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); @@ -119,7 +120,7 @@ public class ScreenEncoder implements Device.RotationListener { long pts; if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { - pts = 0; // non-media data packet + pts = NO_PTS; // non-media data packet } else { if (ptsOrigin == 0) { ptsOrigin = bufferInfo.presentationTimeUs;