Add support for v4l2loopback

It allows to send the video stream to /dev/videoN, so that it can be
captured (like a webcam) by any v4l2-capable tool.

PR #2232 <https://github.com/Genymobile/scrcpy/pull/2232>
PR #2233 <https://github.com/Genymobile/scrcpy/pull/2233>
PR #2268 <https://github.com/Genymobile/scrcpy/pull/2268>

Co-authored-by: Romain Vimont <rom@rom1v.com>
This commit is contained in:
Marco Martinelli 2021-04-04 00:10:44 +02:00 committed by Romain Vimont
parent 5af9d0ee0f
commit d39161f753
9 changed files with 480 additions and 2 deletions

View file

@ -33,6 +33,11 @@ else
src += [ 'src/sys/unix/process.c' ] src += [ 'src/sys/unix/process.c' ]
endif endif
v4l2_support = host_machine.system() == 'linux'
if v4l2_support
src += [ 'src/v4l2_sink.c' ]
endif
check_functions = [ check_functions = [
'strdup' 'strdup'
] ]
@ -49,6 +54,10 @@ if not get_option('crossbuild_windows')
dependency('sdl2'), dependency('sdl2'),
] ]
if v4l2_support
dependencies += dependency('libavdevice')
endif
else else
# cross-compile mingw32 build (from Linux to Windows) # cross-compile mingw32 build (from Linux to Windows)
@ -124,6 +133,9 @@ conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
# select the debugger method ('old' for Android < 9, 'new' for Android >= 9) # select the debugger method ('old' for Android < 9, 'new' for Android >= 9)
conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new') conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new')
# enable V4L2 support (linux only)
conf.set('HAVE_V4L2', v4l2_support)
configure_file(configuration: conf, output: 'config.h') configure_file(configuration: conf, output: 'config.h')
src_dir = include_directories('src') src_dir = include_directories('src')

View file

@ -185,6 +185,12 @@ Enable "show touches" on start, restore the initial value on exit.
It only shows physical touches (not clicks from scrcpy). It only shows physical touches (not clicks from scrcpy).
.TP
.BI "\-\-v4l2_sink " /dev/videoN
Output to v4l2loopback device.
It requires to lock the video orientation (see --lock-video-orientation).
.TP .TP
.BI "\-V, \-\-verbosity " value .BI "\-V, \-\-verbosity " value
Set the log level ("debug", "info", "warn" or "error"). Set the log level ("debug", "info", "warn" or "error").

View file

@ -176,6 +176,13 @@ scrcpy_print_usage(const char *arg0) {
" on exit.\n" " on exit.\n"
" It only shows physical touches (not clicks from scrcpy).\n" " It only shows physical touches (not clicks from scrcpy).\n"
"\n" "\n"
#ifdef HAVE_V4L2
" --v4l2_sink /dev/videoN\n"
" Output to v4l2loopback device.\n"
" It requires to lock the video orientation (see\n"
" --lock-video-orientation).\n"
"\n"
#endif
" -V, --verbosity value\n" " -V, --verbosity value\n"
" Set the log level (debug, info, warn or error).\n" " Set the log level (debug, info, warn or error).\n"
#ifndef NDEBUG #ifndef NDEBUG
@ -676,6 +683,7 @@ guess_record_format(const char *filename) {
#define OPT_LEGACY_PASTE 1024 #define OPT_LEGACY_PASTE 1024
#define OPT_ENCODER_NAME 1025 #define OPT_ENCODER_NAME 1025
#define OPT_POWER_OFF_ON_CLOSE 1026 #define OPT_POWER_OFF_ON_CLOSE 1026
#define OPT_V4L2_SINK 1027
bool bool
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
@ -717,6 +725,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
{"show-touches", no_argument, NULL, 't'}, {"show-touches", no_argument, NULL, 't'},
{"stay-awake", no_argument, NULL, 'w'}, {"stay-awake", no_argument, NULL, 'w'},
{"turn-screen-off", no_argument, NULL, 'S'}, {"turn-screen-off", no_argument, NULL, 'S'},
#ifdef HAVE_V4L2
{"v4l2_sink", required_argument, NULL, OPT_V4L2_SINK},
#endif
{"verbosity", required_argument, NULL, 'V'}, {"verbosity", required_argument, NULL, 'V'},
{"version", no_argument, NULL, 'v'}, {"version", no_argument, NULL, 'v'},
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, {"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
@ -901,16 +912,36 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
case OPT_POWER_OFF_ON_CLOSE: case OPT_POWER_OFF_ON_CLOSE:
opts->power_off_on_close = true; opts->power_off_on_close = true;
break; break;
#ifdef HAVE_V4L2
case OPT_V4L2_SINK:
opts->v4l2_device = optarg;
break;
#endif
default: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
return false; return false;
} }
} }
#ifdef HAVE_V4L2
if (!opts->display && !opts->record_filename && !opts->v4l2_device) {
LOGE("-N/--no-display requires either screen recording (-r/--record)"
" or sink to v4l2loopback device (--v4l2_sink)");
return false;
}
if (opts->v4l2_device && opts->lock_video_orientation
== SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
LOGI("Video orientation is locked for v4l2 sink. "
"See --lock-video-orientation.");
opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
}
#else
if (!opts->display && !opts->record_filename) { if (!opts->display && !opts->record_filename) {
LOGE("-N/--no-display requires screen recording (-r/--record)"); LOGE("-N/--no-display requires screen recording (-r/--record)");
return false; return false;
} }
#endif
int index = optind; int index = optind;
if (index < argc) { if (index < argc) {

View file

@ -8,7 +8,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#define DECODER_MAX_SINKS 1 #define DECODER_MAX_SINKS 2
struct decoder { struct decoder {
struct sc_packet_sink packet_sink; // packet sink trait struct sc_packet_sink packet_sink; // packet sink trait

View file

@ -6,6 +6,9 @@
#include <stdbool.h> #include <stdbool.h>
#include <unistd.h> #include <unistd.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#ifdef HAVE_V4L2
# include <libavdevice/avdevice.h>
#endif
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem #define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
@ -28,6 +31,11 @@ print_version(void) {
fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR, fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR,
LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MINOR,
LIBAVUTIL_VERSION_MICRO); LIBAVUTIL_VERSION_MICRO);
#ifdef HAVE_V4L2
fprintf(stderr, " - libavdevice %d.%d.%d\n", LIBAVDEVICE_VERSION_MAJOR,
LIBAVDEVICE_VERSION_MINOR,
LIBAVDEVICE_VERSION_MICRO);
#endif
} }
static SDL_LogPriority static SDL_LogPriority
@ -90,6 +98,12 @@ main(int argc, char *argv[]) {
av_register_all(); av_register_all();
#endif #endif
#ifdef HAVE_V4L2
if (args.opts.v4l2_device) {
avdevice_register_all();
}
#endif
if (avformat_network_init()) { if (avformat_network_init()) {
return 1; return 1;
} }

View file

@ -27,6 +27,9 @@
#include "tiny_xpm.h" #include "tiny_xpm.h"
#include "util/log.h" #include "util/log.h"
#include "util/net.h" #include "util/net.h"
#ifdef HAVE_V4L2
# include "v4l2_sink.h"
#endif
static struct server server; static struct server server;
static struct screen screen; static struct screen screen;
@ -34,6 +37,9 @@ static struct fps_counter fps_counter;
static struct stream stream; static struct stream stream;
static struct decoder decoder; static struct decoder decoder;
static struct recorder recorder; static struct recorder recorder;
#ifdef HAVE_V4L2
static struct sc_v4l2_sink v4l2_sink;
#endif
static struct controller controller; static struct controller controller;
static struct file_handler file_handler; static struct file_handler file_handler;
@ -247,6 +253,9 @@ scrcpy(const struct scrcpy_options *options) {
bool fps_counter_initialized = false; bool fps_counter_initialized = false;
bool file_handler_initialized = false; bool file_handler_initialized = false;
bool recorder_initialized = false; bool recorder_initialized = false;
#ifdef HAVE_V4L2
bool v4l2_sink_initialized = false;
#endif
bool stream_started = false; bool stream_started = false;
bool controller_initialized = false; bool controller_initialized = false;
bool controller_started = false; bool controller_started = false;
@ -295,7 +304,6 @@ scrcpy(const struct scrcpy_options *options) {
goto end; goto end;
} }
struct decoder *dec = NULL;
if (options->display) { if (options->display) {
if (!fps_counter_init(&fps_counter)) { if (!fps_counter_init(&fps_counter)) {
goto end; goto end;
@ -309,7 +317,14 @@ scrcpy(const struct scrcpy_options *options) {
} }
file_handler_initialized = true; file_handler_initialized = true;
} }
}
struct decoder *dec = NULL;
bool needs_decoder = options->display;
#ifdef HAVE_V4L2
needs_decoder |= !!options->v4l2_device;
#endif
if (needs_decoder) {
decoder_init(&decoder); decoder_init(&decoder);
dec = &decoder; dec = &decoder;
} }
@ -386,6 +401,18 @@ scrcpy(const struct scrcpy_options *options) {
} }
} }
#ifdef HAVE_V4L2
if (options->v4l2_device) {
if (!sc_v4l2_sink_init(&v4l2_sink, options->v4l2_device, frame_size)) {
goto end;
}
decoder_add_sink(&decoder, &v4l2_sink.frame_sink);
v4l2_sink_initialized = true;
}
#endif
// now we consumed the header values, the socket receives the video stream // now we consumed the header values, the socket receives the video stream
// start the stream // start the stream
if (!stream_start(&stream)) { if (!stream_start(&stream)) {
@ -426,6 +453,12 @@ end:
stream_join(&stream); stream_join(&stream);
} }
#ifdef HAVE_V4L2
if (v4l2_sink_initialized) {
sc_v4l2_sink_destroy(&v4l2_sink);
}
#endif
// Destroy the screen only after the stream is guaranteed to be finished, // Destroy the screen only after the stream is guaranteed to be finished,
// because otherwise the screen could receive new frames after destruction // because otherwise the screen could receive new frames after destruction
if (screen_initialized) { if (screen_initialized) {

View file

@ -62,6 +62,7 @@ struct scrcpy_options {
const char *render_driver; const char *render_driver;
const char *codec_options; const char *codec_options;
const char *encoder_name; const char *encoder_name;
const char *v4l2_device;
enum sc_log_level log_level; enum sc_log_level log_level;
enum sc_record_format record_format; enum sc_record_format record_format;
struct sc_port_range port_range; struct sc_port_range port_range;
@ -103,6 +104,7 @@ struct scrcpy_options {
.render_driver = NULL, \ .render_driver = NULL, \
.codec_options = NULL, \ .codec_options = NULL, \
.encoder_name = NULL, \ .encoder_name = NULL, \
.v4l2_device = NULL, \
.log_level = SC_LOG_LEVEL_INFO, \ .log_level = SC_LOG_LEVEL_INFO, \
.record_format = SC_RECORD_FORMAT_AUTO, \ .record_format = SC_RECORD_FORMAT_AUTO, \
.port_range = { \ .port_range = { \

341
app/src/v4l2_sink.c Normal file
View file

@ -0,0 +1,341 @@
#include "v4l2_sink.h"
#include "util/log.h"
#include "util/str_util.h"
/** Downcast frame_sink to sc_v4l2_sink */
#define DOWNCAST(SINK) container_of(SINK, struct sc_v4l2_sink, frame_sink)
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
static const AVOutputFormat *
find_muxer(const char *name) {
#ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
void *opaque = NULL;
#endif
const AVOutputFormat *oformat = NULL;
do {
#ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
oformat = av_muxer_iterate(&opaque);
#else
oformat = av_oformat_next(oformat);
#endif
// until null or containing the requested name
} while (oformat && !strlist_contains(oformat->name, ',', name));
return oformat;
}
static bool
write_header(struct sc_v4l2_sink *vs, const AVPacket *packet) {
AVStream *ostream = vs->format_ctx->streams[0];
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
if (!extradata) {
LOGC("Could not allocate extradata");
return false;
}
// copy the first packet to the extra data
memcpy(extradata, packet->data, packet->size);
ostream->codecpar->extradata = extradata;
ostream->codecpar->extradata_size = packet->size;
int ret = avformat_write_header(vs->format_ctx, NULL);
if (ret < 0) {
LOGE("Failed to write header to %s", vs->device_name);
return false;
}
return true;
}
static void
rescale_packet(struct sc_v4l2_sink *vs, AVPacket *packet) {
AVStream *ostream = vs->format_ctx->streams[0];
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
}
static bool
write_packet(struct sc_v4l2_sink *vs, AVPacket *packet) {
if (!vs->header_written) {
bool ok = write_header(vs, packet);
if (!ok) {
return false;
}
vs->header_written = true;
return true;
}
rescale_packet(vs, packet);
bool ok = av_write_frame(vs->format_ctx, packet) >= 0;
// Failing to write the last frame is not very serious, no future frame may
// depend on it, so the resulting file will still be valid
(void) ok;
return true;
}
static bool
encode_and_write_frame(struct sc_v4l2_sink *vs, const AVFrame *frame) {
int ret = avcodec_send_frame(vs->encoder_ctx, frame);
if (ret < 0 && ret != AVERROR(EAGAIN)) {
LOGE("Could not send v4l2 video frame: %d", ret);
return false;
}
AVPacket *packet = &vs->packet;
ret = avcodec_receive_packet(vs->encoder_ctx, packet);
if (ret == 0) {
// A packet was received
bool ok = write_packet(vs, packet);
if (!ok) {
LOGW("Could not send packet to v4l2 sink");
return false;
}
av_packet_unref(packet);
} else if (ret != AVERROR(EAGAIN)) {
LOGE("Could not receive v4l2 video packet: %d", ret);
return false;
}
return true;
}
static int
run_v4l2_sink(void *data) {
struct sc_v4l2_sink *vs = data;
for (;;) {
sc_mutex_lock(&vs->mutex);
while (!vs->stopped && vs->vb.pending_frame_consumed) {
sc_cond_wait(&vs->cond, &vs->mutex);
}
if (vs->stopped) {
sc_mutex_unlock(&vs->mutex);
break;
}
sc_mutex_unlock(&vs->mutex);
video_buffer_consume(&vs->vb, vs->frame);
bool ok = encode_and_write_frame(vs, vs->frame);
if (!ok) {
LOGE("Could not send frame to v4l2 sink");
break;
}
}
LOGD("V4l2 thread ended");
return 0;
}
static bool
sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
bool ok = video_buffer_init(&vs->vb);
if (!ok) {
return false;
}
ok = sc_mutex_init(&vs->mutex);
if (!ok) {
LOGC("Could not create mutex");
goto error_video_buffer_destroy;
}
ok = sc_cond_init(&vs->cond);
if (!ok) {
LOGC("Could not create cond");
goto error_mutex_destroy;
}
// FIXME
const AVOutputFormat *format = find_muxer("video4linux2,v4l2");
if (!format) {
LOGE("Could not find v4l2 muxer");
goto error_cond_destroy;
}
const AVCodec *encoder = avcodec_find_encoder(AV_CODEC_ID_RAWVIDEO);
if (!encoder) {
LOGE("Raw video encoder not found");
return false;
}
vs->format_ctx = avformat_alloc_context();
if (!vs->format_ctx) {
LOGE("Could not allocate v4l2 output context");
return 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)
// <https://github.com/FFmpeg/FFmpeg/commit/0694d8702421e7aff1340038559c438b61bb30dd>
vs->format_ctx->oformat = (AVOutputFormat *) format;
vs->format_ctx->url = strdup(vs->device_name);
if (!vs->format_ctx->url) {
LOGE("Could not strdup v4l2 device name");
goto error_avformat_free_context;
return false;
}
AVStream *ostream = avformat_new_stream(vs->format_ctx, encoder);
if (!ostream) {
LOGE("Could not allocate new v4l2 stream");
goto error_avformat_free_context;
return false;
}
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
ostream->codecpar->codec_id = encoder->id;
ostream->codecpar->format = AV_PIX_FMT_YUV420P;
ostream->codecpar->width = vs->frame_size.width;
ostream->codecpar->height = vs->frame_size.height;
int ret = avio_open(&vs->format_ctx->pb, vs->device_name, AVIO_FLAG_WRITE);
if (ret < 0) {
LOGE("Failed to open output device: %s", vs->device_name);
// ostream will be cleaned up during context cleaning
goto error_avformat_free_context;
}
vs->encoder_ctx = avcodec_alloc_context3(encoder);
if (!vs->encoder_ctx) {
LOGC("Could not allocate codec context for v4l2");
goto error_avio_close;
}
vs->encoder_ctx->width = vs->frame_size.width;
vs->encoder_ctx->height = vs->frame_size.height;
vs->encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
vs->encoder_ctx->time_base.num = 1;
vs->encoder_ctx->time_base.den = 1;
if (avcodec_open2(vs->encoder_ctx, encoder, NULL) < 0) {
LOGE("Could not open codec for v4l2");
goto error_avcodec_free_context;
}
vs->frame = av_frame_alloc();
if (!vs->frame) {
LOGE("Could not create v4l2 frame");
goto error_avcodec_close;
}
LOGD("Starting v4l2 thread");
ok = sc_thread_create(&vs->thread, run_v4l2_sink, "v4l2", vs);
if (!ok) {
LOGC("Could not start v4l2 thread");
goto error_av_frame_free;
}
vs->header_written = false;
vs->stopped = false;
LOGI("v4l2 sink started to device: %s", vs->device_name);
return true;
error_av_frame_free:
av_frame_free(&vs->frame);
error_avcodec_close:
avcodec_close(vs->encoder_ctx);
error_avcodec_free_context:
avcodec_free_context(&vs->encoder_ctx);
error_avio_close:
avio_close(vs->format_ctx->pb);
error_avformat_free_context:
avformat_free_context(vs->format_ctx);
error_cond_destroy:
sc_cond_destroy(&vs->cond);
error_mutex_destroy:
sc_mutex_destroy(&vs->mutex);
error_video_buffer_destroy:
video_buffer_destroy(&vs->vb);
return false;
}
static void
sc_v4l2_sink_close(struct sc_v4l2_sink *vs) {
sc_mutex_lock(&vs->mutex);
vs->stopped = true;
sc_cond_signal(&vs->cond);
sc_mutex_unlock(&vs->mutex);
sc_thread_join(&vs->thread, NULL);
av_frame_free(&vs->frame);
avcodec_close(vs->encoder_ctx);
avcodec_free_context(&vs->encoder_ctx);
avio_close(vs->format_ctx->pb);
avformat_free_context(vs->format_ctx);
sc_cond_destroy(&vs->cond);
sc_mutex_destroy(&vs->mutex);
video_buffer_destroy(&vs->vb);
}
static bool
sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) {
bool ok = video_buffer_push(&vs->vb, frame, NULL);
if (!ok) {
return false;
}
// signal possible change of vs->vb.pending_frame_consumed
sc_cond_signal(&vs->cond);
return true;
}
static bool
sc_v4l2_frame_sink_open(struct sc_frame_sink *sink) {
struct sc_v4l2_sink *vs = DOWNCAST(sink);
return sc_v4l2_sink_open(vs);
}
static void
sc_v4l2_frame_sink_close(struct sc_frame_sink *sink) {
struct sc_v4l2_sink *vs = DOWNCAST(sink);
sc_v4l2_sink_close(vs);
}
static bool
sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
struct sc_v4l2_sink *vs = DOWNCAST(sink);
return sc_v4l2_sink_push(vs, frame);
}
bool
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
struct size frame_size) {
vs->device_name = strdup(device_name);
if (!vs->device_name) {
LOGE("Could not strdup v4l2 device name");
return false;
}
vs->frame_size = frame_size;
static const struct sc_frame_sink_ops ops = {
.open = sc_v4l2_frame_sink_open,
.close = sc_v4l2_frame_sink_close,
.push = sc_v4l2_frame_sink_push,
};
vs->frame_sink.ops = &ops;
return true;
}
void
sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs) {
free(vs->device_name);
}

39
app/src/v4l2_sink.h Normal file
View file

@ -0,0 +1,39 @@
#ifndef SC_V4L2_SINK_H
#define SC_V4L2_SINK_H
#include "common.h"
#include "coords.h"
#include "trait/frame_sink.h"
#include "video_buffer.h"
#include <libavformat/avformat.h>
struct sc_v4l2_sink {
struct sc_frame_sink frame_sink; // frame sink trait
struct video_buffer vb;
AVFormatContext *format_ctx;
AVCodecContext *encoder_ctx;
char *device_name;
struct size frame_size;
sc_thread thread;
sc_mutex mutex;
sc_cond cond;
bool stopped;
bool header_written;
AVFrame *frame;
AVPacket packet;
};
bool
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
struct size frame_size);
void
sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs);
#endif