mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-04-21 03:55:05 +00:00
Merge pull request #2 from team-mobot/ENG-2301-add-png-screen-capture
ENG-2301 add png screen capture
This commit is contained in:
commit
eaaa9b53cb
16 changed files with 597 additions and 10 deletions
|
@ -1,5 +1,6 @@
|
|||
src = [
|
||||
'src/main.c',
|
||||
'src/capture.c',
|
||||
'src/cli.c',
|
||||
'src/command.c',
|
||||
'src/control_msg.c',
|
||||
|
@ -31,6 +32,8 @@ if not get_option('crossbuild_windows')
|
|||
dependency('libavformat'),
|
||||
dependency('libavcodec'),
|
||||
dependency('libavutil'),
|
||||
dependency('libswscale'),
|
||||
dependency('libpng'),
|
||||
dependency('sdl2'),
|
||||
]
|
||||
|
||||
|
@ -167,7 +170,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',
|
||||
]],
|
||||
]
|
||||
|
|
410
app/src/capture.c
Normal file
410
app/src/capture.c
Normal file
|
@ -0,0 +1,410 @@
|
|||
/*
|
||||
* 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/lock.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) {
|
||||
uint64_t start = get_timestamp();
|
||||
int targetHeight = codecCtx->height;
|
||||
int targetWidth = codecCtx->width;
|
||||
struct SwsContext * swCtx = sws_getContext(codecCtx->width,
|
||||
codecCtx->height,
|
||||
codecCtx->pix_fmt,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
AV_PIX_FMT_RGB24,
|
||||
// NOTE(frankleonrose): These options offer better picture
|
||||
// quality in case of scaling, according to https://stackoverflow.com/a/46169884
|
||||
// SWS_BILINEAR | SWS_FULL_CHR_H_INT | SWS_ACCURATE_RND,
|
||||
SWS_FAST_BILINEAR,
|
||||
0, 0, 0);
|
||||
|
||||
AVFrame * rgbFrame = av_frame_alloc();
|
||||
LOGV("Image frame width: %d height: %d scaling to %d x %d", *inframe->linesize, inframe->height, targetWidth, targetHeight);
|
||||
rgbFrame->width = targetWidth;
|
||||
rgbFrame->height = targetHeight;
|
||||
rgbFrame->format = AV_PIX_FMT_RGB24;
|
||||
av_image_alloc(
|
||||
rgbFrame->data, rgbFrame->linesize, targetWidth, targetHeight, AV_PIX_FMT_RGB24, 1);
|
||||
sws_scale(
|
||||
swCtx,
|
||||
// const_cast<const uint8_t * const*>(inframe->data)
|
||||
(const uint8_t * const*)(inframe->data),
|
||||
inframe->linesize, 0,
|
||||
inframe->height, rgbFrame->data, rgbFrame->linesize);
|
||||
|
||||
LOGV("Scaling image: %llu", get_timestamp() - start);
|
||||
|
||||
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 = targetWidth;
|
||||
outCodecCtx->height = targetHeight;
|
||||
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 = 10; // 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);
|
||||
LOGV("Converted to PNG: %llu", get_timestamp() - start);
|
||||
if (ret >= 0) {
|
||||
// Dump packet
|
||||
avio_write(output_context, outPacket.data, outPacket.size);
|
||||
notify_complete();
|
||||
av_packet_unref(&outPacket);
|
||||
LOGV("Wrote file: %llu", get_timestamp() - start);
|
||||
log_timestamp("Capture written");
|
||||
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;
|
||||
}
|
||||
|
||||
capture->mutex = SDL_CreateMutex();
|
||||
if (!capture->mutex) {
|
||||
LOGC("Could not create mutex");
|
||||
SDL_free(capture->filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
capture->queue_cond = SDL_CreateCond();
|
||||
if (!capture->queue_cond) {
|
||||
LOGC("Could not create capture cond");
|
||||
SDL_DestroyMutex(capture->mutex);
|
||||
SDL_free(capture->filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
queue_init(&capture->queue);
|
||||
capture->stopped = false;
|
||||
capture->finished = 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);
|
||||
}
|
||||
|
||||
static bool capture_process(struct capture *capture, const AVPacket *packet) {
|
||||
log_timestamp("Processing packet");
|
||||
static uint64_t total = 0;
|
||||
uint64_t start = get_timestamp();
|
||||
if (capture->finished) {
|
||||
LOGV("Skipping redundant call to capture_push");
|
||||
} else {
|
||||
bool found_png = decode_packet_to_png(capture->file_context, capture->context, packet, capture->working_frame);
|
||||
if (found_png) {
|
||||
LOGI("Parsed PNG from incoming packets.");
|
||||
capture->finished = found_png;
|
||||
}
|
||||
}
|
||||
uint64_t duration = get_timestamp() - start;
|
||||
total += duration;
|
||||
LOGV("Capture step microseconds: %llu total: %llu", duration, total);
|
||||
return capture->finished;
|
||||
}
|
||||
|
||||
static struct capture_packet *
|
||||
capture_packet_new(const AVPacket *packet) {
|
||||
struct capture_packet *rec = SDL_malloc(sizeof(*rec));
|
||||
if (!rec) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// av_packet_ref() does not initialize all fields in old FFmpeg versions
|
||||
// See <https://github.com/Genymobile/scrcpy/issues/707>
|
||||
av_init_packet(&rec->packet);
|
||||
|
||||
if (av_packet_ref(&rec->packet, packet)) {
|
||||
SDL_free(rec);
|
||||
return NULL;
|
||||
}
|
||||
return rec;
|
||||
}
|
||||
|
||||
static void
|
||||
capture_packet_delete(struct capture_packet *rec) {
|
||||
av_packet_unref(&rec->packet);
|
||||
SDL_free(rec);
|
||||
}
|
||||
|
||||
static int
|
||||
run_capture(void *data) {
|
||||
log_timestamp("Running capture thread");
|
||||
struct capture *capture = data;
|
||||
|
||||
for (;;) {
|
||||
mutex_lock(capture->mutex);
|
||||
|
||||
while (!capture->stopped && queue_is_empty(&capture->queue)) {
|
||||
cond_wait(capture->queue_cond, capture->mutex);
|
||||
}
|
||||
|
||||
// if stopped is set, continue to process the remaining events (to
|
||||
// finish the capture) before actually stopping
|
||||
|
||||
if (capture->stopped && queue_is_empty(&capture->queue)) {
|
||||
mutex_unlock(capture->mutex);
|
||||
break;
|
||||
}
|
||||
|
||||
struct capture_packet *rec;
|
||||
queue_take(&capture->queue, next, &rec);
|
||||
|
||||
mutex_unlock(capture->mutex);
|
||||
|
||||
bool ok = capture_process(capture, &rec->packet);
|
||||
capture_packet_delete(rec);
|
||||
if (ok) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LOGD("capture thread ended");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool
|
||||
capture_start(struct capture *capture) {
|
||||
log_timestamp("Starting capture thread");
|
||||
|
||||
capture->thread = SDL_CreateThread(run_capture, "capture", capture);
|
||||
if (!capture->thread) {
|
||||
LOGC("Could not start capture thread");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
capture_stop(struct capture *capture) {
|
||||
mutex_lock(capture->mutex);
|
||||
capture->stopped = true;
|
||||
cond_signal(capture->queue_cond);
|
||||
mutex_unlock(capture->mutex);
|
||||
}
|
||||
|
||||
void
|
||||
capture_join(struct capture *capture) {
|
||||
SDL_WaitThread(capture->thread, NULL);
|
||||
}
|
||||
|
||||
bool
|
||||
capture_push(struct capture *capture, const AVPacket *packet) {
|
||||
log_timestamp("Received packet");
|
||||
mutex_lock(capture->mutex);
|
||||
assert(!capture->stopped);
|
||||
|
||||
if (capture->finished) {
|
||||
// reject any new packet (this will stop the stream)
|
||||
return false;
|
||||
}
|
||||
|
||||
struct capture_packet *rec = capture_packet_new(packet);
|
||||
if (!rec) {
|
||||
LOGC("Could not allocate capture packet");
|
||||
return false;
|
||||
}
|
||||
|
||||
queue_push(&capture->queue, next, rec);
|
||||
cond_signal(capture->queue_cond);
|
||||
|
||||
mutex_unlock(capture->mutex);
|
||||
return true;
|
||||
}
|
50
app/src/capture.h
Normal file
50
app/src/capture.h
Normal file
|
@ -0,0 +1,50 @@
|
|||
#ifndef CAPTURE_H
|
||||
#define CAPTURE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <libavformat/avio.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <SDL2/SDL_mutex.h>
|
||||
#include <SDL2/SDL_thread.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "common.h"
|
||||
#include "util/queue.h"
|
||||
|
||||
struct capture_packet {
|
||||
AVPacket packet;
|
||||
struct capture_packet *next;
|
||||
};
|
||||
|
||||
struct capture_queue QUEUE(struct capture_packet);
|
||||
|
||||
struct capture {
|
||||
char *filename;
|
||||
AVIOContext *file_context;
|
||||
AVCodecContext *context;
|
||||
AVFrame *working_frame;
|
||||
|
||||
SDL_Thread *thread;
|
||||
SDL_mutex *mutex;
|
||||
SDL_cond *queue_cond;
|
||||
bool finished; // PNG has been captured
|
||||
bool stopped; // set on recorder_stop() by the stream reader
|
||||
struct capture_queue queue;
|
||||
};
|
||||
|
||||
bool capture_init(struct capture *capture, const char *filename);
|
||||
|
||||
void capture_destroy(struct capture *capture);
|
||||
|
||||
bool
|
||||
capture_start(struct capture *capture);
|
||||
|
||||
void
|
||||
capture_stop(struct capture *capture);
|
||||
|
||||
void
|
||||
capture_join(struct capture *capture);
|
||||
|
||||
bool capture_push(struct capture *capture, const AVPacket *packet);
|
||||
|
||||
#endif // CAPTURE_H
|
|
@ -622,6 +622,10 @@ parse_record_format(const char *optarg, enum sc_record_format *format) {
|
|||
*format = SC_RECORD_FORMAT_MKV;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(optarg, "h264")) {
|
||||
*format = SC_RECORD_FORMAT_H264;
|
||||
return true;
|
||||
}
|
||||
LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg);
|
||||
return false;
|
||||
}
|
||||
|
@ -704,6 +708,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||
{"render-expired-frames", no_argument, NULL,
|
||||
OPT_RENDER_EXPIRED_FRAMES},
|
||||
{"rotation", required_argument, NULL, OPT_ROTATION},
|
||||
{"screen-capture", required_argument, NULL, 'C'},
|
||||
{"serial", required_argument, NULL, 's'},
|
||||
{"shortcut-mod", required_argument, NULL, OPT_SHORTCUT_MOD},
|
||||
{"show-touches", no_argument, NULL, 't'},
|
||||
|
@ -726,7 +731,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:StTvV:w",
|
||||
while ((c = getopt_long(argc, argv, "b:cC:fF:hm:nNp:r:s:StTvV:w",
|
||||
long_options, NULL)) != -1) {
|
||||
switch (c) {
|
||||
case 'b':
|
||||
|
@ -734,6 +739,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
|
||||
|
@ -892,8 +900,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;
|
||||
}
|
||||
|
||||
|
|
|
@ -120,6 +120,7 @@ adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
|
|||
cmd[len + i] = NULL;
|
||||
enum process_result r = cmd_execute(cmd, &process);
|
||||
if (r != PROCESS_SUCCESS) {
|
||||
LOGE("adb process execution returned an error.");
|
||||
show_adb_err_msg(r, cmd);
|
||||
return PROCESS_NONE;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -4,6 +4,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>
|
||||
|
||||
|
@ -14,7 +15,7 @@
|
|||
|
||||
static void
|
||||
print_version(void) {
|
||||
fprintf(stderr, "scrcpy %s\n\n", SCRCPY_VERSION);
|
||||
fprintf(stderr, "scrcpy %s (with Mobot extensions)\n\n", SCRCPY_VERSION);
|
||||
|
||||
fprintf(stderr, "dependencies:\n");
|
||||
fprintf(stderr, " - SDL %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION,
|
||||
|
@ -28,6 +29,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);
|
||||
}
|
||||
|
||||
static SDL_LogPriority
|
||||
|
@ -57,6 +61,11 @@ main(int argc, char *argv[]) {
|
|||
setbuf(stderr, NULL);
|
||||
#endif
|
||||
|
||||
#ifndef NDEBUG
|
||||
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
|
||||
#endif
|
||||
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE);
|
||||
|
||||
struct scrcpy_cli_args args = {
|
||||
.opts = SCRCPY_OPTIONS_DEFAULT,
|
||||
.help = false,
|
||||
|
|
|
@ -107,6 +107,7 @@ recorder_destroy(struct recorder *recorder) {
|
|||
static const char *
|
||||
recorder_get_format_name(enum sc_record_format format) {
|
||||
switch (format) {
|
||||
case SC_RECORD_FORMAT_H264: return "h264";
|
||||
case SC_RECORD_FORMAT_MP4: return "mp4";
|
||||
case SC_RECORD_FORMAT_MKV: return "matroska";
|
||||
default: return NULL;
|
||||
|
|
|
@ -24,6 +24,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"
|
||||
|
@ -41,6 +42,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;
|
||||
|
||||
|
@ -175,6 +177,9 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) {
|
|||
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;
|
||||
|
@ -304,6 +309,7 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
|||
|
||||
bool
|
||||
scrcpy(const struct scrcpy_options *options) {
|
||||
log_timestamp("scrcpy Started");
|
||||
if (!server_init(&server)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -315,11 +321,13 @@ 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;
|
||||
|
||||
bool record = !!options->record_filename;
|
||||
bool captures = !!options->capture_filename;
|
||||
struct server_params params = {
|
||||
.log_level = options->log_level,
|
||||
.crop = options->crop,
|
||||
|
@ -336,17 +344,20 @@ scrcpy(const struct scrcpy_options *options) {
|
|||
.encoder_name = options->encoder_name,
|
||||
.force_adb_forward = options->force_adb_forward,
|
||||
};
|
||||
log_timestamp("server_start");
|
||||
if (!server_start(&server, options->serial, ¶ms)) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
server_started = true;
|
||||
|
||||
log_timestamp("sdl_init_and_configure");
|
||||
if (!sdl_init_and_configure(options->display, options->render_driver,
|
||||
options->disable_screensaver)) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
log_timestamp("server_connect_to");
|
||||
if (!server_connect_to(&server)) {
|
||||
goto end;
|
||||
}
|
||||
|
@ -357,12 +368,14 @@ scrcpy(const struct scrcpy_options *options) {
|
|||
// screenrecord does not send frames when the screen content does not
|
||||
// change therefore, we transmit the screen size before the video stream,
|
||||
// to be able to init the window immediately
|
||||
log_timestamp("device_read_info");
|
||||
if (!device_read_info(server.video_socket, device_name, &frame_size)) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
struct decoder *dec = NULL;
|
||||
if (options->display) {
|
||||
log_timestamp("Display and fps_counter_init");
|
||||
if (!fps_counter_init(&fps_counter)) {
|
||||
goto end;
|
||||
}
|
||||
|
@ -398,9 +411,20 @@ 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;
|
||||
}
|
||||
|
||||
log_timestamp("stream_init");
|
||||
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
|
||||
|
@ -451,9 +475,12 @@ scrcpy(const struct scrcpy_options *options) {
|
|||
|
||||
input_manager_init(&input_manager, options);
|
||||
|
||||
log_timestamp("Starting event_loop");
|
||||
ret = event_loop(options);
|
||||
log_timestamp("Terminated event_loop");
|
||||
LOGD("quit...");
|
||||
|
||||
log_timestamp("screen_destroy");
|
||||
screen_destroy(&screen);
|
||||
|
||||
end:
|
||||
|
@ -493,6 +520,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);
|
||||
|
|
|
@ -18,6 +18,7 @@ enum sc_record_format {
|
|||
SC_RECORD_FORMAT_AUTO,
|
||||
SC_RECORD_FORMAT_MP4,
|
||||
SC_RECORD_FORMAT_MKV,
|
||||
SC_RECORD_FORMAT_H264,
|
||||
};
|
||||
|
||||
#define SC_MAX_SHORTCUT_MODS 8
|
||||
|
@ -46,6 +47,7 @@ struct sc_port_range {
|
|||
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;
|
||||
|
@ -87,6 +89,7 @@ struct scrcpy_options {
|
|||
#define SCRCPY_OPTIONS_DEFAULT { \
|
||||
.serial = NULL, \
|
||||
.crop = NULL, \
|
||||
.capture_filename = NULL, \
|
||||
.record_filename = NULL, \
|
||||
.window_title = NULL, \
|
||||
.push_target = NULL, \
|
||||
|
|
|
@ -254,6 +254,7 @@ log_level_to_server_string(enum sc_log_level level) {
|
|||
|
||||
static process_t
|
||||
execute_server(struct server *server, const struct server_params *params) {
|
||||
log_timestamp("Starting device server");
|
||||
char max_size_string[6];
|
||||
char bit_rate_string[11];
|
||||
char max_fps_string[6];
|
||||
|
@ -308,6 +309,7 @@ execute_server(struct server *server, const struct server_params *params) {
|
|||
// Port: 5005
|
||||
// Then click on "Debug"
|
||||
#endif
|
||||
LOGV("Executing ADB command");
|
||||
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
|
||||
}
|
||||
|
||||
|
@ -424,14 +426,17 @@ server_start(struct server *server, const char *serial,
|
|||
}
|
||||
|
||||
if (!push_server(serial)) {
|
||||
LOGE("Failed to push server to device.");
|
||||
goto error1;
|
||||
}
|
||||
|
||||
if (!enable_tunnel_any_port(server, params->port_range,
|
||||
params->force_adb_forward)) {
|
||||
LOGE("Failed to enable tunnel.");
|
||||
goto error1;
|
||||
}
|
||||
|
||||
LOGV("Executing server");
|
||||
// server will connect to our server socket
|
||||
server->process = execute_server(server, params);
|
||||
if (server->process == PROCESS_NONE) {
|
||||
|
@ -473,12 +478,15 @@ error1:
|
|||
|
||||
bool
|
||||
server_connect_to(struct server *server) {
|
||||
log_timestamp("Starting server_connect_to");
|
||||
if (!server->tunnel_forward) {
|
||||
log_timestamp("Accepting video socket");
|
||||
server->video_socket = net_accept(server->server_socket);
|
||||
if (server->video_socket == INVALID_SOCKET) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log_timestamp("Accepting control socket");
|
||||
server->control_socket = net_accept(server->server_socket);
|
||||
if (server->control_socket == INVALID_SOCKET) {
|
||||
// the video_socket will be cleaned up on destroy
|
||||
|
@ -494,6 +502,7 @@ server_connect_to(struct server *server) {
|
|||
} else {
|
||||
uint32_t attempts = 100;
|
||||
uint32_t delay = 100; // ms
|
||||
log_timestamp("Connecting video socket");
|
||||
server->video_socket =
|
||||
connect_to_server(server->local_port, attempts, delay);
|
||||
if (server->video_socket == INVALID_SOCKET) {
|
||||
|
@ -501,6 +510,7 @@ server_connect_to(struct server *server) {
|
|||
}
|
||||
|
||||
// we know that the device is listening, we don't need several attempts
|
||||
log_timestamp("Connecting control socket");
|
||||
server->control_socket =
|
||||
net_connect(IPV4_LOCALHOST, server->local_port);
|
||||
if (server->control_socket == INVALID_SOCKET) {
|
||||
|
@ -512,6 +522,7 @@ server_connect_to(struct server *server) {
|
|||
disable_tunnel(server); // ignore failure
|
||||
server->tunnel_enabled = false;
|
||||
|
||||
log_timestamp("Finished server_connect_to");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
@ -59,6 +60,8 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) {
|
|||
|
||||
packet->pts = pts != NO_PTS ? (int64_t) pts : AV_NOPTS_VALUE;
|
||||
|
||||
log_timestamp("Received packet via socket stream");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -71,6 +74,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 +100,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;
|
||||
}
|
||||
|
||||
|
@ -214,10 +230,17 @@ run_stream(void *data) {
|
|||
}
|
||||
}
|
||||
|
||||
if (stream->capture) {
|
||||
if (!capture_start(stream->capture)) {
|
||||
LOGE("Could not start capture");
|
||||
goto finally_stop_and_join_recorder;
|
||||
}
|
||||
}
|
||||
|
||||
stream->parser = av_parser_init(AV_CODEC_ID_H264);
|
||||
if (!stream->parser) {
|
||||
LOGE("Could not initialize parser");
|
||||
goto finally_stop_and_join_recorder;
|
||||
goto finally_stop_and_join_capture;
|
||||
}
|
||||
|
||||
// We must only pass complete frames to av_parser_parse2()!
|
||||
|
@ -247,6 +270,12 @@ run_stream(void *data) {
|
|||
}
|
||||
|
||||
av_parser_close(stream->parser);
|
||||
finally_stop_and_join_capture:
|
||||
if (stream->capture) {
|
||||
capture_stop(stream->capture);
|
||||
LOGI("Finishing capture...");
|
||||
capture_join(stream->capture);
|
||||
}
|
||||
finally_stop_and_join_recorder:
|
||||
if (stream->recorder) {
|
||||
recorder_stop(stream->recorder);
|
||||
|
@ -270,16 +299,18 @@ 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;
|
||||
}
|
||||
|
||||
bool
|
||||
stream_start(struct stream *stream) {
|
||||
LOGD("Starting stream thread");
|
||||
log_timestamp("Starting stream thread");
|
||||
|
||||
stream->thread = SDL_CreateThread(run_stream, "stream", stream);
|
||||
if (!stream->thread) {
|
||||
|
|
|
@ -17,6 +17,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
|
||||
|
@ -27,7 +28,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);
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
#include <limits.h>
|
||||
#include <signal.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/gmon.h>
|
||||
#include <sys/time.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
@ -26,6 +28,21 @@
|
|||
|
||||
#include "util/log.h"
|
||||
|
||||
uint64_t get_timestamp() {
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv,NULL);
|
||||
return tv.tv_sec*(uint64_t)1000000+tv.tv_usec;
|
||||
}
|
||||
|
||||
static uint64_t StartTime = 0;
|
||||
void log_timestamp(const char *tag) {
|
||||
uint64_t now = get_timestamp();
|
||||
if (StartTime==0) {
|
||||
StartTime = now;
|
||||
}
|
||||
LOGV("Timestamp: %llu ms %s", (now-StartTime) / 1000, tag);
|
||||
}
|
||||
|
||||
bool
|
||||
cmd_search(const char *file) {
|
||||
char *path = getenv("PATH");
|
||||
|
@ -66,6 +83,8 @@ enum process_result
|
|||
cmd_execute(const char *const argv[], pid_t *pid) {
|
||||
int fd[2];
|
||||
|
||||
uint64_t start = get_timestamp();
|
||||
|
||||
if (pipe(fd) == -1) {
|
||||
perror("pipe");
|
||||
return PROCESS_ERROR_GENERIC;
|
||||
|
@ -73,6 +92,7 @@ cmd_execute(const char *const argv[], pid_t *pid) {
|
|||
|
||||
enum process_result ret = PROCESS_SUCCESS;
|
||||
|
||||
log_timestamp("Running command");
|
||||
*pid = fork();
|
||||
if (*pid == -1) {
|
||||
perror("fork");
|
||||
|
@ -90,6 +110,7 @@ cmd_execute(const char *const argv[], pid_t *pid) {
|
|||
ret = PROCESS_ERROR_GENERIC;
|
||||
goto end;
|
||||
}
|
||||
LOGV("Command microseconds: %s %llu ms", argv[0], (get_timestamp()-start)/1000);
|
||||
} else if (*pid == 0) {
|
||||
// child close read side
|
||||
close(fd[0]);
|
||||
|
|
|
@ -10,4 +10,7 @@
|
|||
#define LOGE(...) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
||||
#define LOGC(...) SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
||||
|
||||
#include <time.h>
|
||||
uint64_t get_timestamp();
|
||||
void log_timestamp(const char *tag);
|
||||
#endif
|
||||
|
|
|
@ -58,6 +58,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",
|
||||
|
@ -87,6 +88,7 @@ static void test_options(void) {
|
|||
assert(!strcmp(opts->record_filename, "file"));
|
||||
assert(opts->record_format == SC_RECORD_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);
|
||||
|
|
Loading…
Add table
Reference in a new issue