mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-08-03 14:49:29 +00:00
v4l2loopback support
This commit is contained in:
parent
1d615a0d51
commit
77447ff6c6
9 changed files with 661 additions and 6 deletions
|
@ -21,12 +21,15 @@ src = [
|
||||||
'src/stream.c',
|
'src/stream.c',
|
||||||
'src/tiny_xpm.c',
|
'src/tiny_xpm.c',
|
||||||
'src/video_buffer.c',
|
'src/video_buffer.c',
|
||||||
|
'src/v4l2sink.c',
|
||||||
'src/util/net.c',
|
'src/util/net.c',
|
||||||
'src/util/process.c',
|
'src/util/process.c',
|
||||||
'src/util/str_util.c',
|
'src/util/str_util.c',
|
||||||
'src/util/thread.c',
|
'src/util/thread.c',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
conf = configuration_data()
|
||||||
|
|
||||||
if host_machine.system() == 'windows'
|
if host_machine.system() == 'windows'
|
||||||
src += [ 'src/sys/win/process.c' ]
|
src += [ 'src/sys/win/process.c' ]
|
||||||
else
|
else
|
||||||
|
@ -49,6 +52,11 @@ if not get_option('crossbuild_windows')
|
||||||
dependency('sdl2'),
|
dependency('sdl2'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if host_machine.system() == 'linux'
|
||||||
|
dependencies += dependency('libavdevice')
|
||||||
|
conf.set('V4L2SINK', '1')
|
||||||
|
endif
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|
||||||
# cross-compile mingw32 build (from Linux to Windows)
|
# cross-compile mingw32 build (from Linux to Windows)
|
||||||
|
@ -90,8 +98,6 @@ if host_machine.system() == 'windows'
|
||||||
dependencies += cc.find_library('ws2_32')
|
dependencies += cc.find_library('ws2_32')
|
||||||
endif
|
endif
|
||||||
|
|
||||||
conf = configuration_data()
|
|
||||||
|
|
||||||
foreach f : check_functions
|
foreach f : check_functions
|
||||||
if cc.has_function(f)
|
if cc.has_function(f)
|
||||||
define = 'HAVE_' + f.underscorify().to_upper()
|
define = 'HAVE_' + f.underscorify().to_upper()
|
||||||
|
|
|
@ -179,6 +179,11 @@ 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 V4L2SINK
|
||||||
|
" --v4l2sink /dev/videoN\n"
|
||||||
|
" Output to v4l2loopback device. It will lock video orientation. Use --lock-video-orientation to override the rotation.\n"
|
||||||
|
"\n"
|
||||||
|
#endif
|
||||||
" -v, --version\n"
|
" -v, --version\n"
|
||||||
" Print the version of scrcpy.\n"
|
" Print the version of scrcpy.\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
@ -667,6 +672,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_V4L2SINK 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[]) {
|
||||||
|
@ -708,6 +714,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 V4L2SINK
|
||||||
|
{"v4l2sink", required_argument, NULL, OPT_V4L2SINK},
|
||||||
|
#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},
|
||||||
|
@ -890,14 +899,28 @@ 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 V4L2SINK
|
||||||
|
case OPT_V4L2SINK:
|
||||||
|
opts->v4l2sink_device = optarg;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
default:
|
default:
|
||||||
// getopt prints the error message on stderr
|
// getopt prints the error message on stderr
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!opts->display && !opts->record_filename) {
|
//v4l2loopback can't handle resolution changes so we must lock video orientation in case this is used
|
||||||
|
if (opts->v4l2sink_device && opts->lock_video_orientation < 0) {
|
||||||
|
opts->lock_video_orientation = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opts->display && !opts->record_filename && !opts->v4l2sink_device) {
|
||||||
|
#ifdef V4L2SINK
|
||||||
|
LOGE("-N/--no-display requires screen recording (-r/--record) or sink to v4l2loopback device (--v4l2sink)");
|
||||||
|
#else
|
||||||
LOGE("-N/--no-display requires screen recording (-r/--record)");
|
LOGE("-N/--no-display requires screen recording (-r/--record)");
|
||||||
|
#endif
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 V4L2SINK
|
||||||
|
#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 V4L2SINK
|
||||||
|
fprintf(stderr, " - libavdevice %d.%d.%d\n", LIBAVDEVICE_VERSION_MAJOR,
|
||||||
|
LIBAVDEVICE_VERSION_MINOR,
|
||||||
|
LIBAVDEVICE_VERSION_MICRO);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
static SDL_LogPriority
|
static SDL_LogPriority
|
||||||
|
@ -89,6 +97,9 @@ main(int argc, char *argv[]) {
|
||||||
#ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL
|
#ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL
|
||||||
av_register_all();
|
av_register_all();
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef V4L2SINK
|
||||||
|
avdevice_register_all();
|
||||||
|
#endif
|
||||||
|
|
||||||
if (avformat_network_init()) {
|
if (avformat_network_init()) {
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
#include "stream.h"
|
#include "stream.h"
|
||||||
#include "tiny_xpm.h"
|
#include "tiny_xpm.h"
|
||||||
#include "video_buffer.h"
|
#include "video_buffer.h"
|
||||||
|
#include "v4l2sink.h"
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
#include "util/net.h"
|
#include "util/net.h"
|
||||||
|
|
||||||
|
@ -38,6 +39,9 @@ static struct decoder decoder;
|
||||||
static struct recorder recorder;
|
static struct recorder recorder;
|
||||||
static struct controller controller;
|
static struct controller controller;
|
||||||
static struct file_handler file_handler;
|
static struct file_handler file_handler;
|
||||||
|
#ifdef V4L2SINK
|
||||||
|
static struct v4l2sink v4l2sink;
|
||||||
|
#endif
|
||||||
|
|
||||||
static struct input_manager input_manager = {
|
static struct input_manager input_manager = {
|
||||||
.controller = &controller,
|
.controller = &controller,
|
||||||
|
@ -285,6 +289,12 @@ scrcpy(const struct scrcpy_options *options) {
|
||||||
bool controller_started = false;
|
bool controller_started = false;
|
||||||
|
|
||||||
bool record = !!options->record_filename;
|
bool record = !!options->record_filename;
|
||||||
|
|
||||||
|
#ifdef V4L2SINK
|
||||||
|
bool v4l2sink_initialized = false;
|
||||||
|
bool v4l2 = !!options->v4l2sink_device;
|
||||||
|
#endif
|
||||||
|
|
||||||
struct server_params params = {
|
struct server_params params = {
|
||||||
.log_level = options->log_level,
|
.log_level = options->log_level,
|
||||||
.crop = options->crop,
|
.crop = options->crop,
|
||||||
|
@ -363,9 +373,22 @@ scrcpy(const struct scrcpy_options *options) {
|
||||||
recorder_initialized = true;
|
recorder_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct v4l2sink *sink = NULL;
|
||||||
|
#ifdef V4L2SINK
|
||||||
|
if (v4l2) {
|
||||||
|
if (!v4l2sink_init(&v4l2sink,
|
||||||
|
options->v4l2sink_device,
|
||||||
|
frame_size)) {
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
sink = &v4l2sink;
|
||||||
|
v4l2sink_initialized = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
av_log_set_callback(av_log_callback);
|
av_log_set_callback(av_log_callback);
|
||||||
|
|
||||||
stream_init(&stream, server.video_socket, dec, rec);
|
stream_init(&stream, server.video_socket, dec, rec, sink);
|
||||||
|
|
||||||
if (options->display) {
|
if (options->display) {
|
||||||
if (options->control) {
|
if (options->control) {
|
||||||
|
@ -467,6 +490,12 @@ end:
|
||||||
recorder_destroy(&recorder);
|
recorder_destroy(&recorder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef V4L2SINK
|
||||||
|
if (v4l2sink_initialized) {
|
||||||
|
v4l2sink_destroy(&v4l2sink);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (file_handler_initialized) {
|
if (file_handler_initialized) {
|
||||||
file_handler_join(&file_handler);
|
file_handler_join(&file_handler);
|
||||||
file_handler_destroy(&file_handler);
|
file_handler_destroy(&file_handler);
|
||||||
|
|
|
@ -52,6 +52,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 *v4l2sink_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;
|
||||||
|
@ -94,6 +95,7 @@ struct scrcpy_options {
|
||||||
.render_driver = NULL, \
|
.render_driver = NULL, \
|
||||||
.codec_options = NULL, \
|
.codec_options = NULL, \
|
||||||
.encoder_name = NULL, \
|
.encoder_name = NULL, \
|
||||||
|
.v4l2sink_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 = { \
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include "decoder.h"
|
#include "decoder.h"
|
||||||
#include "events.h"
|
#include "events.h"
|
||||||
#include "recorder.h"
|
#include "recorder.h"
|
||||||
|
#include "v4l2sink.h"
|
||||||
#include "util/buffer_util.h"
|
#include "util/buffer_util.h"
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
|
@ -89,6 +90,17 @@ process_frame(struct stream *stream, AVPacket *packet) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef V4L2SINK
|
||||||
|
if (stream->v4l2sink) {
|
||||||
|
packet->dts = packet->pts;
|
||||||
|
|
||||||
|
if (!v4l2sink_push(stream->v4l2sink, packet)) {
|
||||||
|
LOGE("Could not send packet to v4l2sink");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,6 +222,20 @@ run_stream(void *data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef V4L2SINK
|
||||||
|
if (stream->v4l2sink) {
|
||||||
|
if (!v4l2sink_open(stream->v4l2sink, codec)) {
|
||||||
|
LOGE("Could not open v4l2sink");
|
||||||
|
goto finally_close_decoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!v4l2sink_start(stream->v4l2sink)) {
|
||||||
|
LOGE("Could not start v4l2sink");
|
||||||
|
goto finally_close_v4l2sink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
stream->parser = av_parser_init(AV_CODEC_ID_H264);
|
stream->parser = av_parser_init(AV_CODEC_ID_H264);
|
||||||
if (!stream->parser) {
|
if (!stream->parser) {
|
||||||
LOGE("Could not initialize parser");
|
LOGE("Could not initialize parser");
|
||||||
|
@ -253,6 +279,16 @@ finally_close_recorder:
|
||||||
if (stream->recorder) {
|
if (stream->recorder) {
|
||||||
recorder_close(stream->recorder);
|
recorder_close(stream->recorder);
|
||||||
}
|
}
|
||||||
|
#ifdef V4L2SINK
|
||||||
|
finally_close_v4l2sink:
|
||||||
|
if (stream->v4l2sink) {
|
||||||
|
v4l2sink_stop(stream->v4l2sink);
|
||||||
|
LOGI("Finishing v4l2sink...");
|
||||||
|
v4l2sink_join(stream->v4l2sink);
|
||||||
|
|
||||||
|
v4l2sink_close(stream->v4l2sink);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
finally_close_decoder:
|
finally_close_decoder:
|
||||||
if (stream->decoder) {
|
if (stream->decoder) {
|
||||||
decoder_close(stream->decoder);
|
decoder_close(stream->decoder);
|
||||||
|
@ -266,10 +302,11 @@ end:
|
||||||
|
|
||||||
void
|
void
|
||||||
stream_init(struct stream *stream, socket_t socket,
|
stream_init(struct stream *stream, socket_t socket,
|
||||||
struct decoder *decoder, struct recorder *recorder) {
|
struct decoder *decoder, struct recorder *recorder, struct v4l2sink *v4l2sink) {
|
||||||
stream->socket = socket;
|
stream->socket = socket;
|
||||||
stream->decoder = decoder,
|
stream->decoder = decoder,
|
||||||
stream->recorder = recorder;
|
stream->recorder = recorder;
|
||||||
|
stream->v4l2sink = v4l2sink;
|
||||||
stream->has_pending = false;
|
stream->has_pending = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ struct stream {
|
||||||
sc_thread thread;
|
sc_thread thread;
|
||||||
struct decoder *decoder;
|
struct decoder *decoder;
|
||||||
struct recorder *recorder;
|
struct recorder *recorder;
|
||||||
|
struct v4l2sink *v4l2sink;
|
||||||
AVCodecContext *codec_ctx;
|
AVCodecContext *codec_ctx;
|
||||||
AVCodecParserContext *parser;
|
AVCodecParserContext *parser;
|
||||||
// successive packets may need to be concatenated, until a non-config
|
// successive packets may need to be concatenated, until a non-config
|
||||||
|
@ -28,7 +29,7 @@ struct stream {
|
||||||
|
|
||||||
void
|
void
|
||||||
stream_init(struct stream *stream, socket_t socket,
|
stream_init(struct stream *stream, socket_t socket,
|
||||||
struct decoder *decoder, struct recorder *recorder);
|
struct decoder *decoder, struct recorder *recorder, struct v4l2sink *v4l2sink);
|
||||||
|
|
||||||
bool
|
bool
|
||||||
stream_start(struct stream *stream);
|
stream_start(struct stream *stream);
|
||||||
|
|
466
app/src/v4l2sink.c
Normal file
466
app/src/v4l2sink.c
Normal file
|
@ -0,0 +1,466 @@
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#ifdef V4L2SINK
|
||||||
|
#include "v4l2sink.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <libavutil/time.h>
|
||||||
|
|
||||||
|
#include "util/log.h"
|
||||||
|
|
||||||
|
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 with name "v4l2"
|
||||||
|
} while (oformat && !strstr(oformat->name, name));
|
||||||
|
return oformat;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct v4l2sink_packet *
|
||||||
|
v4l2sink_packet_new(const AVPacket *packet) {
|
||||||
|
struct v4l2sink_packet *rec = 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)) {
|
||||||
|
free(rec);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return rec;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
v4l2sink_packet_delete(struct v4l2sink_packet *rec) {
|
||||||
|
av_packet_unref(&rec->packet);
|
||||||
|
free(rec);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
v4l2sink_queue_clear(struct v4l2sink_queue *queue) {
|
||||||
|
while (!queue_is_empty(queue)) {
|
||||||
|
struct v4l2sink_packet *rec;
|
||||||
|
queue_take(queue, next, &rec);
|
||||||
|
v4l2sink_packet_delete(rec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
v4l2sink_init(struct v4l2sink *v4l2sink,
|
||||||
|
const char *devicename,
|
||||||
|
struct size declared_frame_size) {
|
||||||
|
v4l2sink->devicename = strdup(devicename);
|
||||||
|
if (!v4l2sink->devicename) {
|
||||||
|
LOGE("Could not strdup devicename for v4l2sink");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ok = sc_mutex_init(&v4l2sink->mutex);
|
||||||
|
if (!ok) {
|
||||||
|
LOGC("Could not create mutex for v4l2sink");
|
||||||
|
free(v4l2sink->devicename);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = sc_cond_init(&v4l2sink->queue_cond);
|
||||||
|
if (!ok) {
|
||||||
|
LOGC("Could not create cond for v4l2sink");
|
||||||
|
sc_mutex_destroy(&v4l2sink->mutex);
|
||||||
|
free(v4l2sink->devicename);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
queue_init(&v4l2sink->queue);
|
||||||
|
v4l2sink->stopped = false;
|
||||||
|
v4l2sink->failed = false;
|
||||||
|
v4l2sink->declared_frame_size = declared_frame_size;
|
||||||
|
v4l2sink->header_written = false;
|
||||||
|
v4l2sink->previous = NULL;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
v4l2sink_destroy(struct v4l2sink *v4l2sink) {
|
||||||
|
sc_cond_destroy(&v4l2sink->queue_cond);
|
||||||
|
sc_mutex_destroy(&v4l2sink->mutex);
|
||||||
|
free(v4l2sink->devicename);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
v4l2sink_open(struct v4l2sink *v4l2sink, const AVCodec *codec) {
|
||||||
|
v4l2sink->decoder_ctx = avcodec_alloc_context3(codec);
|
||||||
|
if (!v4l2sink->decoder_ctx) {
|
||||||
|
LOGC("Could not allocate decoder context for v4l2sink");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avcodec_open2(v4l2sink->decoder_ctx, codec, NULL) < 0) {
|
||||||
|
LOGE("Could not open decoder codec for v4l2sink");
|
||||||
|
avcodec_free_context(&v4l2sink->decoder_ctx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AVCodec *encoder = avcodec_find_encoder(AV_CODEC_ID_RAWVIDEO);
|
||||||
|
if (!encoder) {
|
||||||
|
LOGE("RAWVIDEO encoder not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
v4l2sink->encoder_ctx = avcodec_alloc_context3(encoder);
|
||||||
|
if (!v4l2sink->encoder_ctx) {
|
||||||
|
LOGC("Could not allocate encoder context for v4l2sink");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
v4l2sink->encoder_ctx->width=v4l2sink->declared_frame_size.width;
|
||||||
|
v4l2sink->encoder_ctx->height=v4l2sink->declared_frame_size.height;
|
||||||
|
v4l2sink->encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
||||||
|
v4l2sink->encoder_ctx->time_base.num =1;
|
||||||
|
|
||||||
|
if (avcodec_open2(v4l2sink->encoder_ctx, encoder, NULL) < 0) {
|
||||||
|
LOGE("Could not open encoder codec for v4l2sink");
|
||||||
|
avcodec_free_context(&v4l2sink->encoder_ctx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AVOutputFormat *format = find_muxer("v4l2");
|
||||||
|
if (!format) {
|
||||||
|
LOGE("Could not find muxer for v4l2sink");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
v4l2sink->ctx = avformat_alloc_context();
|
||||||
|
if (!v4l2sink->ctx) {
|
||||||
|
LOGE("Could not allocate output context for v4l2sink");
|
||||||
|
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>
|
||||||
|
v4l2sink->ctx->oformat = (AVOutputFormat *) format;
|
||||||
|
|
||||||
|
av_dict_set(&v4l2sink->ctx->metadata, "comment",
|
||||||
|
"Recorded by scrcpy " SCRCPY_VERSION, 0);
|
||||||
|
|
||||||
|
AVStream *ostream = avformat_new_stream(v4l2sink->ctx, encoder);
|
||||||
|
if (!ostream) {
|
||||||
|
avformat_free_context(v4l2sink->ctx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
|
||||||
|
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||||
|
ostream->codecpar->codec_id = encoder->id;
|
||||||
|
ostream->codecpar->format = AV_PIX_FMT_YUV420P;
|
||||||
|
ostream->codecpar->width = v4l2sink->declared_frame_size.width;
|
||||||
|
ostream->codecpar->height = v4l2sink->declared_frame_size.height;
|
||||||
|
#else
|
||||||
|
ostream->codec->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||||
|
ostream->codec->codec_id = encoder->id;
|
||||||
|
ostream->codec->pix_fmt = AV_PIX_FMT_YUV420P;
|
||||||
|
ostream->codec->width = v4l2sink->declared_frame_size.width;
|
||||||
|
ostream->codec->height = v4l2sink->declared_frame_size.height;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int ret = avio_open(&v4l2sink->ctx->pb, v4l2sink->devicename,
|
||||||
|
AVIO_FLAG_WRITE);
|
||||||
|
if (ret < 0) {
|
||||||
|
LOGE("Failed to open output device: %s", v4l2sink->devicename);
|
||||||
|
// ostream will be cleaned up during context cleaning
|
||||||
|
avformat_free_context(v4l2sink->ctx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
v4l2sink->decoded_frame = av_frame_alloc();
|
||||||
|
v4l2sink->raw_packet = av_packet_alloc();
|
||||||
|
|
||||||
|
LOGI("V4l2sink started to device: %s", v4l2sink->devicename);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
v4l2sink_close(struct v4l2sink *v4l2sink) {
|
||||||
|
avcodec_close(v4l2sink->decoder_ctx);
|
||||||
|
avcodec_free_context(&v4l2sink->decoder_ctx);
|
||||||
|
|
||||||
|
avcodec_close(v4l2sink->encoder_ctx);
|
||||||
|
avcodec_free_context(&v4l2sink->encoder_ctx);
|
||||||
|
|
||||||
|
if (v4l2sink->header_written) {
|
||||||
|
int ret = av_write_trailer(v4l2sink->ctx);
|
||||||
|
if (ret < 0) {
|
||||||
|
LOGE("Failed to write trailer to %s", v4l2sink->devicename);
|
||||||
|
v4l2sink->failed = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
v4l2sink->failed = true;
|
||||||
|
}
|
||||||
|
avio_close(v4l2sink->ctx->pb);
|
||||||
|
avformat_free_context(v4l2sink->ctx);
|
||||||
|
|
||||||
|
if (v4l2sink->failed) {
|
||||||
|
LOGE("Sink failed to %s", v4l2sink->devicename);
|
||||||
|
} else {
|
||||||
|
LOGI("Sink completed device: %s", v4l2sink->devicename);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v4l2sink->decoded_frame) {
|
||||||
|
av_frame_free(&v4l2sink->decoded_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v4l2sink->raw_packet) {
|
||||||
|
av_packet_free(&v4l2sink->raw_packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
v4l2sink_write_header(struct v4l2sink *v4l2sink, const AVPacket *packet) {
|
||||||
|
AVStream *ostream = v4l2sink->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);
|
||||||
|
|
||||||
|
#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
|
||||||
|
ostream->codecpar->extradata = extradata;
|
||||||
|
ostream->codecpar->extradata_size = packet->size;
|
||||||
|
#else
|
||||||
|
ostream->codec->extradata = extradata;
|
||||||
|
ostream->codec->extradata_size = packet->size;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
v4l2sink->ctx->url = strdup(v4l2sink->devicename);
|
||||||
|
|
||||||
|
int ret = avformat_write_header(v4l2sink->ctx, NULL);
|
||||||
|
if (ret < 0) {
|
||||||
|
LOGE("Failed to write header to %s", v4l2sink->devicename);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
v4l2sink_rescale_packet(struct v4l2sink *v4l2sink, AVPacket *packet) {
|
||||||
|
AVStream *ostream = v4l2sink->ctx->streams[0];
|
||||||
|
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
v4l2sink_write(struct v4l2sink *v4l2sink, AVPacket *packet) {
|
||||||
|
if (!v4l2sink->header_written) {
|
||||||
|
bool ok = v4l2sink_write_header(v4l2sink, packet);
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
v4l2sink->header_written = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet->pts == AV_NOPTS_VALUE) {
|
||||||
|
// ignore config packets
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
v4l2sink_rescale_packet(v4l2sink, packet);
|
||||||
|
return av_write_frame(v4l2sink->ctx, packet) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
run_v4l2sink(void *data) {
|
||||||
|
struct v4l2sink *v4l2sink = data;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
sc_mutex_lock(&v4l2sink->mutex);
|
||||||
|
|
||||||
|
while (!v4l2sink->stopped && queue_is_empty(&v4l2sink->queue)) {
|
||||||
|
sc_cond_wait(&v4l2sink->queue_cond, &v4l2sink->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if stopped is set, continue to process the remaining events before actually stopping
|
||||||
|
|
||||||
|
if (v4l2sink->stopped && queue_is_empty(&v4l2sink->queue)) {
|
||||||
|
sc_mutex_unlock(&v4l2sink->mutex);
|
||||||
|
struct v4l2sink_packet *last = v4l2sink->previous;
|
||||||
|
if (last) {
|
||||||
|
// assign an arbitrary duration to the last packet
|
||||||
|
last->packet.duration = 100000;
|
||||||
|
bool ok = v4l2sink_write(v4l2sink, &last->packet);
|
||||||
|
if (!ok) {
|
||||||
|
// 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
|
||||||
|
LOGW("Could not send last packet to v4l2sink");
|
||||||
|
}
|
||||||
|
v4l2sink_packet_delete(last);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct v4l2sink_packet *rec;
|
||||||
|
queue_take(&v4l2sink->queue, next, &rec);
|
||||||
|
|
||||||
|
sc_mutex_unlock(&v4l2sink->mutex);
|
||||||
|
|
||||||
|
// v4l2sink->previous is only written from this thread, no need to lock
|
||||||
|
struct v4l2sink_packet *previous = v4l2sink->previous;
|
||||||
|
v4l2sink->previous = rec;
|
||||||
|
|
||||||
|
if (!previous) {
|
||||||
|
// we just received the first packet
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// config packets have no PTS, we must ignore them
|
||||||
|
if (rec->packet.pts != AV_NOPTS_VALUE
|
||||||
|
&& previous->packet.pts != AV_NOPTS_VALUE) {
|
||||||
|
// we now know the duration of the previous packet
|
||||||
|
previous->packet.duration = rec->packet.pts - previous->packet.pts;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ok = v4l2sink_write(v4l2sink, &previous->packet);
|
||||||
|
v4l2sink_packet_delete(previous);
|
||||||
|
if (!ok) {
|
||||||
|
LOGE("V4l2sink: Could not decode packet");
|
||||||
|
|
||||||
|
sc_mutex_lock(&v4l2sink->mutex);
|
||||||
|
v4l2sink->failed = true;
|
||||||
|
// discard pending packets
|
||||||
|
v4l2sink_queue_clear(&v4l2sink->queue);
|
||||||
|
sc_mutex_unlock(&v4l2sink->mutex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("V4l2sink thread ended");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
v4l2sink_start(struct v4l2sink *v4l2sink) {
|
||||||
|
LOGD("Starting v4l2sink thread");
|
||||||
|
|
||||||
|
bool ok = sc_thread_create(&v4l2sink->thread, run_v4l2sink, "v4l2sink",
|
||||||
|
v4l2sink);
|
||||||
|
if (!ok) {
|
||||||
|
LOGC("Could not start v4l2sink thread");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
v4l2sink_stop(struct v4l2sink *v4l2sink) {
|
||||||
|
sc_mutex_lock(&v4l2sink->mutex);
|
||||||
|
v4l2sink->stopped = true;
|
||||||
|
sc_cond_signal(&v4l2sink->queue_cond);
|
||||||
|
sc_mutex_unlock(&v4l2sink->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
v4l2sink_join(struct v4l2sink *v4l2sink) {
|
||||||
|
sc_thread_join(&v4l2sink->thread, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
v4l2sink_push(struct v4l2sink *v4l2sink, const AVPacket *packet) {
|
||||||
|
// the new decoding/encoding API has been introduced by:
|
||||||
|
// <http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=7fc329e2dd6226dfecaa4a1d7adf353bf2773726>
|
||||||
|
#ifdef SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API
|
||||||
|
int ret;
|
||||||
|
if ((ret = avcodec_send_packet(v4l2sink->decoder_ctx, packet)) < 0) {
|
||||||
|
LOGE("Could not send video packet x: %d", ret);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ret = avcodec_receive_frame(v4l2sink->decoder_ctx, v4l2sink->decoded_frame)) < 0) {
|
||||||
|
LOGE("Could not receive video frame: %d", ret);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ret = avcodec_send_frame(v4l2sink->encoder_ctx, v4l2sink->decoded_frame)) < 0) {
|
||||||
|
LOGE("Could not send video frame: %d", ret);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ret = avcodec_receive_packet(v4l2sink->encoder_ctx, v4l2sink->raw_packet)) < 0) {
|
||||||
|
LOGE("Could not receive video packet: %d", ret);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
int got_picture;
|
||||||
|
int len = avcodec_decode_video2(v4l2sink->decoder_ctx,
|
||||||
|
v4l2sink->decoded_frame,
|
||||||
|
&got_picture,
|
||||||
|
packet);
|
||||||
|
if (len < 0) {
|
||||||
|
LOGE("Could not decode video packet: %d", len);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!got_picture) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
len = avcodec_encode_video2(v4l2sink->encoder_ctx,
|
||||||
|
v4l2sink->raw_packet,
|
||||||
|
v4l2sink->decoded_frame,
|
||||||
|
&got_picture);
|
||||||
|
if (len < 0) {
|
||||||
|
LOGE("Could not encode video packet: %d", len);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!got_picture) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
sc_mutex_lock(&v4l2sink->mutex);
|
||||||
|
assert(!v4l2sink->stopped);
|
||||||
|
|
||||||
|
if (v4l2sink->failed) {
|
||||||
|
// reject any new packet (this will stop the stream)
|
||||||
|
sc_mutex_unlock(&v4l2sink->mutex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct v4l2sink_packet *rec = v4l2sink_packet_new(v4l2sink->raw_packet);
|
||||||
|
if (!rec) {
|
||||||
|
LOGC("Could not allocate v4l2sink packet");
|
||||||
|
sc_mutex_unlock(&v4l2sink->mutex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
queue_push(&v4l2sink->queue, next, rec);
|
||||||
|
sc_cond_signal(&v4l2sink->queue_cond);
|
||||||
|
|
||||||
|
sc_mutex_unlock(&v4l2sink->mutex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
80
app/src/v4l2sink.h
Normal file
80
app/src/v4l2sink.h
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
#ifndef V4L2SINK_H
|
||||||
|
#define V4L2SINK_H
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#ifdef V4L2SINK
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <libavformat/avformat.h>
|
||||||
|
|
||||||
|
#include "coords.h"
|
||||||
|
#include "scrcpy.h"
|
||||||
|
#include "util/queue.h"
|
||||||
|
#include "util/thread.h"
|
||||||
|
|
||||||
|
struct v4l2sink_packet {
|
||||||
|
AVPacket packet;
|
||||||
|
struct v4l2sink_packet *next;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct v4l2sink_queue QUEUE(struct v4l2sink_packet);
|
||||||
|
|
||||||
|
struct v4l2sink {
|
||||||
|
AVCodecContext *decoder_ctx;
|
||||||
|
AVCodecContext *encoder_ctx;
|
||||||
|
AVFrame *decoded_frame;
|
||||||
|
AVPacket *raw_packet;
|
||||||
|
|
||||||
|
char *devicename;
|
||||||
|
char *filename;
|
||||||
|
enum sc_record_format format;
|
||||||
|
AVFormatContext *ctx;
|
||||||
|
struct size declared_frame_size;
|
||||||
|
bool header_written;
|
||||||
|
|
||||||
|
sc_thread thread;
|
||||||
|
sc_mutex mutex;
|
||||||
|
sc_cond queue_cond;
|
||||||
|
bool stopped; // set on recorder_stop() by the stream reader
|
||||||
|
bool failed; // set on packet write failure
|
||||||
|
struct v4l2sink_queue queue;
|
||||||
|
|
||||||
|
// we can write a packet only once we received the next one so that we can
|
||||||
|
// set its duration (next_pts - current_pts)
|
||||||
|
// "previous" is only accessed from the recorder thread, so it does not
|
||||||
|
// need to be protected by the mutex
|
||||||
|
struct v4l2sink_packet *previous;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool
|
||||||
|
v4l2sink_init(struct v4l2sink *v4l2sink, const char *devicename, struct size declared_frame_size);
|
||||||
|
|
||||||
|
void
|
||||||
|
v4l2sink_destroy(struct v4l2sink *v4l2sink);
|
||||||
|
|
||||||
|
bool
|
||||||
|
v4l2sink_open(struct v4l2sink *v4l2sink, const AVCodec *input_codec);
|
||||||
|
|
||||||
|
void
|
||||||
|
v4l2sink_close(struct v4l2sink *v4l2sink);
|
||||||
|
|
||||||
|
bool
|
||||||
|
v4l2sink_start(struct v4l2sink *v4l2sink);
|
||||||
|
|
||||||
|
bool
|
||||||
|
v4l2sink_push(struct v4l2sink *v4l2sink, const AVPacket *packet);
|
||||||
|
|
||||||
|
void
|
||||||
|
v4l2sink_stop(struct v4l2sink *v4l2sink);
|
||||||
|
|
||||||
|
void
|
||||||
|
v4l2sink_join(struct v4l2sink *v4l2sink);
|
||||||
|
|
||||||
|
#else
|
||||||
|
struct v4l2sink { /* dummy struct for systems that don't support v4l2 */ };
|
||||||
|
#endif //V4L2SINK
|
||||||
|
|
||||||
|
#endif //V4L2SINK_H
|
Loading…
Add table
Add a link
Reference in a new issue