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: