Adds ability to capture a screenshot to PNG

This commit is contained in:
Frank Leon Rose 2020-01-24 16:48:24 -05:00
parent 158a3e5d2b
commit d16a0d260c
11 changed files with 337 additions and 7 deletions

View file

@ -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',
]],
]

254
app/src/capture.c Normal file
View file

@ -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 <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <SDL2/SDL_events.h>
#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;
}

21
app/src/capture.h Normal file
View file

@ -0,0 +1,21 @@
#ifndef CAPTURE_H
#define CAPTURE_H
#include <stdbool.h>
#include <libavformat/avio.h>
#include <libavcodec/avcodec.h>
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

View file

@ -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;
}

View file

@ -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)

View file

@ -3,6 +3,7 @@
#include <stdbool.h>
#include <unistd.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
#include <SDL2/SDL.h>
@ -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

View file

@ -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);

View file

@ -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, \

View file

@ -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;
}

View file

@ -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);

View file

@ -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);