mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-08-02 22:29:25 +00:00
Adding --serve option :
- Add current working files
This commit is contained in:
parent
81a6b8e3a3
commit
b4dc78af0c
10 changed files with 574 additions and 223 deletions
|
@ -213,8 +213,8 @@ To disable mirroring while forwarding the stream:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --no-display --serve tcp:localhost:1234
|
scrcpy --no-display --serve tcp:localhost:1234
|
||||||
scrcpy -Nr --serve tcp:localhost:1234
|
scrcpy -N --serve tcp:localhost:1234
|
||||||
# interrupt recording with Ctrl+C
|
# interrupt serve with Ctrl+C
|
||||||
# Ctrl+C does not terminate properly on Windows, so disconnect the device
|
# Ctrl+C does not terminate properly on Windows, so disconnect the device
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
11
app/.project
Normal file
11
app/.project
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<projectDescription>
|
||||||
|
<name>app</name>
|
||||||
|
<comment></comment>
|
||||||
|
<projects>
|
||||||
|
</projects>
|
||||||
|
<buildSpec>
|
||||||
|
</buildSpec>
|
||||||
|
<natures>
|
||||||
|
</natures>
|
||||||
|
</projectDescription>
|
|
@ -15,6 +15,7 @@ src = [
|
||||||
'src/recorder.c',
|
'src/recorder.c',
|
||||||
'src/scrcpy.c',
|
'src/scrcpy.c',
|
||||||
'src/screen.c',
|
'src/screen.c',
|
||||||
|
'src/serve.c',
|
||||||
'src/server.c',
|
'src/server.c',
|
||||||
'src/stream.c',
|
'src/stream.c',
|
||||||
'src/tiny_xpm.c',
|
'src/tiny_xpm.c',
|
||||||
|
|
173
app/src/cli.c
173
app/src/cli.c
|
@ -6,9 +6,12 @@
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "recorder.h"
|
#include "recorder.h"
|
||||||
|
#include "serve.h"
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
#include "util/str_util.h"
|
#include "util/str_util.h"
|
||||||
|
|
||||||
|
#define IPV4_LOCALHOST 0x7F000001
|
||||||
|
|
||||||
void
|
void
|
||||||
scrcpy_print_usage(const char *arg0) {
|
scrcpy_print_usage(const char *arg0) {
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
|
@ -79,6 +82,11 @@ scrcpy_print_usage(const char *arg0) {
|
||||||
" The format is determined by the --record-format option if\n"
|
" The format is determined by the --record-format option if\n"
|
||||||
" set, or by the file extension (.mp4 or .mkv).\n"
|
" set, or by the file extension (.mp4 or .mkv).\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
" --serve tcp:localhost:1234\n"
|
||||||
|
" Open a socket to redirect video stream.\n"
|
||||||
|
" It will Wait for a client to connect before starting the mirroring,\n"
|
||||||
|
" then it would forward the video stream.\n"
|
||||||
|
"\n"
|
||||||
" --record-format format\n"
|
" --record-format format\n"
|
||||||
" Force recording format (either mp4 or mkv).\n"
|
" Force recording format (either mp4 or mkv).\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
@ -297,6 +305,7 @@ parse_port(const char *s, uint16_t *port) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
parse_record_format(const char *optarg, enum recorder_format *format) {
|
parse_record_format(const char *optarg, enum recorder_format *format) {
|
||||||
if (!strcmp(optarg, "mp4")) {
|
if (!strcmp(optarg, "mp4")) {
|
||||||
|
@ -311,6 +320,154 @@ parse_record_format(const char *optarg, enum recorder_format *format) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
char**
|
||||||
|
str_split(const char* a_str, const char a_delim)
|
||||||
|
{
|
||||||
|
char** result = 0;
|
||||||
|
size_t count = 0;
|
||||||
|
char* tmp = (char*)a_str;
|
||||||
|
char str[100];
|
||||||
|
strncpy(str, a_str, sizeof(str));
|
||||||
|
char* last_comma = 0;
|
||||||
|
char delim[2];
|
||||||
|
delim[0] = a_delim;
|
||||||
|
delim[1] = 0;
|
||||||
|
|
||||||
|
/* Count how many elements will be extracted. */
|
||||||
|
while (*tmp)
|
||||||
|
{
|
||||||
|
if (a_delim == *tmp)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
last_comma = tmp;
|
||||||
|
}
|
||||||
|
tmp++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add space for trailing token. */
|
||||||
|
count += last_comma < (str + strlen(str) - 1);
|
||||||
|
|
||||||
|
/* Add space for terminating null string so caller
|
||||||
|
knows where the list of returned strings ends. */
|
||||||
|
count++;
|
||||||
|
|
||||||
|
result = malloc(sizeof(char*) * count);
|
||||||
|
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
size_t idx = 0;
|
||||||
|
char* token = strtok(str, delim);
|
||||||
|
|
||||||
|
while (token)
|
||||||
|
{
|
||||||
|
assert(idx < count);
|
||||||
|
*(result + idx++) = strdup(token);
|
||||||
|
token = strtok(0, delim);
|
||||||
|
}
|
||||||
|
assert(idx == count - 1);
|
||||||
|
*(result + idx) = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
validate_ip(char* ip) { //check whether the IP is valid or not
|
||||||
|
int num, dots = 0;
|
||||||
|
char* ptr;
|
||||||
|
if (ip == NULL)
|
||||||
|
return 0;
|
||||||
|
ptr = strtok(ip, "."); //cut the string using dor delimiter
|
||||||
|
if (ptr == NULL)
|
||||||
|
return 0;
|
||||||
|
while (ptr) {
|
||||||
|
long value;
|
||||||
|
if (!parse_integer(ptr, &value)) //check whether the sub string is holding only number or not
|
||||||
|
return 0;
|
||||||
|
num = atoi(ptr); //convert substring to number
|
||||||
|
if (num >= 0 && num <= 255) {
|
||||||
|
ptr = strtok(NULL, "."); //cut the next part of the string
|
||||||
|
if (ptr != NULL)
|
||||||
|
dots++; //increase the dot count
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (dots != 3) //if the number of dots are not 3, return false
|
||||||
|
return 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
parse_serve_args(const char *optarg, char **s_protocol, uint32_t *s_ip, uint16_t *s_port) {
|
||||||
|
bool protocol_valid = false;
|
||||||
|
bool ip_valid = false;
|
||||||
|
bool port_valid = false;
|
||||||
|
|
||||||
|
char* protocol = NULL;
|
||||||
|
char* ip = NULL;
|
||||||
|
uint32_t ip_value;
|
||||||
|
char* port = NULL;
|
||||||
|
char** values;
|
||||||
|
|
||||||
|
values = str_split(optarg, ':');
|
||||||
|
|
||||||
|
if (values)
|
||||||
|
{
|
||||||
|
protocol = *values;
|
||||||
|
ip = *(values + 1);
|
||||||
|
port = *(values + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(values);
|
||||||
|
|
||||||
|
if (!strcmp(protocol, "tcp"))
|
||||||
|
{
|
||||||
|
//protocol = "tcp";
|
||||||
|
protocol_valid = true;
|
||||||
|
} else if (!strcmp(protocol, "udp"))
|
||||||
|
{
|
||||||
|
//protocol = "udp";
|
||||||
|
protocol_valid = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LOGE("Unexpected protocol: %s (expected tcp or udp)", protocol);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Allowing to write localhost or the IP address
|
||||||
|
if (!strcmp(ip, "localhost"))
|
||||||
|
{
|
||||||
|
ip_value = IPV4_LOCALHOST;
|
||||||
|
ip_valid = true;
|
||||||
|
} else if (validate_ip(ip)) {
|
||||||
|
ip_valid = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LOGE("Unexpected ip address (expected \"localhost\" or 255.255.255.255)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long port_value = 0;
|
||||||
|
port_valid = parse_integer_arg(port, &port_value, false, 0, 0xFFFF, "port");
|
||||||
|
|
||||||
|
//Check if everything is valid
|
||||||
|
if (!protocol_valid || !ip_valid || !port_valid) {
|
||||||
|
LOGE("Unexpected argument format: %s (expected [tcp/udp]:[ip or \"localhost\"]:[port])", optarg);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*LOGI("%s", protocol);
|
||||||
|
LOGI("%d", ip_value);
|
||||||
|
LOGI("%ld", port_value);*/
|
||||||
|
|
||||||
|
*s_protocol = protocol;
|
||||||
|
*s_ip = (uint32_t)ip_value;
|
||||||
|
*s_port = (uint16_t)port_value;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static enum recorder_format
|
static enum recorder_format
|
||||||
guess_record_format(const char *filename) {
|
guess_record_format(const char *filename) {
|
||||||
size_t len = strlen(filename);
|
size_t len = strlen(filename);
|
||||||
|
@ -340,6 +497,7 @@ guess_record_format(const char *filename) {
|
||||||
#define OPT_WINDOW_HEIGHT 1010
|
#define OPT_WINDOW_HEIGHT 1010
|
||||||
#define OPT_WINDOW_BORDERLESS 1011
|
#define OPT_WINDOW_BORDERLESS 1011
|
||||||
#define OPT_MAX_FPS 1012
|
#define OPT_MAX_FPS 1012
|
||||||
|
#define OPT_SERVE 1013
|
||||||
|
|
||||||
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[]) {
|
||||||
|
@ -360,6 +518,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||||
{"render-expired-frames", no_argument, NULL,
|
{"render-expired-frames", no_argument, NULL,
|
||||||
OPT_RENDER_EXPIRED_FRAMES},
|
OPT_RENDER_EXPIRED_FRAMES},
|
||||||
{"serial", required_argument, NULL, 's'},
|
{"serial", required_argument, NULL, 's'},
|
||||||
|
{"serve", required_argument, NULL, OPT_SERVE},
|
||||||
{"show-touches", no_argument, NULL, 't'},
|
{"show-touches", no_argument, NULL, 't'},
|
||||||
{"turn-screen-off", no_argument, NULL, 'S'},
|
{"turn-screen-off", no_argument, NULL, 'S'},
|
||||||
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
|
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
|
||||||
|
@ -434,6 +593,16 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||||
case 's':
|
case 's':
|
||||||
opts->serial = optarg;
|
opts->serial = optarg;
|
||||||
break;
|
break;
|
||||||
|
case OPT_SERVE:
|
||||||
|
if (!parse_serve_args(optarg, &opts->serve_protocol, &opts->serve_ip, &opts->serve_port)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
opts->serve = true;
|
||||||
|
}
|
||||||
|
/*LOGI("protocol is %s", opts->serve_protocol);
|
||||||
|
LOGI("ip value is %d", opts->serve_ip);
|
||||||
|
LOGI("port is %d", opts->serve_port);*/
|
||||||
|
break;
|
||||||
case 'S':
|
case 'S':
|
||||||
opts->turn_screen_off = true;
|
opts->turn_screen_off = true;
|
||||||
break;
|
break;
|
||||||
|
@ -490,8 +659,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!opts->display && !opts->record_filename) {
|
if (!opts->display && !opts->record_filename && !opts->serve) {
|
||||||
LOGE("-N/--no-display requires screen recording (-r/--record)");
|
LOGE("-N/--no-display requires screen recording (-r/--record) or to serve to another client (--serve)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
#include "recorder.h"
|
#include "recorder.h"
|
||||||
#include "screen.h"
|
#include "screen.h"
|
||||||
#include "server.h"
|
#include "server.h"
|
||||||
|
#include "serve.h"
|
||||||
#include "stream.h"
|
#include "stream.h"
|
||||||
#include "tiny_xpm.h"
|
#include "tiny_xpm.h"
|
||||||
#include "video_buffer.h"
|
#include "video_buffer.h"
|
||||||
|
@ -35,6 +36,7 @@ static struct video_buffer video_buffer;
|
||||||
static struct stream stream;
|
static struct stream stream;
|
||||||
static struct decoder decoder;
|
static struct decoder decoder;
|
||||||
static struct recorder recorder;
|
static struct recorder recorder;
|
||||||
|
static struct serve serve;
|
||||||
static struct controller controller;
|
static struct controller controller;
|
||||||
static struct file_handler file_handler;
|
static struct file_handler file_handler;
|
||||||
|
|
||||||
|
@ -307,6 +309,7 @@ scrcpy(const struct scrcpy_options *options) {
|
||||||
bool stream_started = false;
|
bool stream_started = false;
|
||||||
bool controller_initialized = false;
|
bool controller_initialized = false;
|
||||||
bool controller_started = false;
|
bool controller_started = false;
|
||||||
|
//bool serve_initialized = false;
|
||||||
|
|
||||||
if (!sdl_init_and_configure(options->display)) {
|
if (!sdl_init_and_configure(options->display)) {
|
||||||
goto end;
|
goto end;
|
||||||
|
@ -365,7 +368,18 @@ scrcpy(const struct scrcpy_options *options) {
|
||||||
|
|
||||||
av_log_set_callback(av_log_callback);
|
av_log_set_callback(av_log_callback);
|
||||||
|
|
||||||
stream_init(&stream, server.video_socket, dec, rec);
|
struct serve *serv = NULL;
|
||||||
|
if (options->serve)
|
||||||
|
{
|
||||||
|
serve_init(&serve, options->serve_protocol, options->serve_ip, options->serve_port);
|
||||||
|
|
||||||
|
if (!serve_start(&serve)) {
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
serv = &serve;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream_init(&stream, server.video_socket, dec, rec, serv);
|
||||||
|
|
||||||
// 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
|
||||||
|
|
|
@ -13,7 +13,7 @@ struct scrcpy_options {
|
||||||
const char *crop;
|
const char *crop;
|
||||||
const char *record_filename;
|
const char *record_filename;
|
||||||
const char *window_title;
|
const char *window_title;
|
||||||
const char *push_target;
|
const char *push_target;
|
||||||
enum recorder_format record_format;
|
enum recorder_format record_format;
|
||||||
uint16_t port;
|
uint16_t port;
|
||||||
uint16_t max_size;
|
uint16_t max_size;
|
||||||
|
@ -32,6 +32,10 @@ struct scrcpy_options {
|
||||||
bool render_expired_frames;
|
bool render_expired_frames;
|
||||||
bool prefer_text;
|
bool prefer_text;
|
||||||
bool window_borderless;
|
bool window_borderless;
|
||||||
|
char *serve_protocol;
|
||||||
|
uint32_t serve_ip;
|
||||||
|
uint16_t serve_port;
|
||||||
|
bool serve;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define SCRCPY_OPTIONS_DEFAULT { \
|
#define SCRCPY_OPTIONS_DEFAULT { \
|
||||||
|
@ -58,6 +62,10 @@ struct scrcpy_options {
|
||||||
.render_expired_frames = false, \
|
.render_expired_frames = false, \
|
||||||
.prefer_text = false, \
|
.prefer_text = false, \
|
||||||
.window_borderless = false, \
|
.window_borderless = false, \
|
||||||
|
.serve_protocol = NULL, \
|
||||||
|
.serve_ip = 0, \
|
||||||
|
.serve_port = 0, \
|
||||||
|
.serve = false, \
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
|
101
app/src/serve.c
Normal file
101
app/src/serve.c
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
#include "serve.h"
|
||||||
|
|
||||||
|
#include <SDL2/SDL_events.h>
|
||||||
|
#include <SDL2/SDL_mutex.h>
|
||||||
|
#include <SDL2/SDL_thread.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "events.h"
|
||||||
|
#include "util/log.h"
|
||||||
|
#include "util/net.h"
|
||||||
|
|
||||||
|
# define SOCKET_ERROR -1
|
||||||
|
|
||||||
|
void
|
||||||
|
serve_init(struct serve* serve, char *protocol, uint32_t ip, uint16_t port) {
|
||||||
|
serve->protocol = protocol;
|
||||||
|
serve->ip = ip;
|
||||||
|
serve->port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
//static int
|
||||||
|
//run_serve(void *data) {
|
||||||
|
// struct serve* serve = data;
|
||||||
|
//
|
||||||
|
// socket_t Listensocket;
|
||||||
|
// socket_t ClientSocket;
|
||||||
|
//
|
||||||
|
// Listensocket = net_listen(serve->ip, serve->port, 1);
|
||||||
|
// if (Listensocket == INVALID_SOCKET) {
|
||||||
|
// LOGI("Listen Error");
|
||||||
|
// net_close(Listensocket);
|
||||||
|
// return 0;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for (;;) {
|
||||||
|
// ClientSocket = net_accept(Listensocket);
|
||||||
|
// if (ClientSocket == INVALID_SOCKET) {
|
||||||
|
// LOGI("Client Error");
|
||||||
|
// net_close(Listensocket);
|
||||||
|
// return 0;
|
||||||
|
// }
|
||||||
|
// LOGI("Client found");
|
||||||
|
//
|
||||||
|
// net_close(Listensocket);
|
||||||
|
//
|
||||||
|
// serve->socket = ClientSocket;
|
||||||
|
//
|
||||||
|
// if (serve->stopped)
|
||||||
|
// {
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// LOGD("Serve thread ended");
|
||||||
|
// return 0;
|
||||||
|
//}
|
||||||
|
|
||||||
|
bool
|
||||||
|
serve_start(struct serve* serve) {
|
||||||
|
LOGD("Starting serve thread");
|
||||||
|
|
||||||
|
socket_t Listensocket;
|
||||||
|
socket_t ClientSocket;
|
||||||
|
|
||||||
|
Listensocket = net_listen(serve->ip, serve->port, 1);
|
||||||
|
if (Listensocket == INVALID_SOCKET) {
|
||||||
|
LOGI("Listen Error");
|
||||||
|
net_close(Listensocket);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientSocket = net_accept(Listensocket);
|
||||||
|
if (ClientSocket == INVALID_SOCKET) {
|
||||||
|
LOGI("Client Error");
|
||||||
|
net_close(Listensocket);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
LOGI("Client found");
|
||||||
|
|
||||||
|
net_close(Listensocket);
|
||||||
|
|
||||||
|
serve->socket = ClientSocket;
|
||||||
|
|
||||||
|
/*serve->thread = SDL_CreateThread(run_serve, "serve", serve);
|
||||||
|
if (!serve->thread) {
|
||||||
|
LOGC("Could not start stream thread");
|
||||||
|
return false;
|
||||||
|
}*/
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
serve_push(struct serve* serve, const AVPacket packet) {
|
||||||
|
if (net_send(serve->socket, packet.data, packet.size) == SOCKET_ERROR)
|
||||||
|
{
|
||||||
|
LOGI("Client lost");
|
||||||
|
net_close(serve->socket);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
28
app/src/serve.h
Normal file
28
app/src/serve.h
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#ifndef SERVE_H
|
||||||
|
#define SERVE_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <libavformat/avformat.h>
|
||||||
|
#include <SDL2/SDL_atomic.h>
|
||||||
|
#include <SDL2/SDL_thread.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "util/net.h"
|
||||||
|
|
||||||
|
struct serve {
|
||||||
|
socket_t socket;
|
||||||
|
char *protocol;
|
||||||
|
uint32_t ip;
|
||||||
|
uint16_t port;
|
||||||
|
};
|
||||||
|
|
||||||
|
void
|
||||||
|
serve_init(struct serve* serve, char* protocol, uint32_t ip, uint16_t port);
|
||||||
|
|
||||||
|
bool
|
||||||
|
serve_start(struct serve* serve);
|
||||||
|
|
||||||
|
bool
|
||||||
|
serve_push(struct serve* serve, const AVPacket packet);
|
||||||
|
#endif
|
450
app/src/stream.c
450
app/src/stream.c
|
@ -7,14 +7,18 @@
|
||||||
#include <SDL2/SDL_mutex.h>
|
#include <SDL2/SDL_mutex.h>
|
||||||
#include <SDL2/SDL_thread.h>
|
#include <SDL2/SDL_thread.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "compat.h"
|
#include "compat.h"
|
||||||
#include "decoder.h"
|
#include "decoder.h"
|
||||||
#include "events.h"
|
#include "events.h"
|
||||||
#include "recorder.h"
|
#include "recorder.h"
|
||||||
|
#include "serve.h"
|
||||||
|
#include "stream.h"
|
||||||
#include "util/buffer_util.h"
|
#include "util/buffer_util.h"
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
|
#include "util/net.h"
|
||||||
|
|
||||||
#define BUFSIZE 0x10000
|
#define BUFSIZE 0x10000
|
||||||
|
|
||||||
|
@ -22,281 +26,295 @@
|
||||||
#define NO_PTS UINT64_C(-1)
|
#define NO_PTS UINT64_C(-1)
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
stream_recv_packet(struct stream *stream, AVPacket *packet) {
|
stream_recv_packet(struct stream* stream, AVPacket* packet) {
|
||||||
// The video stream contains raw packets, without time information. When we
|
// The video stream contains raw packets, without time information. When we
|
||||||
// record, we retrieve the timestamps separately, from a "meta" header
|
// record, we retrieve the timestamps separately, from a "meta" header
|
||||||
// added by the server before each raw packet.
|
// added by the server before each raw packet.
|
||||||
//
|
//
|
||||||
// The "meta" header length is 12 bytes:
|
// The "meta" header length is 12 bytes:
|
||||||
// [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ...
|
// [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ...
|
||||||
// <-------------> <-----> <-----------------------------...
|
// <-------------> <-----> <-----------------------------...
|
||||||
// PTS packet raw packet
|
// PTS packet raw packet
|
||||||
// size
|
// size
|
||||||
//
|
//
|
||||||
// It is followed by <packet_size> bytes containing the packet/frame.
|
// It is followed by <packet_size> bytes containing the packet/frame.
|
||||||
|
|
||||||
uint8_t header[HEADER_SIZE];
|
uint8_t header[HEADER_SIZE];
|
||||||
ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE);
|
ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE);
|
||||||
if (r < HEADER_SIZE) {
|
if (r < HEADER_SIZE) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t pts = buffer_read64be(header);
|
uint64_t pts = buffer_read64be(header);
|
||||||
uint32_t len = buffer_read32be(&header[8]);
|
uint32_t len = buffer_read32be(&header[8]);
|
||||||
assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0);
|
assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0);
|
||||||
assert(len);
|
assert(len);
|
||||||
|
|
||||||
if (av_new_packet(packet, len)) {
|
if (av_new_packet(packet, len)) {
|
||||||
LOGE("Could not allocate packet");
|
LOGE("Could not allocate packet");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
r = net_recv_all(stream->socket, packet->data, len);
|
r = net_recv_all(stream->socket, packet->data, len);
|
||||||
if (r < 0 || ((uint32_t) r) < len) {
|
if (r < 0 || ((uint32_t)r) < len) {
|
||||||
av_packet_unref(packet);
|
av_packet_unref(packet);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
packet->pts = pts != NO_PTS ? (int64_t) pts : AV_NOPTS_VALUE;
|
packet->pts = pts != NO_PTS ? (int64_t)pts : AV_NOPTS_VALUE;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
notify_stopped(void) {
|
notify_stopped(void) {
|
||||||
SDL_Event stop_event;
|
SDL_Event stop_event;
|
||||||
stop_event.type = EVENT_STREAM_STOPPED;
|
stop_event.type = EVENT_STREAM_STOPPED;
|
||||||
SDL_PushEvent(&stop_event);
|
SDL_PushEvent(&stop_event);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
process_config_packet(struct stream *stream, AVPacket *packet) {
|
process_config_packet(struct stream* stream, AVPacket* packet) {
|
||||||
if (stream->recorder && !recorder_push(stream->recorder, packet)) {
|
if (stream->recorder && !recorder_push(stream->recorder, packet)) {
|
||||||
LOGE("Could not send config packet to recorder");
|
LOGE("Could not send config packet to recorder");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
process_frame(struct stream* stream, AVPacket* packet) {
|
||||||
|
if (stream->decoder && !decoder_push(stream->decoder, packet)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stream->recorder) {
|
||||||
|
packet->dts = packet->pts;
|
||||||
|
|
||||||
|
if (!recorder_push(stream->recorder, packet)) {
|
||||||
|
LOGE("Could not send packet to recorder");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stream->serve && !serve_push(stream->serve, *packet)) {
|
||||||
|
LOGE("Could not serve packet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
process_frame(struct stream *stream, AVPacket *packet) {
|
stream_parse(struct stream* stream, AVPacket* packet) {
|
||||||
if (stream->decoder && !decoder_push(stream->decoder, packet)) {
|
uint8_t* in_data = packet->data;
|
||||||
return false;
|
int in_len = packet->size;
|
||||||
}
|
uint8_t* out_data = NULL;
|
||||||
|
int out_len = 0;
|
||||||
|
int r = av_parser_parse2(stream->parser, stream->codec_ctx,
|
||||||
|
&out_data, &out_len, in_data, in_len,
|
||||||
|
AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1);
|
||||||
|
|
||||||
if (stream->recorder) {
|
// PARSER_FLAG_COMPLETE_FRAMES is set
|
||||||
packet->dts = packet->pts;
|
assert(r == in_len);
|
||||||
|
(void)r;
|
||||||
|
assert(out_len == in_len);
|
||||||
|
|
||||||
if (!recorder_push(stream->recorder, packet)) {
|
if (stream->parser->key_frame == 1) {
|
||||||
LOGE("Could not send packet to recorder");
|
packet->flags |= AV_PKT_FLAG_KEY;
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
bool ok = process_frame(stream, packet);
|
||||||
|
if (!ok) {
|
||||||
|
LOGE("Could not process frame");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
stream_parse(struct stream *stream, AVPacket *packet) {
|
stream_push_packet(struct stream* stream, AVPacket* packet) {
|
||||||
uint8_t *in_data = packet->data;
|
bool is_config = packet->pts == AV_NOPTS_VALUE;
|
||||||
int in_len = packet->size;
|
|
||||||
uint8_t *out_data = NULL;
|
|
||||||
int out_len = 0;
|
|
||||||
int r = av_parser_parse2(stream->parser, stream->codec_ctx,
|
|
||||||
&out_data, &out_len, in_data, in_len,
|
|
||||||
AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1);
|
|
||||||
|
|
||||||
// PARSER_FLAG_COMPLETE_FRAMES is set
|
// A config packet must not be decoded immetiately (it contains no
|
||||||
assert(r == in_len);
|
// frame); instead, it must be concatenated with the future data packet.
|
||||||
(void) r;
|
if (stream->has_pending || is_config) {
|
||||||
assert(out_len == in_len);
|
size_t offset;
|
||||||
|
if (stream->has_pending) {
|
||||||
|
offset = stream->pending.size;
|
||||||
|
if (av_grow_packet(&stream->pending, packet->size)) {
|
||||||
|
LOGE("Could not grow packet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
offset = 0;
|
||||||
|
if (av_new_packet(&stream->pending, packet->size)) {
|
||||||
|
LOGE("Could not create packet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
stream->has_pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (stream->parser->key_frame == 1) {
|
memcpy(stream->pending.data + offset, packet->data, packet->size);
|
||||||
packet->flags |= AV_PKT_FLAG_KEY;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ok = process_frame(stream, packet);
|
if (!is_config) {
|
||||||
if (!ok) {
|
// prepare the concat packet to send to the decoder
|
||||||
LOGE("Could not process frame");
|
stream->pending.pts = packet->pts;
|
||||||
return false;
|
stream->pending.dts = packet->dts;
|
||||||
}
|
stream->pending.flags = packet->flags;
|
||||||
|
packet = &stream->pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
if (is_config) {
|
||||||
}
|
// config packet
|
||||||
|
bool ok = process_config_packet(stream, packet);
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// data packet
|
||||||
|
bool ok = stream_parse(stream, packet);
|
||||||
|
|
||||||
static bool
|
if (stream->has_pending) {
|
||||||
stream_push_packet(struct stream *stream, AVPacket *packet) {
|
// the pending packet must be discarded (consumed or error)
|
||||||
bool is_config = packet->pts == AV_NOPTS_VALUE;
|
stream->has_pending = false;
|
||||||
|
av_packet_unref(&stream->pending);
|
||||||
|
}
|
||||||
|
|
||||||
// A config packet must not be decoded immetiately (it contains no
|
if (!ok) {
|
||||||
// frame); instead, it must be concatenated with the future data packet.
|
return false;
|
||||||
if (stream->has_pending || is_config) {
|
}
|
||||||
size_t offset;
|
}
|
||||||
if (stream->has_pending) {
|
return true;
|
||||||
offset = stream->pending.size;
|
|
||||||
if (av_grow_packet(&stream->pending, packet->size)) {
|
|
||||||
LOGE("Could not grow packet");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
offset = 0;
|
|
||||||
if (av_new_packet(&stream->pending, packet->size)) {
|
|
||||||
LOGE("Could not create packet");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
stream->has_pending = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
memcpy(stream->pending.data + offset, packet->data, packet->size);
|
|
||||||
|
|
||||||
if (!is_config) {
|
|
||||||
// prepare the concat packet to send to the decoder
|
|
||||||
stream->pending.pts = packet->pts;
|
|
||||||
stream->pending.dts = packet->dts;
|
|
||||||
stream->pending.flags = packet->flags;
|
|
||||||
packet = &stream->pending;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_config) {
|
|
||||||
// config packet
|
|
||||||
bool ok = process_config_packet(stream, packet);
|
|
||||||
if (!ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// data packet
|
|
||||||
bool ok = stream_parse(stream, packet);
|
|
||||||
|
|
||||||
if (stream->has_pending) {
|
|
||||||
// the pending packet must be discarded (consumed or error)
|
|
||||||
stream->has_pending = false;
|
|
||||||
av_packet_unref(&stream->pending);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
run_stream(void *data) {
|
run_stream(void* data) {
|
||||||
struct stream *stream = data;
|
struct stream* stream = data;
|
||||||
|
|
||||||
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
|
AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_H264);
|
||||||
if (!codec) {
|
if (!codec) {
|
||||||
LOGE("H.264 decoder not found");
|
LOGE("H.264 decoder not found");
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
stream->codec_ctx = avcodec_alloc_context3(codec);
|
stream->codec_ctx = avcodec_alloc_context3(codec);
|
||||||
if (!stream->codec_ctx) {
|
if (!stream->codec_ctx) {
|
||||||
LOGC("Could not allocate codec context");
|
LOGC("Could not allocate codec context");
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stream->decoder && !decoder_open(stream->decoder, codec)) {
|
if (stream->decoder && !decoder_open(stream->decoder, codec)) {
|
||||||
LOGE("Could not open decoder");
|
LOGE("Could not open decoder");
|
||||||
goto finally_free_codec_ctx;
|
goto finally_free_codec_ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stream->recorder) {
|
if (stream->recorder) {
|
||||||
if (!recorder_open(stream->recorder, codec)) {
|
if (!recorder_open(stream->recorder, codec)) {
|
||||||
LOGE("Could not open recorder");
|
LOGE("Could not open recorder");
|
||||||
goto finally_close_decoder;
|
goto finally_close_decoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!recorder_start(stream->recorder)) {
|
if (!recorder_start(stream->recorder)) {
|
||||||
LOGE("Could not start recorder");
|
LOGE("Could not start recorder");
|
||||||
goto finally_close_recorder;
|
goto finally_close_recorder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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");
|
||||||
goto finally_stop_and_join_recorder;
|
goto finally_stop_and_join_recorder;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We must only pass complete frames to av_parser_parse2()!
|
// We must only pass complete frames to av_parser_parse2()!
|
||||||
// It's more complicated, but this allows to reduce the latency by 1 frame!
|
// It's more complicated, but this allows to reduce the latency by 1 frame!
|
||||||
stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
|
stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
AVPacket packet;
|
AVPacket packet;
|
||||||
bool ok = stream_recv_packet(stream, &packet);
|
bool ok = stream_recv_packet(stream, &packet);
|
||||||
if (!ok) {
|
|
||||||
// end of stream
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = stream_push_packet(stream, &packet);
|
if (!ok) {
|
||||||
av_packet_unref(&packet);
|
// end of stream
|
||||||
if (!ok) {
|
break;
|
||||||
// cannot process packet (error already logged)
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGD("End of frames");
|
ok = stream_push_packet(stream, &packet);
|
||||||
|
|
||||||
if (stream->has_pending) {
|
av_packet_unref(&packet);
|
||||||
av_packet_unref(&stream->pending);
|
|
||||||
}
|
if (!ok) {
|
||||||
|
// cannot process packet (error already logged)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
av_parser_close(stream->parser);
|
LOGD("End of frames");
|
||||||
|
|
||||||
|
if (stream->has_pending) {
|
||||||
|
av_packet_unref(&stream->pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
av_parser_close(stream->parser);
|
||||||
finally_stop_and_join_recorder:
|
finally_stop_and_join_recorder:
|
||||||
if (stream->recorder) {
|
if (stream->recorder) {
|
||||||
recorder_stop(stream->recorder);
|
recorder_stop(stream->recorder);
|
||||||
LOGI("Finishing recording...");
|
LOGI("Finishing recording...");
|
||||||
recorder_join(stream->recorder);
|
recorder_join(stream->recorder);
|
||||||
}
|
}
|
||||||
finally_close_recorder:
|
finally_close_recorder:
|
||||||
if (stream->recorder) {
|
if (stream->recorder) {
|
||||||
recorder_close(stream->recorder);
|
recorder_close(stream->recorder);
|
||||||
}
|
}
|
||||||
finally_close_decoder:
|
finally_close_decoder:
|
||||||
if (stream->decoder) {
|
if (stream->decoder) {
|
||||||
decoder_close(stream->decoder);
|
decoder_close(stream->decoder);
|
||||||
}
|
}
|
||||||
finally_free_codec_ctx:
|
finally_free_codec_ctx:
|
||||||
avcodec_free_context(&stream->codec_ctx);
|
avcodec_free_context(&stream->codec_ctx);
|
||||||
end:
|
end:
|
||||||
notify_stopped();
|
notify_stopped();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 serve* serve) {
|
||||||
stream->socket = socket;
|
stream->socket = socket;
|
||||||
stream->decoder = decoder,
|
stream->decoder = decoder;
|
||||||
stream->recorder = recorder;
|
stream->recorder = recorder;
|
||||||
stream->has_pending = false;
|
stream->serve = serve;
|
||||||
|
stream->has_pending = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
stream_start(struct stream *stream) {
|
stream_start(struct stream* stream) {
|
||||||
LOGD("Starting stream thread");
|
LOGD("Starting stream thread");
|
||||||
|
|
||||||
stream->thread = SDL_CreateThread(run_stream, "stream", stream);
|
stream->thread = SDL_CreateThread(run_stream, "stream", stream);
|
||||||
if (!stream->thread) {
|
if (!stream->thread) {
|
||||||
LOGC("Could not start stream thread");
|
LOGC("Could not start stream thread");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
stream_stop(struct stream *stream) {
|
stream_stop(struct stream* stream) {
|
||||||
if (stream->decoder) {
|
if (stream->decoder) {
|
||||||
decoder_interrupt(stream->decoder);
|
decoder_interrupt(stream->decoder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
stream_join(struct stream *stream) {
|
stream_join(struct stream* stream) {
|
||||||
SDL_WaitThread(stream->thread, NULL);
|
SDL_WaitThread(stream->thread, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ struct stream {
|
||||||
SDL_Thread *thread;
|
SDL_Thread *thread;
|
||||||
struct decoder *decoder;
|
struct decoder *decoder;
|
||||||
struct recorder *recorder;
|
struct recorder *recorder;
|
||||||
|
struct serve *serve;
|
||||||
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 serve *serve);
|
||||||
|
|
||||||
bool
|
bool
|
||||||
stream_start(struct stream *stream);
|
stream_start(struct stream *stream);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue