diff --git a/app/meson.build b/app/meson.build index 3bcb9bc1..6a9ff316 100644 --- a/app/meson.build +++ b/app/meson.build @@ -1,5 +1,6 @@ src = [ 'src/main.c', + 'src/capture.c', 'src/cli.c', 'src/command.c', 'src/control_msg.c', @@ -30,6 +31,8 @@ if not get_option('crossbuild_windows') dependency('libavformat'), dependency('libavcodec'), dependency('libavutil'), + dependency('libswscale'), + dependency('libpng'), dependency('sdl2'), ] @@ -168,7 +171,8 @@ if get_option('buildtype') == 'debug' 'tests/test_queue.c', ]], ['test_strutil', [ - 'tests/test_strutil.c', + 'tests/test_s + til.c', 'src/util/str_util.c', ]], ] diff --git a/app/src/capture.c b/app/src/capture.c new file mode 100644 index 00000000..4e5c60bc --- /dev/null +++ b/app/src/capture.c @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2001 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * @file + * video decoding with libavcodec API example + * + * @example decode_video.c + */ + +#include "capture.h" + +#include +#include +#include + +#include +#include +#include + +#include + +#include "events.h" +#include "util/log.h" + +static const char *string_error(int err) { + switch (err) { + case AVERROR_EOF: return "End of File"; + case AVERROR_BUG: return "Bug"; + case AVERROR_BUG2: return "Bug2"; + case AVERROR_BUFFER_TOO_SMALL: return "Buffer Too Small"; + case AVERROR_EXIT: return "Exit Requested"; + case AVERROR_DECODER_NOT_FOUND: return "Decoder Not Found"; + case AVERROR_DEMUXER_NOT_FOUND: return "Demuxer Not Found"; + case AVERROR_ENCODER_NOT_FOUND: return "Encoder Not Found"; + case AVERROR_EXTERNAL: return "External Error"; + case AVERROR_FILTER_NOT_FOUND: return "Filter Not Found"; + case AVERROR_MUXER_NOT_FOUND: return "Muxer Not Found"; + case AVERROR_OPTION_NOT_FOUND: return "Option Not Found"; + case AVERROR_PATCHWELCOME: return "Patch Welcome"; + case AVERROR_PROTOCOL_NOT_FOUND: return "Protocol Not Found"; + case AVERROR_STREAM_NOT_FOUND: return "Stream Not Found"; + case AVERROR_INVALIDDATA: return "Invalid Data (waiting for index frame?)"; + case AVERROR(EINVAL): return "Invalid"; + case AVERROR(ENOMEM): return "No Memory"; + case AVERROR(EAGAIN): return "Try Again"; + default: return "Unknown"; + } +} + +static void notify_complete() { + static SDL_Event new_frame_event = { + .type = EVENT_SCREEN_CAPTURE_COMPLETE, + }; + SDL_PushEvent(&new_frame_event); +} + +// Thanks, https://stackoverflow.com/a/18313791 +// Also, https://raw.githubusercontent.com/FFmpeg/FFmpeg/master/doc/examples/decode_video.c + +static bool in_frame_to_png( + AVIOContext *output_context, + AVCodecContext *codecCtx, AVFrame *inframe) { + struct SwsContext * swCtx = sws_getContext(codecCtx->width, + codecCtx->height, + codecCtx->pix_fmt, + codecCtx->width, + codecCtx->height, + AV_PIX_FMT_RGB24, + SWS_FAST_BILINEAR, 0, 0, 0); + + AVFrame * rgbFrame = av_frame_alloc(); + LOGV("Image frame width: %d height: %d", codecCtx->width, codecCtx->height); + rgbFrame->width = codecCtx->width; + rgbFrame->height = codecCtx->height; + rgbFrame->format = AV_PIX_FMT_RGB24; + av_image_alloc( + rgbFrame->data, rgbFrame->linesize, codecCtx->width, + codecCtx->height, AV_PIX_FMT_RGB24, 1); + sws_scale( + swCtx, inframe->data, inframe->linesize, 0, + inframe->height, rgbFrame->data, rgbFrame->linesize); + + AVCodec *outCodec = avcodec_find_encoder(AV_CODEC_ID_PNG); + if (!outCodec) { + LOGE("Failed to find PNG codec"); + return false; + } + AVCodecContext *outCodecCtx = avcodec_alloc_context3(outCodec); + if (!outCodecCtx) { + LOGE("Unable to allocate PNG codec context"); + return false; + } + + outCodecCtx->width = codecCtx->width; + outCodecCtx->height = codecCtx->height; + outCodecCtx->pix_fmt = AV_PIX_FMT_RGB24; + outCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO; + if (codecCtx->time_base.num && codecCtx->time_base.num) { + outCodecCtx->time_base.num = codecCtx->time_base.num; + outCodecCtx->time_base.den = codecCtx->time_base.den; + } else { + outCodecCtx->time_base.num = 1; + outCodecCtx->time_base.den = 30; // FPS + } + + int ret = avcodec_open2(outCodecCtx, outCodec, NULL); + if (ret < 0) { + LOGE("Failed to open PNG codec: %s", string_error(ret)); + return false; + } + + ret = avcodec_send_frame(outCodecCtx, rgbFrame); + av_frame_free(&rgbFrame); + if (ret < 0) { + LOGE("Failed to send frame to output codex"); + return false; + } + + AVPacket outPacket; + av_init_packet(&outPacket); + outPacket.size = 0; + outPacket.data = NULL; + ret = avcodec_receive_packet(outCodecCtx, &outPacket); + avcodec_close(outCodecCtx); + av_free(outCodecCtx); + if (ret >= 0) { + // Dump packet + avio_write(output_context, outPacket.data, outPacket.size); + notify_complete(); + av_packet_unref(&outPacket); + return true; + } else { + LOGE("Failed to receive packet"); + return false; + } +} + +// Decodes a video packet into PNG image, if possible. +// Sends a new video packet `packet` into the active decoding +// context `dec_ctx`. +// Returns `true` when the decoding yielded a PNG. +static bool decode_packet_to_png( + AVIOContext *output_context, + AVCodecContext *dec_ctx, + const AVPacket *packet, + AVFrame *working_frame) { + LOGV("Sending video packet: %p Size: %d", packet, packet ? packet->size : 0); + + int ret = avcodec_send_packet(dec_ctx, packet); + if (ret < 0) { + LOGW( "Error sending a packet for decoding: %s", string_error(ret)); + return false; + } + + bool found = false; + while (ret >= 0) { + ret = avcodec_receive_frame(dec_ctx, working_frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { + LOGV("No frame received: %s", string_error(ret)); + break; // We're done + } else if (ret < 0) { + LOGW("Error during decoding: %s", string_error(ret)); + } + + LOGI("Found frame: %d", dec_ctx->frame_number); + if (in_frame_to_png(output_context, dec_ctx, working_frame)) { + found = true; + break; + } else { + LOGE("Failed to generate PNG"); + } + } + return found; +} + +bool capture_init(struct capture *capture, const char *filename) { + capture->filename = SDL_strdup(filename); + if (!capture->filename) { + LOGE("Could not strdup filename"); + return false; + } + + int ret = avio_open(&capture->file_context, capture->filename, + AVIO_FLAG_WRITE); + if (ret < 0) { + LOGE("Failed to open output file: %s", capture->filename); + return false; + } + + /* find the MPEG-1 video decoder */ + const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); + if (!codec) { + LOGE("Codec not found: %d", AV_CODEC_ID_H264); + return false; + } + + capture->context = avcodec_alloc_context3(codec); + if (!capture->context) { + LOGE("Could not allocate video codec context"); + return false; + } + + /* For some codecs, such as msmpeg4 and mpeg4, width and height + MUST be initialized there because this information is not + available in the bitstream. */ + + /* open it */ + if (avcodec_open2(capture->context, codec, NULL) < 0) { + LOGE("Could not open codec"); + return false; + } + + // Create a reusable frame buffer. + capture->working_frame = av_frame_alloc(); + if (!capture->working_frame) { + LOGE("Could not allocate video frame"); + return false; + } + return true; +} + +void capture_destroy(struct capture *capture) { + av_frame_free(&capture->working_frame); + avcodec_free_context(&capture->context); + avio_close(capture->file_context); +} + +bool capture_push(struct capture *capture, const AVPacket *packet) { + if (decode_packet_to_png(capture->file_context, capture->context, packet, capture->working_frame)) { + LOGI("Parsed PNG from incoming packets."); + return true; + } + return false; +} diff --git a/app/src/capture.h b/app/src/capture.h new file mode 100644 index 00000000..71549b02 --- /dev/null +++ b/app/src/capture.h @@ -0,0 +1,21 @@ +#ifndef CAPTURE_H +#define CAPTURE_H + +#include +#include +#include + +struct capture { + char *filename; + AVIOContext *file_context; + AVCodecContext *context; + AVFrame *working_frame; +}; + +bool capture_init(struct capture *capture, const char *filename); + +void capture_destroy(struct capture *capture); + +bool capture_push(struct capture *capture, const AVPacket *packet); + +#endif // CAPTURE_H \ No newline at end of file diff --git a/app/src/cli.c b/app/src/cli.c index ef31a723..4b74efab 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -363,6 +363,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, {"render-expired-frames", no_argument, NULL, OPT_RENDER_EXPIRED_FRAMES}, + {"screen-capture", required_argument, NULL, 'C'}, {"serial", required_argument, NULL, 's'}, {"show-touches", no_argument, NULL, 't'}, {"turn-screen-off", no_argument, NULL, 'S'}, @@ -383,7 +384,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { optind = 0; // reset to start from the first argument in tests int c; - while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, + while ((c = getopt_long(argc, argv, "b:cC:fF:hm:nNp:r:s:StTv", long_options, NULL)) != -1) { switch (c) { case 'b': @@ -391,6 +392,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return false; } break; + case 'C': + opts->capture_filename = optarg; + break; case 'c': LOGW("Deprecated option -c. Use --crop instead."); // fall through @@ -494,8 +498,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { } } - if (!opts->display && !opts->record_filename) { - LOGE("-N/--no-display requires screen recording (-r/--record)"); + if (!opts->display && !opts->record_filename && !opts->capture_filename) { + LOGE("-N/--no-display requires screen recording (-r/--record) or screen capture (-C/--screen-capture)"); return false; } diff --git a/app/src/events.h b/app/src/events.h index e9512048..07c3a04a 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -1,3 +1,4 @@ #define EVENT_NEW_SESSION SDL_USEREVENT #define EVENT_NEW_FRAME (SDL_USEREVENT + 1) #define EVENT_STREAM_STOPPED (SDL_USEREVENT + 2) +#define EVENT_SCREEN_CAPTURE_COMPLETE (SDL_USEREVENT + 3) diff --git a/app/src/main.c b/app/src/main.c index d683c508..5aea485d 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -3,6 +3,7 @@ #include #include #include +#include #define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem #include @@ -27,6 +28,9 @@ print_version(void) { fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR, LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO); + fprintf(stderr, " - libswscale %d.%d.%d\n", LIBSWSCALE_VERSION_MAJOR, + LIBSWSCALE_VERSION_MINOR, + LIBSWSCALE_VERSION_MICRO); } int diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 17be1ed4..6fe2965a 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -18,6 +18,7 @@ #include "file_handler.h" #include "fps_counter.h" #include "input_manager.h" +#include "capture.h" #include "recorder.h" #include "screen.h" #include "server.h" @@ -35,6 +36,7 @@ static struct video_buffer video_buffer; static struct stream stream; static struct decoder decoder; static struct recorder recorder; +static struct capture capture; static struct controller controller; static struct file_handler file_handler; @@ -131,6 +133,9 @@ handle_event(SDL_Event *event, bool control) { case EVENT_STREAM_STOPPED: LOGD("Video stream stopped"); return EVENT_RESULT_STOPPED_BY_EOS; + case EVENT_SCREEN_CAPTURE_COMPLETE: + LOGD("Screen capture completed"); + return EVENT_RESULT_STOPPED_BY_USER; case SDL_QUIT: LOGD("User requested to quit"); return EVENT_RESULT_STOPPED_BY_USER; @@ -277,6 +282,7 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { bool scrcpy(const struct scrcpy_options *options) { + bool captures = !!options->capture_filename; bool record = !!options->record_filename; struct server_params params = { .crop = options->crop, @@ -304,6 +310,7 @@ scrcpy(const struct scrcpy_options *options) { bool video_buffer_initialized = false; bool file_handler_initialized = false; bool recorder_initialized = false; + bool capture_initialized = false; bool stream_started = false; bool controller_initialized = false; bool controller_started = false; @@ -363,9 +370,19 @@ scrcpy(const struct scrcpy_options *options) { recorder_initialized = true; } + struct capture *cap = NULL; + if (captures) { + if (!capture_init(&capture, + options->capture_filename)) { + goto end; + } + cap = &capture; + capture_initialized = true; + } + av_log_set_callback(av_log_callback); - stream_init(&stream, server.video_socket, dec, rec); + stream_init(&stream, server.video_socket, dec, rec, cap); // now we consumed the header values, the socket receives the video stream // start the stream @@ -460,6 +477,9 @@ end: recorder_destroy(&recorder); } + if (capture_initialized) { + capture_destroy(&capture); + } if (file_handler_initialized) { file_handler_join(&file_handler); file_handler_destroy(&file_handler); diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 75de8717..de937d92 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -11,6 +11,7 @@ struct scrcpy_options { const char *serial; const char *crop; + const char *capture_filename; const char *record_filename; const char *window_title; const char *push_target; @@ -37,6 +38,7 @@ struct scrcpy_options { #define SCRCPY_OPTIONS_DEFAULT { \ .serial = NULL, \ .crop = NULL, \ + .capture_filename = NULL, \ .record_filename = NULL, \ .window_title = NULL, \ .push_target = NULL, \ diff --git a/app/src/stream.c b/app/src/stream.c index dd2dbd76..8aeff0e5 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -12,6 +12,7 @@ #include "compat.h" #include "decoder.h" #include "events.h" +#include "capture.h" #include "recorder.h" #include "util/buffer_util.h" #include "util/log.h" @@ -71,6 +72,10 @@ notify_stopped(void) { static bool process_config_packet(struct stream *stream, AVPacket *packet) { + if (stream->capture && !capture_push(stream->capture, packet)) { + LOGW("Could not send config packet to capture"); + return true; // We're Ok. Keep sending packets. + } if (stream->recorder && !recorder_push(stream->recorder, packet)) { LOGE("Could not send config packet to recorder"); return false; @@ -93,6 +98,15 @@ process_frame(struct stream *stream, AVPacket *packet) { } } + if (stream->capture) { + packet->dts = packet->pts; + + if (!capture_push(stream->capture, packet)) { + LOGE("Could not send packet to capture"); + return false; + } + } + return true; } @@ -270,9 +284,11 @@ end: void stream_init(struct stream *stream, socket_t socket, - struct decoder *decoder, struct recorder *recorder) { + struct decoder *decoder, struct recorder *recorder, + struct capture *capture) { stream->socket = socket; stream->decoder = decoder, + stream->capture = capture; stream->recorder = recorder; stream->has_pending = false; } diff --git a/app/src/stream.h b/app/src/stream.h index f7c5e475..ed571b31 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -18,6 +18,7 @@ struct stream { SDL_Thread *thread; struct decoder *decoder; struct recorder *recorder; + struct capture *capture; AVCodecContext *codec_ctx; AVCodecParserContext *parser; // successive packets may need to be concatenated, until a non-config @@ -28,7 +29,8 @@ struct stream { void stream_init(struct stream *stream, socket_t socket, - struct decoder *decoder, struct recorder *recorder); + struct decoder *decoder, struct recorder *recorder, + struct capture *capture); bool stream_start(struct stream *stream); diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 539c3c94..49ebe3b8 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -55,6 +55,7 @@ static void test_options(void) { "--record", "file", "--record-format", "mkv", "--render-expired-frames", + "--screen-capture", "filename.png", "--serial", "0123456789abcdef", "--show-touches", "--turn-screen-off", @@ -83,6 +84,7 @@ static void test_options(void) { assert(!strcmp(opts->record_filename, "file")); assert(opts->record_format == RECORDER_FORMAT_MKV); assert(opts->render_expired_frames); + assert(!strcmp(opts->capture_filename, "filename.png")); assert(!strcmp(opts->serial, "0123456789abcdef")); assert(opts->show_touches); assert(opts->turn_screen_off);