Compare commits
104 Commits
linux_maco
...
audio.107
Author | SHA1 | Date | |
---|---|---|---|
1b7a600bdd | |||
fea9ad9bf9 | |||
6a43a68800 | |||
f42a320690 | |||
711f7b7693 | |||
03ea4b6918 | |||
7f0feea155 | |||
c101ec598f | |||
ff3ec5dc5a | |||
3e06105f59 | |||
89c638282f | |||
06f68a1570 | |||
fe7207da49 | |||
19c13651b3 | |||
a1f39b0227 | |||
d9b2488880 | |||
b5304dc9d2 | |||
b26af71bfb | |||
7de4b8bea7 | |||
0ebbe4b268 | |||
bf8c6f9050 | |||
8fd61c1d93 | |||
e318aa1cb1 | |||
f3998c280b | |||
f6ae6865ed | |||
f0f277ba71 | |||
35689a73ab | |||
84751937f6 | |||
9896cf0f9a | |||
3c18cfb23b | |||
e9fc35e9b1 | |||
ffe3b87f3c | |||
5d8f891153 | |||
c2b3985f80 | |||
d81804359e | |||
96385b531c | |||
954c774894 | |||
1bdf0f1594 | |||
d358139656 | |||
f816558e7a | |||
c0fe77d0b4 | |||
74e380c8e0 | |||
67d9396db1 | |||
9ae632ca2f | |||
092b683402 | |||
b1ccbbea55 | |||
bcd51211f2 | |||
de2b17873a | |||
38e317f3b7 | |||
5ba37b0522 | |||
b6b178f6cf | |||
4d83cc3ec6 | |||
26e7d495d4 | |||
0dfa43a6a1 | |||
484c3dedc0 | |||
dfb3347633 | |||
9a89e21527 | |||
fd4fffa436 | |||
c54a087e44 | |||
f3da281ce8 | |||
8fc3e20cd7 | |||
0026ea4cd9 | |||
fa5e40c6b8 | |||
42b12a0ee8 | |||
e04add64d8 | |||
2ee2660ef7 | |||
2be2376cf7 | |||
4fa52d7983 | |||
369a56fcd8 | |||
8d319cbd67 | |||
72aa2ebd03 | |||
792d2f2c66 | |||
bebff7989f | |||
4f09051835 | |||
740a57ec4f | |||
f48963d450 | |||
3c3e743726 | |||
c0b26a90cb | |||
050fe55b6f | |||
4182b29a91 | |||
6c29c2a3c7 | |||
3b4727761e | |||
0c3ae37d86 | |||
fe0dd73835 | |||
51091f8465 | |||
93cc1fcff0 | |||
0f818e5d87 | |||
113eb864da | |||
72811e7b1f | |||
51365ca8d4 | |||
db7f27deb1 | |||
54e5cf1e24 | |||
5d67a9e7f3 | |||
97890db385 | |||
1d12fc2409 | |||
f2b23fc977 | |||
1b88ae4db0 | |||
67b22517f4 | |||
0e44b3158b | |||
6bf861ec2c | |||
251ea6dfff | |||
976978abe6 | |||
fd463a0220 | |||
2731c60896 |
4
BUILD.md
4
BUILD.md
@ -15,7 +15,7 @@ First, you need to install the required packages:
|
|||||||
sudo apt install ffmpeg libsdl2-2.0-0 adb wget \
|
sudo apt install ffmpeg libsdl2-2.0-0 adb wget \
|
||||||
gcc git pkg-config meson ninja-build libsdl2-dev \
|
gcc git pkg-config meson ninja-build libsdl2-dev \
|
||||||
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
||||||
libusb-1.0-0 libusb-1.0-0-dev
|
libswresample-dev libusb-1.0-0 libusb-1.0-0-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Then clone the repo and execute the installation script
|
Then clone the repo and execute the installation script
|
||||||
@ -94,7 +94,7 @@ sudo apt install ffmpeg libsdl2-2.0-0 adb libusb-1.0-0
|
|||||||
# client build dependencies
|
# client build dependencies
|
||||||
sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \
|
sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \
|
||||||
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
||||||
libusb-1.0-0-dev
|
libswresample-dev libusb-1.0-0-dev
|
||||||
|
|
||||||
# server build dependencies
|
# server build dependencies
|
||||||
sudo apt install openjdk-11-jdk
|
sudo apt install openjdk-11-jdk
|
||||||
|
18
README.md
18
README.md
@ -199,7 +199,7 @@ preserved. That way, a device in 1920×1080 will be mirrored at 1024×576.
|
|||||||
The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
|
The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --bit-rate=2M
|
scrcpy --video-bit-rate=2M
|
||||||
scrcpy -b 2M # short version
|
scrcpy -b 2M # short version
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -258,9 +258,9 @@ The video codec can be selected. The possible values are `h264` (default),
|
|||||||
`h265` and `av1`:
|
`h265` and `av1`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --codec=h264 # default
|
scrcpy --video-codec=h264 # default
|
||||||
scrcpy --codec=h265
|
scrcpy --video-codec=h265
|
||||||
scrcpy --codec=av1
|
scrcpy --video-codec=av1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -270,15 +270,13 @@ Some devices have more than one encoder for a specific codec, and some of them
|
|||||||
may cause issues or crash. It is possible to select a different encoder:
|
may cause issues or crash. It is possible to select a different encoder:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --encoder=OMX.qcom.video.encoder.avc
|
scrcpy --video-encoder=OMX.qcom.video.encoder.avc
|
||||||
```
|
```
|
||||||
|
|
||||||
To list the available encoders, you can pass an invalid encoder name; the
|
To list the available encoders:
|
||||||
error will give the available encoders:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --encoder=_ # for the default codec
|
scrcpy --list-encoders
|
||||||
scrcpy --codec=h265 --encoder=_ # for a specific codec
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Capture
|
### Capture
|
||||||
@ -444,7 +442,7 @@ none found, try running `adb disconnect`, and then run those two commands again.
|
|||||||
It may be useful to decrease the bit-rate and the resolution:
|
It may be useful to decrease the bit-rate and the resolution:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --bit-rate=2M --max-size=800
|
scrcpy --video-bit-rate=2M --max-size=800
|
||||||
scrcpy -b2M -m800 # short version
|
scrcpy -b2M -m800 # short version
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -2,29 +2,33 @@ _scrcpy() {
|
|||||||
local cur prev words cword
|
local cur prev words cword
|
||||||
local opts="
|
local opts="
|
||||||
--always-on-top
|
--always-on-top
|
||||||
-b --bit-rate=
|
--audio-bit-rate=
|
||||||
--codec=
|
--audio-codec=
|
||||||
--codec-options=
|
--audio-codec-options=
|
||||||
|
--audio-encoder=
|
||||||
|
-b --video-bit-rate=
|
||||||
--crop=
|
--crop=
|
||||||
-d --select-usb
|
-d --select-usb
|
||||||
--disable-screensaver
|
--disable-screensaver
|
||||||
--display=
|
--display=
|
||||||
--display-buffer=
|
--display-buffer=
|
||||||
-e --select-tcpip
|
-e --select-tcpip
|
||||||
--encoder=
|
|
||||||
--force-adb-forward
|
--force-adb-forward
|
||||||
--forward-all-clicks
|
--forward-all-clicks
|
||||||
-f --fullscreen
|
-f --fullscreen
|
||||||
-K --hid-keyboard
|
-K --hid-keyboard
|
||||||
-h --help
|
-h --help
|
||||||
--legacy-paste
|
--legacy-paste
|
||||||
|
--list-displays
|
||||||
|
--list-encoders
|
||||||
--lock-video-orientation
|
--lock-video-orientation
|
||||||
--lock-video-orientation=
|
--lock-video-orientation=
|
||||||
--max-fps=
|
--max-fps=
|
||||||
-M --hid-mouse
|
-M --hid-mouse
|
||||||
-m --max-size=
|
-m --max-size=
|
||||||
|
--no-audio
|
||||||
--no-cleanup
|
--no-cleanup
|
||||||
--no-clipboard-on-error
|
--no-clipboard-autosync
|
||||||
--no-downsize-on-error
|
--no-downsize-on-error
|
||||||
-n --no-control
|
-n --no-control
|
||||||
-N --no-display
|
-N --no-display
|
||||||
@ -54,6 +58,9 @@ _scrcpy() {
|
|||||||
--v4l2-sink=
|
--v4l2-sink=
|
||||||
-V --verbosity=
|
-V --verbosity=
|
||||||
-v --version
|
-v --version
|
||||||
|
--video-codec=
|
||||||
|
--video-codec-options=
|
||||||
|
--video-encoder=
|
||||||
-w --stay-awake
|
-w --stay-awake
|
||||||
--window-borderless
|
--window-borderless
|
||||||
--window-title=
|
--window-title=
|
||||||
@ -65,10 +72,14 @@ _scrcpy() {
|
|||||||
_init_completion -s || return
|
_init_completion -s || return
|
||||||
|
|
||||||
case "$prev" in
|
case "$prev" in
|
||||||
--codec)
|
--video-codec)
|
||||||
COMPREPLY=($(compgen -W 'h264 h265 av1' -- "$cur"))
|
COMPREPLY=($(compgen -W 'h264 h265 av1' -- "$cur"))
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
|
--audio-codec)
|
||||||
|
COMPREPLY=($(compgen -W 'opus aac' -- "$cur"))
|
||||||
|
return
|
||||||
|
;;
|
||||||
--lock-video-orientation)
|
--lock-video-orientation)
|
||||||
COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur"))
|
COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur"))
|
||||||
return
|
return
|
||||||
@ -103,7 +114,7 @@ _scrcpy() {
|
|||||||
COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur}))
|
COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur}))
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
-b|--bit-rate \
|
-b|--video-bit-rate \
|
||||||
|--codec-options \
|
|--codec-options \
|
||||||
|--crop \
|
|--crop \
|
||||||
|--display \
|
|--display \
|
||||||
|
@ -9,26 +9,30 @@ local arguments
|
|||||||
|
|
||||||
arguments=(
|
arguments=(
|
||||||
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
|
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
|
||||||
{-b,--bit-rate=}'[Encode the video at the given bit-rate]'
|
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
|
||||||
'--codec=[Select the video codec]:codec:(h264 h265 av1)'
|
'--audio-codec=[Select the audio codec]:codec:(opus aac)'
|
||||||
'--codec-options=[Set a list of comma-separated key\:type=value options for the device encoder]'
|
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
|
||||||
|
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
|
||||||
|
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
|
||||||
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
|
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
|
||||||
{-d,--select-usb}'[Use USB device]'
|
{-d,--select-usb}'[Use USB device]'
|
||||||
'--disable-screensaver[Disable screensaver while scrcpy is running]'
|
'--disable-screensaver[Disable screensaver while scrcpy is running]'
|
||||||
'--display=[Specify the display id to mirror]'
|
'--display=[Specify the display id to mirror]'
|
||||||
'--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]'
|
'--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]'
|
||||||
{-e,--select-tcpip}'[Use TCP/IP device]'
|
{-e,--select-tcpip}'[Use TCP/IP device]'
|
||||||
'--encoder=[Use a specific MediaCodec encoder \(must be a H.264 encoder\)]'
|
|
||||||
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
|
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
|
||||||
'--forward-all-clicks[Forward clicks to device]'
|
'--forward-all-clicks[Forward clicks to device]'
|
||||||
{-f,--fullscreen}'[Start in fullscreen]'
|
{-f,--fullscreen}'[Start in fullscreen]'
|
||||||
{-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]'
|
{-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]'
|
||||||
{-h,--help}'[Print the help]'
|
{-h,--help}'[Print the help]'
|
||||||
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
|
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
|
||||||
|
'--list-displays[List displays available on the device]'
|
||||||
|
'--list-encoders[List video and audio encoders available on the device]'
|
||||||
'--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)'
|
'--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)'
|
||||||
'--max-fps=[Limit the frame rate of screen capture]'
|
'--max-fps=[Limit the frame rate of screen capture]'
|
||||||
{-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]'
|
{-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]'
|
||||||
{-m,--max-size=}'[Limit both the width and height of the video to value]'
|
{-m,--max-size=}'[Limit both the width and height of the video to value]'
|
||||||
|
'--no-audio[Disable audio forwarding]'
|
||||||
'--no-cleanup[Disable device cleanup actions on exit]'
|
'--no-cleanup[Disable device cleanup actions on exit]'
|
||||||
'--no-clipboard-autosync[Disable automatic clipboard synchronization]'
|
'--no-clipboard-autosync[Disable automatic clipboard synchronization]'
|
||||||
'--no-downsize-on-error[Disable lowering definition on MediaCodec error]'
|
'--no-downsize-on-error[Disable lowering definition on MediaCodec error]'
|
||||||
@ -59,6 +63,9 @@ arguments=(
|
|||||||
'--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]'
|
'--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]'
|
||||||
{-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)'
|
{-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)'
|
||||||
{-v,--version}'[Print the version of scrcpy]'
|
{-v,--version}'[Print the version of scrcpy]'
|
||||||
|
'--video-codec=[Select the video codec]:codec:(h264 h265 av1)'
|
||||||
|
'--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]'
|
||||||
|
'--video-encoder=[Use a specific MediaCodec video encoder]'
|
||||||
{-w,--stay-awake}'[Keep the device on while scrcpy is running, when the device is plugged in]'
|
{-w,--stay-awake}'[Keep the device on while scrcpy is running, when the device is plugged in]'
|
||||||
'--window-borderless[Disable window decorations \(display borderless window\)]'
|
'--window-borderless[Disable window decorations \(display borderless window\)]'
|
||||||
'--window-title=[Set a custom window title]'
|
'--window-title=[Set a custom window title]'
|
||||||
|
@ -4,12 +4,14 @@ src = [
|
|||||||
'src/adb/adb_device.c',
|
'src/adb/adb_device.c',
|
||||||
'src/adb/adb_parser.c',
|
'src/adb/adb_parser.c',
|
||||||
'src/adb/adb_tunnel.c',
|
'src/adb/adb_tunnel.c',
|
||||||
|
'src/audio_player.c',
|
||||||
'src/cli.c',
|
'src/cli.c',
|
||||||
'src/clock.c',
|
'src/clock.c',
|
||||||
'src/compat.c',
|
'src/compat.c',
|
||||||
'src/control_msg.c',
|
'src/control_msg.c',
|
||||||
'src/controller.c',
|
'src/controller.c',
|
||||||
'src/decoder.c',
|
'src/decoder.c',
|
||||||
|
'src/delay_buffer.c',
|
||||||
'src/demuxer.c',
|
'src/demuxer.c',
|
||||||
'src/device_msg.c',
|
'src/device_msg.c',
|
||||||
'src/icon.c',
|
'src/icon.c',
|
||||||
@ -28,12 +30,16 @@ src = [
|
|||||||
'src/screen.c',
|
'src/screen.c',
|
||||||
'src/server.c',
|
'src/server.c',
|
||||||
'src/version.c',
|
'src/version.c',
|
||||||
'src/video_buffer.c',
|
'src/trait/frame_source.c',
|
||||||
|
'src/trait/packet_source.c',
|
||||||
'src/util/acksync.c',
|
'src/util/acksync.c',
|
||||||
|
'src/util/average.c',
|
||||||
|
'src/util/bytebuf.c',
|
||||||
'src/util/file.c',
|
'src/util/file.c',
|
||||||
'src/util/intmap.c',
|
'src/util/intmap.c',
|
||||||
'src/util/intr.c',
|
'src/util/intr.c',
|
||||||
'src/util/log.c',
|
'src/util/log.c',
|
||||||
|
'src/util/memory.c',
|
||||||
'src/util/net.c',
|
'src/util/net.c',
|
||||||
'src/util/net_intr.c',
|
'src/util/net_intr.c',
|
||||||
'src/util/process.c',
|
'src/util/process.c',
|
||||||
@ -99,6 +105,7 @@ if not crossbuild_windows
|
|||||||
dependency('libavformat', version: '>= 57.33'),
|
dependency('libavformat', version: '>= 57.33'),
|
||||||
dependency('libavcodec', version: '>= 57.37'),
|
dependency('libavcodec', version: '>= 57.37'),
|
||||||
dependency('libavutil'),
|
dependency('libavutil'),
|
||||||
|
dependency('libswresample'),
|
||||||
dependency('sdl2', version: '>= 2.0.5'),
|
dependency('sdl2', version: '>= 2.0.5'),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -133,12 +140,14 @@ else
|
|||||||
ffmpeg_avcodec = meson.get_cross_property('ffmpeg_avcodec')
|
ffmpeg_avcodec = meson.get_cross_property('ffmpeg_avcodec')
|
||||||
ffmpeg_avformat = meson.get_cross_property('ffmpeg_avformat')
|
ffmpeg_avformat = meson.get_cross_property('ffmpeg_avformat')
|
||||||
ffmpeg_avutil = meson.get_cross_property('ffmpeg_avutil')
|
ffmpeg_avutil = meson.get_cross_property('ffmpeg_avutil')
|
||||||
|
ffmpeg_swresample = meson.get_cross_property('ffmpeg_swresample')
|
||||||
|
|
||||||
ffmpeg = declare_dependency(
|
ffmpeg = declare_dependency(
|
||||||
dependencies: [
|
dependencies: [
|
||||||
cc.find_library(ffmpeg_avcodec, dirs: ffmpeg_bin_dir),
|
cc.find_library(ffmpeg_avcodec, dirs: ffmpeg_bin_dir),
|
||||||
cc.find_library(ffmpeg_avformat, dirs: ffmpeg_bin_dir),
|
cc.find_library(ffmpeg_avformat, dirs: ffmpeg_bin_dir),
|
||||||
cc.find_library(ffmpeg_avutil, dirs: ffmpeg_bin_dir),
|
cc.find_library(ffmpeg_avutil, dirs: ffmpeg_bin_dir),
|
||||||
|
cc.find_library(ffmpeg_swresample, dirs: ffmpeg_bin_dir),
|
||||||
],
|
],
|
||||||
include_directories: include_directories(ffmpeg_include_dir)
|
include_directories: include_directories(ffmpeg_include_dir)
|
||||||
)
|
)
|
||||||
@ -174,6 +183,7 @@ check_functions = [
|
|||||||
'vasprintf',
|
'vasprintf',
|
||||||
'nrand48',
|
'nrand48',
|
||||||
'jrand48',
|
'jrand48',
|
||||||
|
'reallocarray',
|
||||||
]
|
]
|
||||||
|
|
||||||
foreach f : check_functions
|
foreach f : check_functions
|
||||||
@ -201,10 +211,6 @@ conf.set('PORTABLE', get_option('portable'))
|
|||||||
conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183')
|
conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183')
|
||||||
conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
|
conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
|
||||||
|
|
||||||
# the default video bitrate, in bits/second
|
|
||||||
# overridden by option --bit-rate
|
|
||||||
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
|
|
||||||
|
|
||||||
# run a server debugger and wait for a client to be attached
|
# run a server debugger and wait for a client to be attached
|
||||||
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
|
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
|
||||||
|
|
||||||
@ -264,8 +270,9 @@ if get_option('buildtype') == 'debug'
|
|||||||
['test_binary', [
|
['test_binary', [
|
||||||
'tests/test_binary.c',
|
'tests/test_binary.c',
|
||||||
]],
|
]],
|
||||||
['test_cbuf', [
|
['test_bytebuf', [
|
||||||
'tests/test_cbuf.c',
|
'tests/test_bytebuf.c',
|
||||||
|
'src/util/bytebuf.c',
|
||||||
]],
|
]],
|
||||||
['test_cli', [
|
['test_cli', [
|
||||||
'tests/test_cli.c',
|
'tests/test_cli.c',
|
||||||
@ -291,9 +298,6 @@ if get_option('buildtype') == 'debug'
|
|||||||
'tests/test_device_msg_deserialize.c',
|
'tests/test_device_msg_deserialize.c',
|
||||||
'src/device_msg.c',
|
'src/device_msg.c',
|
||||||
]],
|
]],
|
||||||
['test_queue', [
|
|
||||||
'tests/test_queue.c',
|
|
||||||
]],
|
|
||||||
['test_strbuf', [
|
['test_strbuf', [
|
||||||
'tests/test_strbuf.c',
|
'tests/test_strbuf.c',
|
||||||
'src/util/strbuf.c',
|
'src/util/strbuf.c',
|
||||||
@ -303,6 +307,10 @@ if get_option('buildtype') == 'debug'
|
|||||||
'src/util/str.c',
|
'src/util/str.c',
|
||||||
'src/util/strbuf.c',
|
'src/util/strbuf.c',
|
||||||
]],
|
]],
|
||||||
|
['test_vecdeque', [
|
||||||
|
'tests/test_vecdeque.c',
|
||||||
|
'src/util/memory.c',
|
||||||
|
]],
|
||||||
['test_vector', [
|
['test_vector', [
|
||||||
'tests/test_vector.c',
|
'tests/test_vector.c',
|
||||||
]],
|
]],
|
||||||
|
87
app/scrcpy.1
87
app/scrcpy.1
@ -20,26 +20,34 @@ provides display and control of Android devices connected on USB (or over TCP/IP
|
|||||||
Make scrcpy window always on top (above other windows).
|
Make scrcpy window always on top (above other windows).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-b, \-\-bit\-rate " value
|
.BI "\-\-audio\-bit\-rate " value
|
||||||
|
Encode the audio at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
||||||
|
|
||||||
|
Default is 196K (196000).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-audio\-buffer ms
|
||||||
|
Add a buffering delay (in milliseconds) before playing audio. This increases latency to compensate for jitter.
|
||||||
|
|
||||||
|
Default is 0 (no buffering).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-audio\-codec " name
|
||||||
|
Select an audio codec (opus or aac).
|
||||||
|
|
||||||
|
Default is opus.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-audio\-encoder " name
|
||||||
|
Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR).
|
||||||
|
|
||||||
|
The available encoders can be listed by \-\-list\-encoders.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-b, \-\-video\-bit\-rate " value
|
||||||
Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
||||||
|
|
||||||
Default is 8000000.
|
Default is 8M (8000000).
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-codec " name
|
|
||||||
Select a video codec (h264, h265 or av1).
|
|
||||||
|
|
||||||
Default is h264.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...]
|
|
||||||
Set a list of comma-separated key:type=value options for the device encoder.
|
|
||||||
|
|
||||||
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
|
|
||||||
|
|
||||||
The list of possible codec options is available in the Android documentation
|
|
||||||
.UR https://d.android.com/reference/android/media/MediaFormat
|
|
||||||
.UE .
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
||||||
@ -61,10 +69,9 @@ Disable screensaver while scrcpy is running.
|
|||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-display " id
|
.BI "\-\-display " id
|
||||||
Specify the display id to mirror.
|
Specify the device display id to mirror.
|
||||||
|
|
||||||
The list of possible display ids can be listed by "adb shell dumpsys display"
|
The available display ids can be listed by \-\-list\-displays.
|
||||||
(search "mDisplayId=" in the output).
|
|
||||||
|
|
||||||
Default is 0.
|
Default is 0.
|
||||||
|
|
||||||
@ -80,10 +87,6 @@ Use TCP/IP device (if there is exactly one, like adb -e).
|
|||||||
|
|
||||||
Also see \fB\-d\fR (\fB\-\-select\-usb\fR).
|
Also see \fB\-d\fR (\fB\-\-select\-usb\fR).
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-encoder " name
|
|
||||||
Use a specific MediaCodec encoder (must be a H.264 encoder).
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-\-force\-adb\-forward
|
.B \-\-force\-adb\-forward
|
||||||
Do not attempt to use "adb reverse" to connect to the device.
|
Do not attempt to use "adb reverse" to connect to the device.
|
||||||
@ -122,6 +125,14 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S
|
|||||||
|
|
||||||
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
|
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-\-list\-encoders
|
||||||
|
List video and audio encoders available on the device.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-\-list\-displays
|
||||||
|
List displays available on the device.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR]
|
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR]
|
||||||
Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise.
|
Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise.
|
||||||
@ -257,6 +268,10 @@ Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "me
|
|||||||
.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER
|
.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER
|
||||||
.UE
|
.UE
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-\-require\-audio
|
||||||
|
By default, scrcpy mirrors only the video if audio capture fails on the device. This flag makes scrcpy fail if audio is enabled but does not work.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-rotation " value
|
.BI "\-\-rotation " value
|
||||||
Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise.
|
Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise.
|
||||||
@ -329,6 +344,28 @@ Default is "info" for release builds, "debug" for debug builds.
|
|||||||
.B \-v, \-\-version
|
.B \-v, \-\-version
|
||||||
Print the version of scrcpy.
|
Print the version of scrcpy.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-video\-codec " name
|
||||||
|
Select a video codec (h264, h265 or av1).
|
||||||
|
|
||||||
|
Default is h264.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-video\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...]
|
||||||
|
Set a list of comma-separated key:type=value options for the device video encoder.
|
||||||
|
|
||||||
|
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
|
||||||
|
|
||||||
|
The list of possible codec options is available in the Android documentation
|
||||||
|
.UR https://d.android.com/reference/android/media/MediaFormat
|
||||||
|
.UE .
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-video\-encoder " name
|
||||||
|
Use a specific MediaCodec video encoder (depending on the codec provided by \fB\-\-video\-codec\fR).
|
||||||
|
|
||||||
|
The available encoders can be listed by \-\-list\-encoders.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-w, \-\-stay-awake
|
.B \-w, \-\-stay-awake
|
||||||
Keep the device on while scrcpy is running, when the device is plugged in.
|
Keep the device on while scrcpy is running, when the device is plugged in.
|
||||||
|
370
app/src/audio_player.c
Normal file
370
app/src/audio_player.c
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
#include "audio_player.h"
|
||||||
|
|
||||||
|
#include <libavutil/opt.h>
|
||||||
|
|
||||||
|
#include "util/log.h"
|
||||||
|
|
||||||
|
//#define SC_AUDIO_PLAYER_NDEBUG // comment to debug
|
||||||
|
|
||||||
|
/** Downcast frame_sink to sc_audio_player */
|
||||||
|
#define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink)
|
||||||
|
|
||||||
|
#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT
|
||||||
|
#define SC_SDL_SAMPLE_FMT AUDIO_F32
|
||||||
|
|
||||||
|
#define SC_AUDIO_OUTPUT_BUFFER_SAMPLES 480 // 10ms at 48000Hz
|
||||||
|
|
||||||
|
// The target number of buffered samples between the producer and the consumer.
|
||||||
|
// This value is directly use for compensation.
|
||||||
|
#define SC_TARGET_BUFFERED_SAMPLES (3 * SC_AUDIO_OUTPUT_BUFFER_SAMPLES)
|
||||||
|
|
||||||
|
// Use a ring-buffer of 1 second (at 48000Hz) between the producer and the
|
||||||
|
// consumer. It too big, but it guarantees that the producer and the consumer
|
||||||
|
// will be able to access it in parallel without locking.
|
||||||
|
#define SC_BYTEBUF_SIZE_IN_SAMPLES 48000
|
||||||
|
|
||||||
|
static inline size_t
|
||||||
|
bytes_to_samples(struct sc_audio_player *ap, size_t bytes) {
|
||||||
|
assert(bytes % (ap->nb_channels * ap->out_bytes_per_sample) == 0);
|
||||||
|
return bytes / (ap->nb_channels * ap->out_bytes_per_sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline size_t
|
||||||
|
samples_to_bytes(struct sc_audio_player *ap, size_t samples) {
|
||||||
|
return samples * ap->nb_channels * ap->out_bytes_per_sample;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
|
||||||
|
struct sc_audio_player *ap = userdata;
|
||||||
|
|
||||||
|
// This callback is called with the lock used by SDL_AudioDeviceLock(), so
|
||||||
|
// the bytebuf is protected
|
||||||
|
|
||||||
|
assert(len_int > 0);
|
||||||
|
size_t len = len_int;
|
||||||
|
|
||||||
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
|
LOGD("[Audio] SDL callback requests %" SC_PRIsizet " samples",
|
||||||
|
bytes_to_samples(ap, len));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
size_t read_avail = sc_bytebuf_read_available(&ap->buf);
|
||||||
|
size_t read = MIN(read_avail, len);
|
||||||
|
if (read) {
|
||||||
|
sc_bytebuf_read(&ap->buf, stream, read);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (read < len) {
|
||||||
|
// Insert silence
|
||||||
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
|
LOGD("[Audio] Buffer underflow, inserting silence: %" SC_PRIsizet
|
||||||
|
" samples", bytes_to_samples(ap, len - read));
|
||||||
|
#endif
|
||||||
|
memset(stream + read, 0, len - read);
|
||||||
|
ap->underflow += bytes_to_samples(ap, len - read);
|
||||||
|
}
|
||||||
|
|
||||||
|
ap->last_consumed = sc_tick_now();
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t *
|
||||||
|
sc_audio_player_get_swr_buf(struct sc_audio_player *ap, size_t min_samples) {
|
||||||
|
size_t min_buf_size = samples_to_bytes(ap, min_samples);
|
||||||
|
if (min_buf_size < ap->swr_buf_alloc_size) {
|
||||||
|
size_t new_size = min_buf_size + 4096;
|
||||||
|
uint8_t *buf = realloc(ap->swr_buf, new_size);
|
||||||
|
if (!buf) {
|
||||||
|
LOG_OOM();
|
||||||
|
// Could not realloc to the requested size
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
ap->swr_buf = buf;
|
||||||
|
ap->swr_buf_alloc_size = new_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ap->swr_buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||||
|
const AVCodecContext *ctx) {
|
||||||
|
struct sc_audio_player *ap = DOWNCAST(sink);
|
||||||
|
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
||||||
|
assert(ctx->ch_layout.nb_channels > 0);
|
||||||
|
unsigned nb_channels = ctx->ch_layout.nb_channels;
|
||||||
|
#else
|
||||||
|
int tmp = av_get_channel_layout_nb_channels(ctx->channel_layout);
|
||||||
|
assert(tmp > 0);
|
||||||
|
unsigned nb_channels = tmp;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SDL_AudioSpec desired = {
|
||||||
|
.freq = ctx->sample_rate,
|
||||||
|
.format = SC_SDL_SAMPLE_FMT,
|
||||||
|
.channels = nb_channels,
|
||||||
|
.samples = SC_AUDIO_OUTPUT_BUFFER_SAMPLES,
|
||||||
|
.callback = sc_audio_player_sdl_callback,
|
||||||
|
.userdata = ap,
|
||||||
|
};
|
||||||
|
SDL_AudioSpec obtained;
|
||||||
|
|
||||||
|
ap->device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0);
|
||||||
|
if (!ap->device) {
|
||||||
|
LOGE("Could not open audio device: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SwrContext *swr_ctx = swr_alloc();
|
||||||
|
if (!swr_ctx) {
|
||||||
|
LOG_OOM();
|
||||||
|
goto error_close_audio_device;
|
||||||
|
}
|
||||||
|
ap->swr_ctx = swr_ctx;
|
||||||
|
|
||||||
|
assert(ctx->sample_rate > 0);
|
||||||
|
assert(!av_sample_fmt_is_planar(SC_AV_SAMPLE_FMT));
|
||||||
|
|
||||||
|
int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT);
|
||||||
|
assert(out_bytes_per_sample > 0);
|
||||||
|
|
||||||
|
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
||||||
|
av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0);
|
||||||
|
av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0);
|
||||||
|
#else
|
||||||
|
av_opt_set_channel_layout(swr_ctx, "in_channel_layout",
|
||||||
|
ctx->channel_layout, 0);
|
||||||
|
av_opt_set_channel_layout(swr_ctx, "out_channel_layout",
|
||||||
|
ctx->channel_layout, 0);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0);
|
||||||
|
av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0);
|
||||||
|
|
||||||
|
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0);
|
||||||
|
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0);
|
||||||
|
|
||||||
|
int ret = swr_init(swr_ctx);
|
||||||
|
if (ret) {
|
||||||
|
LOGE("Failed to initialize the resampling context");
|
||||||
|
goto error_free_swr_ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
ap->sample_rate = ctx->sample_rate;
|
||||||
|
ap->nb_channels = nb_channels;
|
||||||
|
ap->out_bytes_per_sample = out_bytes_per_sample;
|
||||||
|
|
||||||
|
size_t bytebuf_size = samples_to_bytes(ap, SC_BYTEBUF_SIZE_IN_SAMPLES);
|
||||||
|
|
||||||
|
bool ok = sc_bytebuf_init(&ap->buf, bytebuf_size);
|
||||||
|
if (!ok) {
|
||||||
|
goto error_free_swr_ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t initial_swr_buf_size = samples_to_bytes(ap, 4096);
|
||||||
|
ap->swr_buf = malloc(initial_swr_buf_size);
|
||||||
|
if (!ap->swr_buf) {
|
||||||
|
LOG_OOM();
|
||||||
|
goto error_destroy_bytebuf;
|
||||||
|
}
|
||||||
|
ap->swr_buf_alloc_size = initial_swr_buf_size;
|
||||||
|
|
||||||
|
ap->previous_write_avail = sc_bytebuf_write_available(&ap->buf);
|
||||||
|
|
||||||
|
sc_average_init(&ap->avg_buffering, 8);
|
||||||
|
ap->samples_since_resync = 0;
|
||||||
|
|
||||||
|
ap->last_consumed = 0;
|
||||||
|
ap->underflow = 0;
|
||||||
|
|
||||||
|
SDL_PauseAudioDevice(ap->device, 0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
error_destroy_bytebuf:
|
||||||
|
sc_bytebuf_destroy(&ap->buf);
|
||||||
|
error_free_swr_ctx:
|
||||||
|
swr_free(&ap->swr_ctx);
|
||||||
|
error_close_audio_device:
|
||||||
|
SDL_CloseAudioDevice(ap->device);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) {
|
||||||
|
struct sc_audio_player *ap = DOWNCAST(sink);
|
||||||
|
|
||||||
|
assert(ap->device);
|
||||||
|
SDL_PauseAudioDevice(ap->device, 1);
|
||||||
|
SDL_CloseAudioDevice(ap->device);
|
||||||
|
|
||||||
|
free(ap->swr_buf);
|
||||||
|
sc_bytebuf_destroy(&ap->buf);
|
||||||
|
swr_free(&ap->swr_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
||||||
|
const AVFrame *frame) {
|
||||||
|
struct sc_audio_player *ap = DOWNCAST(sink);
|
||||||
|
|
||||||
|
SwrContext *swr_ctx = ap->swr_ctx;
|
||||||
|
|
||||||
|
int64_t delay = swr_get_delay(swr_ctx, ap->sample_rate);
|
||||||
|
// No need to av_rescale_rnd(), input and output sample rates are the same
|
||||||
|
// Add more space (256) for clock compensation
|
||||||
|
int dst_nb_samples = delay + frame->nb_samples + 256;
|
||||||
|
|
||||||
|
uint8_t *swr_buf = sc_audio_player_get_swr_buf(ap, dst_nb_samples);
|
||||||
|
if (!swr_buf) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples,
|
||||||
|
(const uint8_t **) frame->data, frame->nb_samples);
|
||||||
|
if (ret < 0) {
|
||||||
|
LOGE("Resampling failed: %d", ret);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// swr_convert() returns the number of samples which would have been
|
||||||
|
// written if the buffer was big enough.
|
||||||
|
size_t samples_written = MIN(ret, dst_nb_samples);
|
||||||
|
size_t swr_buf_size = samples_to_bytes(ap, samples_written);
|
||||||
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
|
LOGI("[Audio] %" SC_PRIsizet " samples written to buffer", samples_written);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Since this function is the only writer, the current available space is
|
||||||
|
// at least the previous available space. In practice, it should almost
|
||||||
|
// always be possible to write without lock.
|
||||||
|
bool lockless_write = swr_buf_size <= ap->previous_write_avail;
|
||||||
|
if (lockless_write) {
|
||||||
|
sc_bytebuf_prepare_write(&ap->buf, swr_buf, swr_buf_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LockAudioDevice(ap->device);
|
||||||
|
|
||||||
|
// The consumer requests audio samples blocks (e.g. 480 samples).
|
||||||
|
// Convert the duration since the last consumption into samples.
|
||||||
|
size_t extrapolated = 0;
|
||||||
|
if (ap->last_consumed) {
|
||||||
|
sc_tick now = sc_tick_now();
|
||||||
|
assert(now >= ap->last_consumed);
|
||||||
|
extrapolated = (sc_tick_now() - ap->last_consumed) * ap->sample_rate
|
||||||
|
/ SC_TICK_FREQ;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t read_avail = sc_bytebuf_read_available(&ap->buf);
|
||||||
|
|
||||||
|
// The consumer may not increase underflow value if there are still samples
|
||||||
|
// available
|
||||||
|
assert(read_avail == 0 || ap->underflow == 0);
|
||||||
|
|
||||||
|
size_t buffered_samples = bytes_to_samples(ap, read_avail);
|
||||||
|
// Underflow caused silence samples in excess (so it adds buffering).
|
||||||
|
// Extrapolated samples must be considered consumed for smoothing (so it
|
||||||
|
// removes buffering).
|
||||||
|
float buffering = (float) buffered_samples + ap->underflow - extrapolated;
|
||||||
|
sc_average_push(&ap->avg_buffering, buffering);
|
||||||
|
|
||||||
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
|
LOGD("[AUDIO] buffered_samples=%" SC_PRIsizet
|
||||||
|
" underflow=%" SC_PRIsizet
|
||||||
|
" extrapolated=%" SC_PRIsizet
|
||||||
|
" buffering=%f avg_buffering=%f",
|
||||||
|
buffered_samples, ap->underflow, extrapolated, buffering,
|
||||||
|
sc_average_get(&ap->avg_buffering));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (lockless_write) {
|
||||||
|
sc_bytebuf_commit_write(&ap->buf, swr_buf_size);
|
||||||
|
} else {
|
||||||
|
// Take care to keep full samples
|
||||||
|
size_t align = ap->nb_channels * ap->out_bytes_per_sample;
|
||||||
|
size_t write_avail =
|
||||||
|
sc_bytebuf_write_available(&ap->buf) / align * align;
|
||||||
|
if (swr_buf_size > write_avail) {
|
||||||
|
// Skip old samples
|
||||||
|
size_t cap = sc_bytebuf_capacity(&ap->buf) / align * align;
|
||||||
|
if (swr_buf_size > cap) {
|
||||||
|
// Ignore the first bytes in swr_buf
|
||||||
|
swr_buf += swr_buf_size - cap;
|
||||||
|
swr_buf_size = cap;
|
||||||
|
}
|
||||||
|
assert(swr_buf_size > write_avail);
|
||||||
|
if (swr_buf_size - write_avail > 0) {
|
||||||
|
sc_bytebuf_skip(&ap->buf, swr_buf_size - write_avail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sc_bytebuf_write(&ap->buf, swr_buf, swr_buf_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// On buffer underflow, typically because a packet is late, silence is
|
||||||
|
// inserted. In that case, the late samples must be ignored when they
|
||||||
|
// arrive, otherwise they will delay playback.
|
||||||
|
//
|
||||||
|
// As an improvement, instead of naively skipping the silence duration, we
|
||||||
|
// can absorb it if it helps clock compensation.
|
||||||
|
if (ap->underflow) {
|
||||||
|
size_t avg = sc_average_get(&ap->avg_buffering);
|
||||||
|
if (avg > SC_TARGET_BUFFERED_SAMPLES) {
|
||||||
|
size_t diff = SC_TARGET_BUFFERED_SAMPLES - avg;
|
||||||
|
if (diff < ap->underflow) {
|
||||||
|
// Partially absorb underflow for clock compensation (only keep
|
||||||
|
// the diff with the target buffering level).
|
||||||
|
ap->underflow = diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t skip_samples = MIN(ap->underflow, buffered_samples);
|
||||||
|
if (skip_samples) {
|
||||||
|
size_t skip_bytes = samples_to_bytes(ap, skip_samples);
|
||||||
|
sc_bytebuf_skip(&ap->buf, skip_bytes);
|
||||||
|
read_avail -= skip_bytes;
|
||||||
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
|
LOGD("[Audio] Skipping %" SC_PRIsizet " samples", skip_samples);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Totally absorb underflow for clock compensation
|
||||||
|
ap->underflow = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ap->previous_write_avail = sc_bytebuf_write_available(&ap->buf);
|
||||||
|
|
||||||
|
SDL_UnlockAudioDevice(ap->device);
|
||||||
|
|
||||||
|
ap->samples_since_resync += samples_written;
|
||||||
|
if (ap->samples_since_resync >= ap->sample_rate) {
|
||||||
|
// Resync every second
|
||||||
|
ap->samples_since_resync = 0;
|
||||||
|
|
||||||
|
float avg = sc_average_get(&ap->avg_buffering);
|
||||||
|
int diff = SC_TARGET_BUFFERED_SAMPLES - avg;
|
||||||
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
|
LOGI("[Audio] Average buffering=%f, compensation %d", avg, diff);
|
||||||
|
#endif
|
||||||
|
// Compensate the diff over 3 seconds (but will be recomputed after
|
||||||
|
// 1 second)
|
||||||
|
int ret = swr_set_compensation(swr_ctx, diff, 3 * ap->sample_rate);
|
||||||
|
if (ret < 0) {
|
||||||
|
LOGW("Resampling compensation failed: %d", ret);
|
||||||
|
// not fatal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_audio_player_init(struct sc_audio_player *ap) {
|
||||||
|
static const struct sc_frame_sink_ops ops = {
|
||||||
|
.open = sc_audio_player_frame_sink_open,
|
||||||
|
.close = sc_audio_player_frame_sink_close,
|
||||||
|
.push = sc_audio_player_frame_sink_push,
|
||||||
|
};
|
||||||
|
|
||||||
|
ap->frame_sink.ops = &ops;
|
||||||
|
}
|
60
app/src/audio_player.h
Normal file
60
app/src/audio_player.h
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#ifndef SC_AUDIO_PLAYER_H
|
||||||
|
#define SC_AUDIO_PLAYER_H
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include "trait/frame_sink.h"
|
||||||
|
#include <util/average.h>
|
||||||
|
#include <util/bytebuf.h>
|
||||||
|
#include <util/thread.h>
|
||||||
|
|
||||||
|
#include <libavformat/avformat.h>
|
||||||
|
#include <libswresample/swresample.h>
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
|
struct sc_audio_player {
|
||||||
|
struct sc_frame_sink frame_sink;
|
||||||
|
|
||||||
|
SDL_AudioDeviceID device;
|
||||||
|
|
||||||
|
// protected by SDL_AudioDeviceLock()
|
||||||
|
struct sc_bytebuf buf;
|
||||||
|
size_t previous_write_avail;
|
||||||
|
|
||||||
|
struct SwrContext *swr_ctx;
|
||||||
|
|
||||||
|
// The sample rate is the same for input and output
|
||||||
|
unsigned sample_rate;
|
||||||
|
// The number of channels is the same for input and output
|
||||||
|
unsigned nb_channels;
|
||||||
|
// The number of bytes per sample for a single channel
|
||||||
|
unsigned out_bytes_per_sample;
|
||||||
|
|
||||||
|
// Target buffer for resampling
|
||||||
|
uint8_t *swr_buf;
|
||||||
|
size_t swr_buf_alloc_size;
|
||||||
|
|
||||||
|
// Number of buffered samples (may be negative on underflow)
|
||||||
|
struct sc_average avg_buffering;
|
||||||
|
// Count the number of samples to trigger a compensation update regularly
|
||||||
|
size_t samples_since_resync;
|
||||||
|
|
||||||
|
// The last date a sample has been consumed by the audio output
|
||||||
|
sc_tick last_consumed;
|
||||||
|
|
||||||
|
// Number of silence samples inserted to be compensated
|
||||||
|
size_t underflow;
|
||||||
|
|
||||||
|
const struct sc_audio_player_callbacks *cbs;
|
||||||
|
void *cbs_userdata;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct sc_audio_player_callbacks {
|
||||||
|
void (*on_ended)(struct sc_audio_player *ap, bool success, void *userdata);
|
||||||
|
};
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_audio_player_init(struct sc_audio_player *ap);
|
||||||
|
|
||||||
|
#endif
|
211
app/src/cli.c
211
app/src/cli.c
@ -37,13 +37,15 @@ enum {
|
|||||||
OPT_RENDER_DRIVER,
|
OPT_RENDER_DRIVER,
|
||||||
OPT_NO_MIPMAPS,
|
OPT_NO_MIPMAPS,
|
||||||
OPT_CODEC_OPTIONS,
|
OPT_CODEC_OPTIONS,
|
||||||
|
OPT_VIDEO_CODEC_OPTIONS,
|
||||||
OPT_FORCE_ADB_FORWARD,
|
OPT_FORCE_ADB_FORWARD,
|
||||||
OPT_DISABLE_SCREENSAVER,
|
OPT_DISABLE_SCREENSAVER,
|
||||||
OPT_SHORTCUT_MOD,
|
OPT_SHORTCUT_MOD,
|
||||||
OPT_NO_KEY_REPEAT,
|
OPT_NO_KEY_REPEAT,
|
||||||
OPT_FORWARD_ALL_CLICKS,
|
OPT_FORWARD_ALL_CLICKS,
|
||||||
OPT_LEGACY_PASTE,
|
OPT_LEGACY_PASTE,
|
||||||
OPT_ENCODER_NAME,
|
OPT_ENCODER,
|
||||||
|
OPT_VIDEO_ENCODER,
|
||||||
OPT_POWER_OFF_ON_CLOSE,
|
OPT_POWER_OFF_ON_CLOSE,
|
||||||
OPT_V4L2_SINK,
|
OPT_V4L2_SINK,
|
||||||
OPT_DISPLAY_BUFFER,
|
OPT_DISPLAY_BUFFER,
|
||||||
@ -59,6 +61,16 @@ enum {
|
|||||||
OPT_PRINT_FPS,
|
OPT_PRINT_FPS,
|
||||||
OPT_NO_POWER_ON,
|
OPT_NO_POWER_ON,
|
||||||
OPT_CODEC,
|
OPT_CODEC,
|
||||||
|
OPT_VIDEO_CODEC,
|
||||||
|
OPT_NO_AUDIO,
|
||||||
|
OPT_AUDIO_BIT_RATE,
|
||||||
|
OPT_AUDIO_CODEC,
|
||||||
|
OPT_AUDIO_CODEC_OPTIONS,
|
||||||
|
OPT_AUDIO_ENCODER,
|
||||||
|
OPT_LIST_ENCODERS,
|
||||||
|
OPT_LIST_DISPLAYS,
|
||||||
|
OPT_REQUIRE_AUDIO,
|
||||||
|
OPT_AUDIO_BUFFER,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct sc_option {
|
struct sc_option {
|
||||||
@ -101,32 +113,71 @@ static const struct sc_option options[] = {
|
|||||||
.text = "Make scrcpy window always on top (above other windows).",
|
.text = "Make scrcpy window always on top (above other windows).",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.shortopt = 'b',
|
.longopt_id = OPT_AUDIO_BIT_RATE,
|
||||||
.longopt = "bit-rate",
|
.longopt = "audio-bit-rate",
|
||||||
.argdesc = "value",
|
.argdesc = "value",
|
||||||
.text = "Encode the video at the given bit-rate, expressed in bits/s. "
|
.text = "Encode the audio at the given bit-rate, expressed in bits/s. "
|
||||||
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||||
"Default is " STR(DEFAULT_BIT_RATE) ".",
|
"Default is 196K (196000).",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.longopt_id = OPT_CODEC,
|
.longopt_id = OPT_AUDIO_BUFFER,
|
||||||
.longopt = "codec",
|
.longopt = "audio-buffer",
|
||||||
|
.argdesc = "ms",
|
||||||
|
.text = "Add a buffering delay (in milliseconds) before playing audio. "
|
||||||
|
"This increases latency to compensate for jitter.\n"
|
||||||
|
"Default is 0 (no buffering).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.longopt_id = OPT_AUDIO_CODEC,
|
||||||
|
.longopt = "audio-codec",
|
||||||
.argdesc = "name",
|
.argdesc = "name",
|
||||||
.text = "Select a video codec (h264, h265 or av1).\n"
|
.text = "Select an audio codec (opus or aac).\n"
|
||||||
"Default is h264.",
|
"Default is opus.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.longopt_id = OPT_CODEC_OPTIONS,
|
.longopt_id = OPT_AUDIO_CODEC_OPTIONS,
|
||||||
.longopt = "codec-options",
|
.longopt = "audio-codec-options",
|
||||||
.argdesc = "key[:type]=value[,...]",
|
.argdesc = "key[:type]=value[,...]",
|
||||||
.text = "Set a list of comma-separated key:type=value options for the "
|
.text = "Set a list of comma-separated key:type=value options for the "
|
||||||
"device encoder.\n"
|
"device audio encoder.\n"
|
||||||
"The possible values for 'type' are 'int' (default), 'long', "
|
"The possible values for 'type' are 'int' (default), 'long', "
|
||||||
"'float' and 'string'.\n"
|
"'float' and 'string'.\n"
|
||||||
"The list of possible codec options is available in the "
|
"The list of possible codec options is available in the "
|
||||||
"Android documentation: "
|
"Android documentation: "
|
||||||
"<https://d.android.com/reference/android/media/MediaFormat>",
|
"<https://d.android.com/reference/android/media/MediaFormat>",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.longopt_id = OPT_AUDIO_ENCODER,
|
||||||
|
.longopt = "audio-encoder",
|
||||||
|
.argdesc = "name",
|
||||||
|
.text = "Use a specific MediaCodec audio encoder (depending on the "
|
||||||
|
"codec provided by --audio-codec).\n"
|
||||||
|
"The available encoders can be listed by --list-encoders.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.shortopt = 'b',
|
||||||
|
.longopt = "video-bit-rate",
|
||||||
|
.argdesc = "value",
|
||||||
|
.text = "Encode the video at the given bit-rate, expressed in bits/s. "
|
||||||
|
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||||
|
"Default is 8M (8000000).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Not really deprecated (--codec has never been released), but without
|
||||||
|
// declaring an explicit --codec option, getopt_long() partial matching
|
||||||
|
// behavior would consider --codec to be equivalent to --codec-options,
|
||||||
|
// which would be confusing.
|
||||||
|
.longopt_id = OPT_CODEC,
|
||||||
|
.longopt = "codec",
|
||||||
|
.argdesc = "value",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// deprecated
|
||||||
|
.longopt_id = OPT_CODEC_OPTIONS,
|
||||||
|
.longopt = "codec-options",
|
||||||
|
.argdesc = "key[:type]=value[,...]",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.longopt_id = OPT_CROP,
|
.longopt_id = OPT_CROP,
|
||||||
.longopt = "crop",
|
.longopt = "crop",
|
||||||
@ -151,10 +202,9 @@ static const struct sc_option options[] = {
|
|||||||
.longopt_id = OPT_DISPLAY_ID,
|
.longopt_id = OPT_DISPLAY_ID,
|
||||||
.longopt = "display",
|
.longopt = "display",
|
||||||
.argdesc = "id",
|
.argdesc = "id",
|
||||||
.text = "Specify the display id to mirror.\n"
|
.text = "Specify the device display id to mirror.\n"
|
||||||
"The list of possible display ids can be listed by:\n"
|
"The available display ids can be listed by:\n"
|
||||||
" adb shell dumpsys display\n"
|
" scrcpy --list-displays\n"
|
||||||
"(search \"mDisplayId=\" in the output)\n"
|
|
||||||
"Default is 0.",
|
"Default is 0.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -172,10 +222,10 @@ static const struct sc_option options[] = {
|
|||||||
"Also see -d (--select-usb).",
|
"Also see -d (--select-usb).",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.longopt_id = OPT_ENCODER_NAME,
|
// deprecated
|
||||||
|
.longopt_id = OPT_ENCODER,
|
||||||
.longopt = "encoder",
|
.longopt = "encoder",
|
||||||
.argdesc = "name",
|
.argdesc = "name",
|
||||||
.text = "Use a specific MediaCodec encoder (must be a H.264 encoder).",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.longopt_id = OPT_FORCE_ADB_FORWARD,
|
.longopt_id = OPT_FORCE_ADB_FORWARD,
|
||||||
@ -225,6 +275,16 @@ static const struct sc_option options[] = {
|
|||||||
"This is a workaround for some devices not behaving as "
|
"This is a workaround for some devices not behaving as "
|
||||||
"expected when setting the device clipboard programmatically.",
|
"expected when setting the device clipboard programmatically.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.longopt_id = OPT_LIST_DISPLAYS,
|
||||||
|
.longopt = "list-displays",
|
||||||
|
.text = "List device displays.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.longopt_id = OPT_LIST_ENCODERS,
|
||||||
|
.longopt = "list-encoders",
|
||||||
|
.text = "List video and audio encoders available on the device.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.longopt_id = OPT_LOCK_VIDEO_ORIENTATION,
|
.longopt_id = OPT_LOCK_VIDEO_ORIENTATION,
|
||||||
.longopt = "lock-video-orientation",
|
.longopt = "lock-video-orientation",
|
||||||
@ -266,6 +326,11 @@ static const struct sc_option options[] = {
|
|||||||
"is preserved.\n"
|
"is preserved.\n"
|
||||||
"Default is 0 (unlimited).",
|
"Default is 0 (unlimited).",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.longopt_id = OPT_NO_AUDIO,
|
||||||
|
.longopt = "no-audio",
|
||||||
|
.text = "Disable audio forwarding.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.longopt_id = OPT_NO_CLEANUP,
|
.longopt_id = OPT_NO_CLEANUP,
|
||||||
.longopt = "no-cleanup",
|
.longopt = "no-cleanup",
|
||||||
@ -403,6 +468,13 @@ static const struct sc_option options[] = {
|
|||||||
.longopt_id = OPT_RENDER_EXPIRED_FRAMES,
|
.longopt_id = OPT_RENDER_EXPIRED_FRAMES,
|
||||||
.longopt = "render-expired-frames",
|
.longopt = "render-expired-frames",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.longopt_id = OPT_REQUIRE_AUDIO,
|
||||||
|
.longopt = "require-audio",
|
||||||
|
.text = "By default, scrcpy mirrors only the video when audio capture "
|
||||||
|
"fails on the device. This flag makes scrcpy fail if audio is "
|
||||||
|
"enabled but does not work."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.longopt_id = OPT_ROTATION,
|
.longopt_id = OPT_ROTATION,
|
||||||
.longopt = "rotation",
|
.longopt = "rotation",
|
||||||
@ -512,6 +584,33 @@ static const struct sc_option options[] = {
|
|||||||
.longopt = "version",
|
.longopt = "version",
|
||||||
.text = "Print the version of scrcpy.",
|
.text = "Print the version of scrcpy.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.longopt_id = OPT_VIDEO_CODEC,
|
||||||
|
.longopt = "video-codec",
|
||||||
|
.argdesc = "name",
|
||||||
|
.text = "Select a video codec (h264, h265 or av1).\n"
|
||||||
|
"Default is h264.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.longopt_id = OPT_VIDEO_CODEC_OPTIONS,
|
||||||
|
.longopt = "video-codec-options",
|
||||||
|
.argdesc = "key[:type]=value[,...]",
|
||||||
|
.text = "Set a list of comma-separated key:type=value options for the "
|
||||||
|
"device video encoder.\n"
|
||||||
|
"The possible values for 'type' are 'int' (default), 'long', "
|
||||||
|
"'float' and 'string'.\n"
|
||||||
|
"The list of possible codec options is available in the "
|
||||||
|
"Android documentation: "
|
||||||
|
"<https://d.android.com/reference/android/media/MediaFormat>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.longopt_id = OPT_VIDEO_ENCODER,
|
||||||
|
.longopt = "video-encoder",
|
||||||
|
.argdesc = "name",
|
||||||
|
.text = "Use a specific MediaCodec video encoder (depending on the "
|
||||||
|
"codec provided by --video-codec).\n"
|
||||||
|
"The available encoders can be listed by --list-encoders.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.shortopt = 'w',
|
.shortopt = 'w',
|
||||||
.longopt = "stay-awake",
|
.longopt = "stay-awake",
|
||||||
@ -1388,7 +1487,7 @@ guess_record_format(const char *filename) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
parse_codec(const char *optarg, enum sc_codec *codec) {
|
parse_video_codec(const char *optarg, enum sc_codec *codec) {
|
||||||
if (!strcmp(optarg, "h264")) {
|
if (!strcmp(optarg, "h264")) {
|
||||||
*codec = SC_CODEC_H264;
|
*codec = SC_CODEC_H264;
|
||||||
return true;
|
return true;
|
||||||
@ -1401,7 +1500,21 @@ parse_codec(const char *optarg, enum sc_codec *codec) {
|
|||||||
*codec = SC_CODEC_AV1;
|
*codec = SC_CODEC_AV1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
LOGE("Unsupported codec: %s (expected h264, h265 or av1)", optarg);
|
LOGE("Unsupported video codec: %s (expected h264, h265 or av1)", optarg);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
parse_audio_codec(const char *optarg, enum sc_codec *codec) {
|
||||||
|
if (!strcmp(optarg, "opus")) {
|
||||||
|
*codec = SC_CODEC_OPUS;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!strcmp(optarg, "aac")) {
|
||||||
|
*codec = SC_CODEC_AAC;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
LOGE("Unsupported audio codec: %s (expected opus)", optarg);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1416,7 +1529,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) {
|
while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) {
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case 'b':
|
case 'b':
|
||||||
if (!parse_bit_rate(optarg, &opts->bit_rate)) {
|
if (!parse_bit_rate(optarg, &opts->video_bit_rate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case OPT_AUDIO_BIT_RATE:
|
||||||
|
if (!parse_bit_rate(optarg, &opts->audio_bit_rate)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -1589,10 +1707,23 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
opts->forward_key_repeat = false;
|
opts->forward_key_repeat = false;
|
||||||
break;
|
break;
|
||||||
case OPT_CODEC_OPTIONS:
|
case OPT_CODEC_OPTIONS:
|
||||||
opts->codec_options = optarg;
|
LOGW("--codec-options is deprecated, use --video-codec-options "
|
||||||
|
"instead.");
|
||||||
|
// fall through
|
||||||
|
case OPT_VIDEO_CODEC_OPTIONS:
|
||||||
|
opts->video_codec_options = optarg;
|
||||||
break;
|
break;
|
||||||
case OPT_ENCODER_NAME:
|
case OPT_AUDIO_CODEC_OPTIONS:
|
||||||
opts->encoder_name = optarg;
|
opts->audio_codec_options = optarg;
|
||||||
|
break;
|
||||||
|
case OPT_ENCODER:
|
||||||
|
LOGW("--encoder is deprecated, use --video-encoder instead.");
|
||||||
|
// fall through
|
||||||
|
case OPT_VIDEO_ENCODER:
|
||||||
|
opts->video_encoder = optarg;
|
||||||
|
break;
|
||||||
|
case OPT_AUDIO_ENCODER:
|
||||||
|
opts->audio_encoder = optarg;
|
||||||
break;
|
break;
|
||||||
case OPT_FORCE_ADB_FORWARD:
|
case OPT_FORCE_ADB_FORWARD:
|
||||||
opts->force_adb_forward = true;
|
opts->force_adb_forward = true;
|
||||||
@ -1629,6 +1760,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
case OPT_NO_DOWNSIZE_ON_ERROR:
|
case OPT_NO_DOWNSIZE_ON_ERROR:
|
||||||
opts->downsize_on_error = false;
|
opts->downsize_on_error = false;
|
||||||
break;
|
break;
|
||||||
|
case OPT_NO_AUDIO:
|
||||||
|
opts->audio = false;
|
||||||
|
break;
|
||||||
case OPT_NO_CLEANUP:
|
case OPT_NO_CLEANUP:
|
||||||
opts->cleanup = false;
|
opts->cleanup = false;
|
||||||
break;
|
break;
|
||||||
@ -1639,7 +1773,15 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
opts->start_fps_counter = true;
|
opts->start_fps_counter = true;
|
||||||
break;
|
break;
|
||||||
case OPT_CODEC:
|
case OPT_CODEC:
|
||||||
if (!parse_codec(optarg, &opts->codec)) {
|
LOGW("--codec is deprecated, use --video-codec instead.");
|
||||||
|
// fall through
|
||||||
|
case OPT_VIDEO_CODEC:
|
||||||
|
if (!parse_video_codec(optarg, &opts->video_codec)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case OPT_AUDIO_CODEC:
|
||||||
|
if (!parse_audio_codec(optarg, &opts->audio_codec)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -1670,6 +1812,20 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
LOGE("V4L2 (--v4l2-buffer) is only available on Linux.");
|
LOGE("V4L2 (--v4l2-buffer) is only available on Linux.");
|
||||||
return false;
|
return false;
|
||||||
#endif
|
#endif
|
||||||
|
case OPT_LIST_ENCODERS:
|
||||||
|
opts->list_encoders = true;
|
||||||
|
break;
|
||||||
|
case OPT_LIST_DISPLAYS:
|
||||||
|
opts->list_displays = true;
|
||||||
|
break;
|
||||||
|
case OPT_REQUIRE_AUDIO:
|
||||||
|
opts->require_audio = true;
|
||||||
|
break;
|
||||||
|
case OPT_AUDIO_BUFFER:
|
||||||
|
if (!parse_buffering_time(optarg, &opts->audio_buffer)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// getopt prints the error message on stderr
|
// getopt prints the error message on stderr
|
||||||
return false;
|
return false;
|
||||||
@ -1730,6 +1886,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if (opts->audio && !opts->display && !opts->record_filename) {
|
||||||
|
LOGI("No display and no recording: audio disabled");
|
||||||
|
opts->audio = false;
|
||||||
|
}
|
||||||
|
|
||||||
if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) {
|
if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) {
|
||||||
LOGI("Tunnel host/port is set, "
|
LOGI("Tunnel host/port is set, "
|
||||||
"--force-adb-forward automatically enabled.");
|
"--force-adb-forward automatically enabled.");
|
||||||
|
@ -18,7 +18,15 @@ sc_clock_init(struct sc_clock *clock) {
|
|||||||
static void
|
static void
|
||||||
sc_clock_estimate(struct sc_clock *clock,
|
sc_clock_estimate(struct sc_clock *clock,
|
||||||
double *out_slope, sc_tick *out_offset) {
|
double *out_slope, sc_tick *out_offset) {
|
||||||
assert(clock->count > 1); // two points are necessary
|
assert(clock->count);
|
||||||
|
|
||||||
|
if (clock->count == 1) {
|
||||||
|
// If there is only 1 point, we can't compute a slope. Assume it is 1.
|
||||||
|
struct sc_clock_point *single_point = &clock->right_sum;
|
||||||
|
*out_slope = 1;
|
||||||
|
*out_offset = single_point->system - single_point->stream;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
struct sc_clock_point left_avg = {
|
struct sc_clock_point left_avg = {
|
||||||
.system = clock->left_sum.system / (clock->count / 2),
|
.system = clock->left_sum.system / (clock->count / 2),
|
||||||
@ -93,19 +101,17 @@ sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
|
|||||||
|
|
||||||
clock->head = (clock->head + 1) % SC_CLOCK_RANGE;
|
clock->head = (clock->head + 1) % SC_CLOCK_RANGE;
|
||||||
|
|
||||||
if (clock->count > 1) {
|
// Update estimation
|
||||||
// Update estimation
|
sc_clock_estimate(clock, &clock->slope, &clock->offset);
|
||||||
sc_clock_estimate(clock, &clock->slope, &clock->offset);
|
|
||||||
|
|
||||||
#ifndef SC_CLOCK_NDEBUG
|
#ifndef SC_CLOCK_NDEBUG
|
||||||
LOGD("Clock estimation: %f * pts + %" PRItick,
|
LOGD("Clock estimation: %f * pts + %" PRItick,
|
||||||
clock->slope, clock->offset);
|
clock->slope, clock->offset);
|
||||||
#endif
|
#endif
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sc_tick
|
sc_tick
|
||||||
sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) {
|
sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) {
|
||||||
assert(clock->count > 1); // sc_clock_update() must have been called
|
assert(clock->count); // sc_clock_update() must have been called
|
||||||
return (sc_tick) (stream * clock->slope) + clock->offset;
|
return (sc_tick) (stream * clock->slope) + clock->offset;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
#ifndef HAVE_REALLOCARRAY
|
||||||
|
# include <errno.h>
|
||||||
|
#endif
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
@ -93,5 +96,15 @@ long jrand48(unsigned short xsubi[3]) {
|
|||||||
return v.i;
|
return v.i;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef HAVE_REALLOCARRAY
|
||||||
|
void *reallocarray(void *ptr, size_t nmemb, size_t size) {
|
||||||
|
size_t bytes;
|
||||||
|
if (__builtin_mul_overflow(nmemb, size, &bytes)) {
|
||||||
|
errno = ENOMEM;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return realloc(ptr, bytes);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
@ -37,6 +37,13 @@
|
|||||||
# define SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL
|
# define SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Not documented in ffmpeg/doc/APIchanges, but the channel_layout API
|
||||||
|
// has been replaced by chlayout in FFmpeg commit
|
||||||
|
// f423497b455da06c1337846902c770028760e094.
|
||||||
|
#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 23, 100)
|
||||||
|
# define SCRCPY_LAVU_HAS_CHLAYOUT
|
||||||
|
#endif
|
||||||
|
|
||||||
#if SDL_VERSION_ATLEAST(2, 0, 6)
|
#if SDL_VERSION_ATLEAST(2, 0, 6)
|
||||||
// <https://github.com/libsdl-org/SDL/commit/d7a318de563125e5bb465b1000d6bc9576fbc6fc>
|
// <https://github.com/libsdl-org/SDL/commit/d7a318de563125e5bb465b1000d6bc9576fbc6fc>
|
||||||
# define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS
|
# define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS
|
||||||
@ -67,4 +74,8 @@ long nrand48(unsigned short xsubi[3]);
|
|||||||
long jrand48(unsigned short xsubi[3]);
|
long jrand48(unsigned short xsubi[3]);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef HAVE_REALLOCARRAY
|
||||||
|
void *reallocarray(void *ptr, size_t nmemb, size_t size);
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -4,19 +4,28 @@
|
|||||||
|
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
|
#define SC_CONTROL_MSG_QUEUE_MAX 64
|
||||||
|
|
||||||
bool
|
bool
|
||||||
sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
|
sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
|
||||||
struct sc_acksync *acksync) {
|
struct sc_acksync *acksync) {
|
||||||
cbuf_init(&controller->queue);
|
sc_vecdeque_init(&controller->queue);
|
||||||
|
|
||||||
bool ok = sc_receiver_init(&controller->receiver, control_socket, acksync);
|
bool ok = sc_vecdeque_reserve(&controller->queue, SC_CONTROL_MSG_QUEUE_MAX);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ok = sc_receiver_init(&controller->receiver, control_socket, acksync);
|
||||||
|
if (!ok) {
|
||||||
|
sc_vecdeque_destroy(&controller->queue);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
ok = sc_mutex_init(&controller->mutex);
|
ok = sc_mutex_init(&controller->mutex);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
sc_receiver_destroy(&controller->receiver);
|
sc_receiver_destroy(&controller->receiver);
|
||||||
|
sc_vecdeque_destroy(&controller->queue);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,6 +33,7 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
|
|||||||
if (!ok) {
|
if (!ok) {
|
||||||
sc_receiver_destroy(&controller->receiver);
|
sc_receiver_destroy(&controller->receiver);
|
||||||
sc_mutex_destroy(&controller->mutex);
|
sc_mutex_destroy(&controller->mutex);
|
||||||
|
sc_vecdeque_destroy(&controller->queue);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,10 +48,12 @@ sc_controller_destroy(struct sc_controller *controller) {
|
|||||||
sc_cond_destroy(&controller->msg_cond);
|
sc_cond_destroy(&controller->msg_cond);
|
||||||
sc_mutex_destroy(&controller->mutex);
|
sc_mutex_destroy(&controller->mutex);
|
||||||
|
|
||||||
struct sc_control_msg msg;
|
while (!sc_vecdeque_is_empty(&controller->queue)) {
|
||||||
while (cbuf_take(&controller->queue, &msg)) {
|
struct sc_control_msg *msg = sc_vecdeque_popref(&controller->queue);
|
||||||
sc_control_msg_destroy(&msg);
|
assert(msg);
|
||||||
|
sc_control_msg_destroy(msg);
|
||||||
}
|
}
|
||||||
|
sc_vecdeque_destroy(&controller->queue);
|
||||||
|
|
||||||
sc_receiver_destroy(&controller->receiver);
|
sc_receiver_destroy(&controller->receiver);
|
||||||
}
|
}
|
||||||
@ -54,13 +66,19 @@ sc_controller_push_msg(struct sc_controller *controller,
|
|||||||
}
|
}
|
||||||
|
|
||||||
sc_mutex_lock(&controller->mutex);
|
sc_mutex_lock(&controller->mutex);
|
||||||
bool was_empty = cbuf_is_empty(&controller->queue);
|
bool full = sc_vecdeque_is_full(&controller->queue);
|
||||||
bool res = cbuf_push(&controller->queue, *msg);
|
if (!full) {
|
||||||
if (was_empty) {
|
bool was_empty = sc_vecdeque_is_empty(&controller->queue);
|
||||||
sc_cond_signal(&controller->msg_cond);
|
sc_vecdeque_push_noresize(&controller->queue, *msg);
|
||||||
|
if (was_empty) {
|
||||||
|
sc_cond_signal(&controller->msg_cond);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Otherwise (if the queue is full), the msg is discarded
|
||||||
|
|
||||||
sc_mutex_unlock(&controller->mutex);
|
sc_mutex_unlock(&controller->mutex);
|
||||||
return res;
|
|
||||||
|
return !full;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
@ -82,7 +100,8 @@ run_controller(void *data) {
|
|||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
sc_mutex_lock(&controller->mutex);
|
sc_mutex_lock(&controller->mutex);
|
||||||
while (!controller->stopped && cbuf_is_empty(&controller->queue)) {
|
while (!controller->stopped
|
||||||
|
&& sc_vecdeque_is_empty(&controller->queue)) {
|
||||||
sc_cond_wait(&controller->msg_cond, &controller->mutex);
|
sc_cond_wait(&controller->msg_cond, &controller->mutex);
|
||||||
}
|
}
|
||||||
if (controller->stopped) {
|
if (controller->stopped) {
|
||||||
@ -90,10 +109,9 @@ run_controller(void *data) {
|
|||||||
sc_mutex_unlock(&controller->mutex);
|
sc_mutex_unlock(&controller->mutex);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
struct sc_control_msg msg;
|
|
||||||
bool non_empty = cbuf_take(&controller->queue, &msg);
|
assert(!sc_vecdeque_is_empty(&controller->queue));
|
||||||
assert(non_empty);
|
struct sc_control_msg msg = sc_vecdeque_pop(&controller->queue);
|
||||||
(void) non_empty;
|
|
||||||
sc_mutex_unlock(&controller->mutex);
|
sc_mutex_unlock(&controller->mutex);
|
||||||
|
|
||||||
bool ok = process_msg(controller, &msg);
|
bool ok = process_msg(controller, &msg);
|
||||||
|
@ -8,11 +8,11 @@
|
|||||||
#include "control_msg.h"
|
#include "control_msg.h"
|
||||||
#include "receiver.h"
|
#include "receiver.h"
|
||||||
#include "util/acksync.h"
|
#include "util/acksync.h"
|
||||||
#include "util/cbuf.h"
|
|
||||||
#include "util/net.h"
|
#include "util/net.h"
|
||||||
#include "util/thread.h"
|
#include "util/thread.h"
|
||||||
|
#include "util/vecdeque.h"
|
||||||
|
|
||||||
struct sc_control_msg_queue CBUF(struct sc_control_msg, 64);
|
struct sc_control_msg_queue SC_VECDEQUE(struct sc_control_msg);
|
||||||
|
|
||||||
struct sc_controller {
|
struct sc_controller {
|
||||||
sc_socket control_socket;
|
sc_socket control_socket;
|
||||||
|
@ -2,41 +2,15 @@
|
|||||||
|
|
||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
|
#include <libavutil/channel_layout.h>
|
||||||
|
|
||||||
#include "events.h"
|
#include "events.h"
|
||||||
#include "video_buffer.h"
|
|
||||||
#include "trait/frame_sink.h"
|
#include "trait/frame_sink.h"
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
/** Downcast packet_sink to decoder */
|
/** Downcast packet_sink to decoder */
|
||||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_decoder, packet_sink)
|
#define DOWNCAST(SINK) container_of(SINK, struct sc_decoder, packet_sink)
|
||||||
|
|
||||||
static void
|
|
||||||
sc_decoder_close_first_sinks(struct sc_decoder *decoder, unsigned count) {
|
|
||||||
while (count) {
|
|
||||||
struct sc_frame_sink *sink = decoder->sinks[--count];
|
|
||||||
sink->ops->close(sink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
sc_decoder_close_sinks(struct sc_decoder *decoder) {
|
|
||||||
sc_decoder_close_first_sinks(decoder, decoder->sink_count);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_decoder_open_sinks(struct sc_decoder *decoder) {
|
|
||||||
for (unsigned i = 0; i < decoder->sink_count; ++i) {
|
|
||||||
struct sc_frame_sink *sink = decoder->sinks[i];
|
|
||||||
if (!sink->ops->open(sink)) {
|
|
||||||
sc_decoder_close_first_sinks(decoder, i);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) {
|
sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) {
|
||||||
decoder->codec_ctx = avcodec_alloc_context3(codec);
|
decoder->codec_ctx = avcodec_alloc_context3(codec);
|
||||||
@ -47,8 +21,23 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) {
|
|||||||
|
|
||||||
decoder->codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
decoder->codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
||||||
|
|
||||||
|
if (codec->type == AVMEDIA_TYPE_VIDEO) {
|
||||||
|
// Hardcoded video properties
|
||||||
|
decoder->codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
||||||
|
} else {
|
||||||
|
// Hardcoded audio properties
|
||||||
|
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
||||||
|
decoder->codec_ctx->ch_layout =
|
||||||
|
(AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO;
|
||||||
|
#else
|
||||||
|
decoder->codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
|
||||||
|
decoder->codec_ctx->channels = 2;
|
||||||
|
#endif
|
||||||
|
decoder->codec_ctx->sample_rate = 48000;
|
||||||
|
}
|
||||||
|
|
||||||
if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) {
|
if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) {
|
||||||
LOGE("Could not open codec");
|
LOGE("Decoder '%s': could not open codec", decoder->name);
|
||||||
avcodec_free_context(&decoder->codec_ctx);
|
avcodec_free_context(&decoder->codec_ctx);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -61,7 +50,8 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sc_decoder_open_sinks(decoder)) {
|
if (!sc_frame_source_sinks_open(&decoder->frame_source,
|
||||||
|
decoder->codec_ctx)) {
|
||||||
av_frame_free(&decoder->frame);
|
av_frame_free(&decoder->frame);
|
||||||
avcodec_close(decoder->codec_ctx);
|
avcodec_close(decoder->codec_ctx);
|
||||||
avcodec_free_context(&decoder->codec_ctx);
|
avcodec_free_context(&decoder->codec_ctx);
|
||||||
@ -73,24 +63,12 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) {
|
|||||||
|
|
||||||
static void
|
static void
|
||||||
sc_decoder_close(struct sc_decoder *decoder) {
|
sc_decoder_close(struct sc_decoder *decoder) {
|
||||||
sc_decoder_close_sinks(decoder);
|
sc_frame_source_sinks_close(&decoder->frame_source);
|
||||||
av_frame_free(&decoder->frame);
|
av_frame_free(&decoder->frame);
|
||||||
avcodec_close(decoder->codec_ctx);
|
avcodec_close(decoder->codec_ctx);
|
||||||
avcodec_free_context(&decoder->codec_ctx);
|
avcodec_free_context(&decoder->codec_ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
|
||||||
push_frame_to_sinks(struct sc_decoder *decoder, const AVFrame *frame) {
|
|
||||||
for (unsigned i = 0; i < decoder->sink_count; ++i) {
|
|
||||||
struct sc_frame_sink *sink = decoder->sinks[i];
|
|
||||||
if (!sink->ops->push(sink, frame)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
|
sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
|
||||||
bool is_config = packet->pts == AV_NOPTS_VALUE;
|
bool is_config = packet->pts == AV_NOPTS_VALUE;
|
||||||
@ -101,22 +79,33 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
|
|||||||
|
|
||||||
int ret = avcodec_send_packet(decoder->codec_ctx, packet);
|
int ret = avcodec_send_packet(decoder->codec_ctx, packet);
|
||||||
if (ret < 0 && ret != AVERROR(EAGAIN)) {
|
if (ret < 0 && ret != AVERROR(EAGAIN)) {
|
||||||
LOGE("Could not send video packet: %d", ret);
|
LOGE("Decoder '%s': could not send video packet: %d",
|
||||||
|
decoder->name, ret);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame);
|
|
||||||
if (!ret) {
|
|
||||||
// a frame was received
|
|
||||||
bool ok = push_frame_to_sinks(decoder, decoder->frame);
|
|
||||||
// A frame lost should not make the whole pipeline fail. The error, if
|
|
||||||
// any, is already logged.
|
|
||||||
(void) ok;
|
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame);
|
||||||
|
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret) {
|
||||||
|
LOGE("Decoder '%s', could not receive video frame: %d",
|
||||||
|
decoder->name, ret);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a frame was received
|
||||||
|
bool ok = sc_frame_source_sinks_push(&decoder->frame_source,
|
||||||
|
decoder->frame);
|
||||||
av_frame_unref(decoder->frame);
|
av_frame_unref(decoder->frame);
|
||||||
} else if (ret != AVERROR(EAGAIN)) {
|
if (!ok) {
|
||||||
LOGE("Could not receive video frame: %d", ret);
|
// Error already logged
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,8 +129,9 @@ sc_decoder_packet_sink_push(struct sc_packet_sink *sink,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_decoder_init(struct sc_decoder *decoder) {
|
sc_decoder_init(struct sc_decoder *decoder, const char *name) {
|
||||||
decoder->sink_count = 0;
|
decoder->name = name; // statically allocated
|
||||||
|
sc_frame_source_init(&decoder->frame_source);
|
||||||
|
|
||||||
static const struct sc_packet_sink_ops ops = {
|
static const struct sc_packet_sink_ops ops = {
|
||||||
.open = sc_decoder_packet_sink_open,
|
.open = sc_decoder_packet_sink_open,
|
||||||
@ -151,11 +141,3 @@ sc_decoder_init(struct sc_decoder *decoder) {
|
|||||||
|
|
||||||
decoder->packet_sink.ops = &ops;
|
decoder->packet_sink.ops = &ops;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
sc_decoder_add_sink(struct sc_decoder *decoder, struct sc_frame_sink *sink) {
|
|
||||||
assert(decoder->sink_count < SC_DECODER_MAX_SINKS);
|
|
||||||
assert(sink);
|
|
||||||
assert(sink->ops);
|
|
||||||
decoder->sinks[decoder->sink_count++] = sink;
|
|
||||||
}
|
|
||||||
|
@ -3,28 +3,25 @@
|
|||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
|
#include "trait/frame_source.h"
|
||||||
#include "trait/packet_sink.h"
|
#include "trait/packet_sink.h"
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
|
|
||||||
#define SC_DECODER_MAX_SINKS 2
|
|
||||||
|
|
||||||
struct sc_decoder {
|
struct sc_decoder {
|
||||||
struct sc_packet_sink packet_sink; // packet sink trait
|
struct sc_packet_sink packet_sink; // packet sink trait
|
||||||
|
struct sc_frame_source frame_source; // frame source trait
|
||||||
|
|
||||||
struct sc_frame_sink *sinks[SC_DECODER_MAX_SINKS];
|
const char *name; // must be statically allocated (e.g. a string literal)
|
||||||
unsigned sink_count;
|
|
||||||
|
|
||||||
AVCodecContext *codec_ctx;
|
AVCodecContext *codec_ctx;
|
||||||
AVFrame *frame;
|
AVFrame *frame;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The name must be statically allocated (e.g. a string literal)
|
||||||
void
|
void
|
||||||
sc_decoder_init(struct sc_decoder *decoder);
|
sc_decoder_init(struct sc_decoder *decoder, const char *name);
|
||||||
|
|
||||||
void
|
|
||||||
sc_decoder_add_sink(struct sc_decoder *decoder, struct sc_frame_sink *sink);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
244
app/src/delay_buffer.c
Normal file
244
app/src/delay_buffer.c
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
#include "delay_buffer.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include <libavutil/avutil.h>
|
||||||
|
#include <libavformat/avformat.h>
|
||||||
|
|
||||||
|
#include "util/log.h"
|
||||||
|
|
||||||
|
#define SC_BUFFERING_NDEBUG // comment to debug
|
||||||
|
|
||||||
|
/** Downcast frame_sink to sc_delay_buffer */
|
||||||
|
#define DOWNCAST(SINK) container_of(SINK, struct sc_delay_buffer, frame_sink)
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sc_delayed_frame_init(struct sc_delayed_frame *dframe, const AVFrame *frame) {
|
||||||
|
dframe->frame = av_frame_alloc();
|
||||||
|
if (!dframe->frame) {
|
||||||
|
LOG_OOM();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (av_frame_ref(dframe->frame, frame)) {
|
||||||
|
LOG_OOM();
|
||||||
|
av_frame_free(&dframe->frame);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_delayed_frame_destroy(struct sc_delayed_frame *dframe) {
|
||||||
|
av_frame_unref(dframe->frame);
|
||||||
|
av_frame_free(&dframe->frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
run_buffering(void *data) {
|
||||||
|
struct sc_delay_buffer *db = data;
|
||||||
|
|
||||||
|
assert(db->delay > 0);
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
sc_mutex_lock(&db->mutex);
|
||||||
|
|
||||||
|
while (!db->stopped && sc_vecdeque_is_empty(&db->queue)) {
|
||||||
|
sc_cond_wait(&db->queue_cond, &db->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db->stopped) {
|
||||||
|
sc_mutex_unlock(&db->mutex);
|
||||||
|
goto stopped;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sc_delayed_frame dframe = sc_vecdeque_pop(&db->queue);
|
||||||
|
|
||||||
|
sc_tick max_deadline = sc_tick_now() + db->delay;
|
||||||
|
// PTS (written by the server) are expressed in microseconds
|
||||||
|
sc_tick pts = SC_TICK_TO_US(dframe.frame->pts);
|
||||||
|
|
||||||
|
bool timed_out = false;
|
||||||
|
while (!db->stopped && !timed_out) {
|
||||||
|
sc_tick deadline = sc_clock_to_system_time(&db->clock, pts)
|
||||||
|
+ db->delay;
|
||||||
|
if (deadline > max_deadline) {
|
||||||
|
deadline = max_deadline;
|
||||||
|
}
|
||||||
|
|
||||||
|
timed_out =
|
||||||
|
!sc_cond_timedwait(&db->wait_cond, &db->mutex, deadline);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool stopped = db->stopped;
|
||||||
|
sc_mutex_unlock(&db->mutex);
|
||||||
|
|
||||||
|
if (stopped) {
|
||||||
|
sc_delayed_frame_destroy(&dframe);
|
||||||
|
goto stopped;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef SC_BUFFERING_NDEBUG
|
||||||
|
LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick,
|
||||||
|
pts, dframe.push_date, sc_tick_now());
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool ok = sc_frame_source_sinks_push(&db->frame_source, dframe.frame);
|
||||||
|
sc_delayed_frame_destroy(&dframe);
|
||||||
|
if (!ok) {
|
||||||
|
LOGE("Delayed frame could not be pushed, stopping");
|
||||||
|
sc_mutex_lock(&db->mutex);
|
||||||
|
// Prevent to push any new frame
|
||||||
|
db->stopped = true;
|
||||||
|
sc_mutex_unlock(&db->mutex);
|
||||||
|
goto stopped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopped:
|
||||||
|
assert(db->stopped);
|
||||||
|
|
||||||
|
// Flush queue
|
||||||
|
while (!sc_vecdeque_is_empty(&db->queue)) {
|
||||||
|
struct sc_delayed_frame *dframe = sc_vecdeque_popref(&db->queue);
|
||||||
|
sc_delayed_frame_destroy(dframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("Buffering thread ended");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink,
|
||||||
|
const AVCodecContext *ctx) {
|
||||||
|
struct sc_delay_buffer *db = DOWNCAST(sink);
|
||||||
|
(void) ctx;
|
||||||
|
|
||||||
|
bool ok = sc_mutex_init(&db->mutex);
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = sc_cond_init(&db->queue_cond);
|
||||||
|
if (!ok) {
|
||||||
|
goto error_destroy_mutex;
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = sc_cond_init(&db->wait_cond);
|
||||||
|
if (!ok) {
|
||||||
|
goto error_destroy_queue_cond;
|
||||||
|
}
|
||||||
|
|
||||||
|
sc_clock_init(&db->clock);
|
||||||
|
sc_vecdeque_init(&db->queue);
|
||||||
|
|
||||||
|
if (!sc_frame_source_sinks_open(&db->frame_source, ctx)) {
|
||||||
|
goto error_destroy_wait_cond;
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = sc_thread_create(&db->thread, run_buffering, "scrcpy-dbuf", db);
|
||||||
|
if (!ok) {
|
||||||
|
LOGE("Could not start buffering thread");
|
||||||
|
goto error_close_sinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
error_close_sinks:
|
||||||
|
sc_frame_source_sinks_close(&db->frame_source);
|
||||||
|
error_destroy_wait_cond:
|
||||||
|
sc_cond_destroy(&db->wait_cond);
|
||||||
|
error_destroy_queue_cond:
|
||||||
|
sc_cond_destroy(&db->queue_cond);
|
||||||
|
error_destroy_mutex:
|
||||||
|
sc_mutex_destroy(&db->mutex);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_delay_buffer_frame_sink_close(struct sc_frame_sink *sink) {
|
||||||
|
struct sc_delay_buffer *db = DOWNCAST(sink);
|
||||||
|
|
||||||
|
sc_mutex_lock(&db->mutex);
|
||||||
|
db->stopped = true;
|
||||||
|
sc_cond_signal(&db->queue_cond);
|
||||||
|
sc_cond_signal(&db->wait_cond);
|
||||||
|
sc_mutex_unlock(&db->mutex);
|
||||||
|
|
||||||
|
sc_thread_join(&db->thread, NULL);
|
||||||
|
|
||||||
|
sc_frame_source_sinks_close(&db->frame_source);
|
||||||
|
|
||||||
|
sc_cond_destroy(&db->wait_cond);
|
||||||
|
sc_cond_destroy(&db->queue_cond);
|
||||||
|
sc_mutex_destroy(&db->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink,
|
||||||
|
const AVFrame *frame) {
|
||||||
|
struct sc_delay_buffer *db = DOWNCAST(sink);
|
||||||
|
|
||||||
|
sc_mutex_lock(&db->mutex);
|
||||||
|
|
||||||
|
if (db->stopped) {
|
||||||
|
sc_mutex_unlock(&db->mutex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sc_tick pts = SC_TICK_FROM_US(frame->pts);
|
||||||
|
sc_clock_update(&db->clock, sc_tick_now(), pts);
|
||||||
|
sc_cond_signal(&db->wait_cond);
|
||||||
|
|
||||||
|
if (db->first_frame_asap && db->clock.count == 1) {
|
||||||
|
sc_mutex_unlock(&db->mutex);
|
||||||
|
return sc_frame_source_sinks_push(&db->frame_source, frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sc_delayed_frame dframe;
|
||||||
|
bool ok = sc_delayed_frame_init(&dframe, frame);
|
||||||
|
if (!ok) {
|
||||||
|
sc_mutex_unlock(&db->mutex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef SC_BUFFERING_NDEBUG
|
||||||
|
dframe.push_date = sc_tick_now();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ok = sc_vecdeque_push(&db->queue, dframe);
|
||||||
|
if (!ok) {
|
||||||
|
sc_mutex_unlock(&db->mutex);
|
||||||
|
LOG_OOM();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sc_cond_signal(&db->queue_cond);
|
||||||
|
|
||||||
|
sc_mutex_unlock(&db->mutex);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay,
|
||||||
|
bool first_frame_asap) {
|
||||||
|
assert(delay > 0);
|
||||||
|
|
||||||
|
db->delay = delay;
|
||||||
|
db->first_frame_asap = first_frame_asap;
|
||||||
|
|
||||||
|
sc_frame_source_init(&db->frame_source);
|
||||||
|
|
||||||
|
static const struct sc_frame_sink_ops ops = {
|
||||||
|
.open = sc_delay_buffer_frame_sink_open,
|
||||||
|
.close = sc_delay_buffer_frame_sink_close,
|
||||||
|
.push = sc_delay_buffer_frame_sink_push,
|
||||||
|
};
|
||||||
|
|
||||||
|
db->frame_sink.ops = &ops;
|
||||||
|
}
|
60
app/src/delay_buffer.h
Normal file
60
app/src/delay_buffer.h
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#ifndef SC_DELAY_BUFFER_H
|
||||||
|
#define SC_DELAY_BUFFER_H
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#include "clock.h"
|
||||||
|
#include "trait/frame_source.h"
|
||||||
|
#include "trait/frame_sink.h"
|
||||||
|
#include "util/thread.h"
|
||||||
|
#include "util/tick.h"
|
||||||
|
#include "util/vecdeque.h"
|
||||||
|
|
||||||
|
// forward declarations
|
||||||
|
typedef struct AVFrame AVFrame;
|
||||||
|
|
||||||
|
struct sc_delayed_frame {
|
||||||
|
AVFrame *frame;
|
||||||
|
#ifndef NDEBUG
|
||||||
|
sc_tick push_date;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
struct sc_delayed_frame_queue SC_VECDEQUE(struct sc_delayed_frame);
|
||||||
|
|
||||||
|
struct sc_delay_buffer {
|
||||||
|
struct sc_frame_source frame_source; // frame source trait
|
||||||
|
struct sc_frame_sink frame_sink; // frame sink trait
|
||||||
|
|
||||||
|
sc_tick delay;
|
||||||
|
bool first_frame_asap;
|
||||||
|
|
||||||
|
sc_thread thread;
|
||||||
|
sc_mutex mutex;
|
||||||
|
sc_cond queue_cond;
|
||||||
|
sc_cond wait_cond;
|
||||||
|
|
||||||
|
struct sc_clock clock;
|
||||||
|
struct sc_delayed_frame_queue queue;
|
||||||
|
bool stopped;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct sc_delay_buffer_callbacks {
|
||||||
|
bool (*on_new_frame)(struct sc_delay_buffer *db, const AVFrame *frame,
|
||||||
|
void *userdata);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a delay buffer.
|
||||||
|
*
|
||||||
|
* \param delay a (strictly) positive delay
|
||||||
|
* \param first_frame_asap if true, do not delay the first frame (useful for
|
||||||
|
a video stream).
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay,
|
||||||
|
bool first_frame_asap);
|
||||||
|
|
||||||
|
#endif
|
@ -23,6 +23,8 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) {
|
|||||||
#define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII
|
#define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII
|
||||||
#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII
|
#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII
|
||||||
#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII
|
#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII
|
||||||
|
#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII
|
||||||
|
#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII"
|
||||||
switch (codec_id) {
|
switch (codec_id) {
|
||||||
case SC_CODEC_ID_H264:
|
case SC_CODEC_ID_H264:
|
||||||
return AV_CODEC_ID_H264;
|
return AV_CODEC_ID_H264;
|
||||||
@ -30,6 +32,10 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) {
|
|||||||
return AV_CODEC_ID_HEVC;
|
return AV_CODEC_ID_HEVC;
|
||||||
case SC_CODEC_ID_AV1:
|
case SC_CODEC_ID_AV1:
|
||||||
return AV_CODEC_ID_AV1;
|
return AV_CODEC_ID_AV1;
|
||||||
|
case SC_CODEC_ID_OPUS:
|
||||||
|
return AV_CODEC_ID_OPUS;
|
||||||
|
case SC_CODEC_ID_AAC:
|
||||||
|
return AV_CODEC_ID_AAC;
|
||||||
default:
|
default:
|
||||||
LOGE("Unknown codec id 0x%08" PRIx32, codec_id);
|
LOGE("Unknown codec id 0x%08" PRIx32, codec_id);
|
||||||
return AV_CODEC_ID_NONE;
|
return AV_CODEC_ID_NONE;
|
||||||
@ -106,87 +112,64 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
|
||||||
push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) {
|
|
||||||
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
|
|
||||||
struct sc_packet_sink *sink = demuxer->sinks[i];
|
|
||||||
if (!sink->ops->push(sink, packet)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
|
||||||
bool ok = push_packet_to_sinks(demuxer, packet);
|
|
||||||
if (!ok) {
|
|
||||||
LOGE("Could not process packet");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
sc_demuxer_close_first_sinks(struct sc_demuxer *demuxer, unsigned count) {
|
|
||||||
while (count) {
|
|
||||||
struct sc_packet_sink *sink = demuxer->sinks[--count];
|
|
||||||
sink->ops->close(sink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
sc_demuxer_close_sinks(struct sc_demuxer *demuxer) {
|
|
||||||
sc_demuxer_close_first_sinks(demuxer, demuxer->sink_count);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_demuxer_open_sinks(struct sc_demuxer *demuxer, const AVCodec *codec) {
|
|
||||||
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
|
|
||||||
struct sc_packet_sink *sink = demuxer->sinks[i];
|
|
||||||
if (!sink->ops->open(sink, codec)) {
|
|
||||||
sc_demuxer_close_first_sinks(demuxer, i);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
static int
|
||||||
run_demuxer(void *data) {
|
run_demuxer(void *data) {
|
||||||
struct sc_demuxer *demuxer = data;
|
struct sc_demuxer *demuxer = data;
|
||||||
|
|
||||||
// Flag to report end-of-stream (i.e. device disconnected)
|
// Flag to report end-of-stream (i.e. device disconnected)
|
||||||
bool eos = false;
|
enum sc_demuxer_status status = SC_DEMUXER_STATUS_ERROR;
|
||||||
|
|
||||||
uint32_t raw_codec_id;
|
uint32_t raw_codec_id;
|
||||||
bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id);
|
bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
eos = true;
|
LOGE("Demuxer '%s': stream disabled due to connection error",
|
||||||
|
demuxer->name);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw_codec_id == 0) {
|
||||||
|
LOGW("Demuxer '%s': stream explicitly disabled by the device",
|
||||||
|
demuxer->name);
|
||||||
|
sc_packet_source_sinks_disable(&demuxer->packet_source);
|
||||||
|
status = SC_DEMUXER_STATUS_DISABLED;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw_codec_id == 1) {
|
||||||
|
LOGE("Demuxer '%s': stream configuration error on the device",
|
||||||
|
demuxer->name);
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AVCodecID codec_id = sc_demuxer_to_avcodec_id(raw_codec_id);
|
enum AVCodecID codec_id = sc_demuxer_to_avcodec_id(raw_codec_id);
|
||||||
if (codec_id == AV_CODEC_ID_NONE) {
|
if (codec_id == AV_CODEC_ID_NONE) {
|
||||||
// Error already logged
|
LOGE("Demuxer '%s': stream disabled due to unsupported codec",
|
||||||
|
demuxer->name);
|
||||||
|
sc_packet_source_sinks_disable(&demuxer->packet_source);
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AVCodec *codec = avcodec_find_decoder(codec_id);
|
const AVCodec *codec = avcodec_find_decoder(codec_id);
|
||||||
if (!codec) {
|
if (!codec) {
|
||||||
LOGE("Decoder not found");
|
LOGE("Demuxer '%s': stream disabled due to missing decoder",
|
||||||
|
demuxer->name);
|
||||||
|
sc_packet_source_sinks_disable(&demuxer->packet_source);
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sc_demuxer_open_sinks(demuxer, codec)) {
|
if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec)) {
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Config packets must be merged with the next non-config packet only for
|
||||||
|
// video streams
|
||||||
|
bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO;
|
||||||
|
|
||||||
struct sc_packet_merger merger;
|
struct sc_packet_merger merger;
|
||||||
sc_packet_merger_init(&merger);
|
|
||||||
|
if (must_merge_config_packet) {
|
||||||
|
sc_packet_merger_init(&merger);
|
||||||
|
}
|
||||||
|
|
||||||
AVPacket *packet = av_packet_alloc();
|
AVPacket *packet = av_packet_alloc();
|
||||||
if (!packet) {
|
if (!packet) {
|
||||||
@ -198,43 +181,50 @@ run_demuxer(void *data) {
|
|||||||
bool ok = sc_demuxer_recv_packet(demuxer, packet);
|
bool ok = sc_demuxer_recv_packet(demuxer, packet);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
// end of stream
|
// end of stream
|
||||||
eos = true;
|
status = SC_DEMUXER_STATUS_EOS;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepend any config packet to the next media packet
|
if (must_merge_config_packet) {
|
||||||
ok = sc_packet_merger_merge(&merger, packet);
|
// Prepend any config packet to the next media packet
|
||||||
if (!ok) {
|
ok = sc_packet_merger_merge(&merger, packet);
|
||||||
av_packet_unref(packet);
|
if (!ok) {
|
||||||
break;
|
av_packet_unref(packet);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = sc_demuxer_push_packet(demuxer, packet);
|
ok = sc_packet_source_sinks_push(&demuxer->packet_source, packet);
|
||||||
av_packet_unref(packet);
|
av_packet_unref(packet);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
// cannot process packet (error already logged)
|
LOGE("Demuxer '%s': could not process packet", demuxer->name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGD("End of frames");
|
LOGD("Demuxer '%s': end of frames", demuxer->name);
|
||||||
|
|
||||||
sc_packet_merger_destroy(&merger);
|
if (must_merge_config_packet) {
|
||||||
|
sc_packet_merger_destroy(&merger);
|
||||||
|
}
|
||||||
|
|
||||||
av_packet_free(&packet);
|
av_packet_free(&packet);
|
||||||
finally_close_sinks:
|
finally_close_sinks:
|
||||||
sc_demuxer_close_sinks(demuxer);
|
sc_packet_source_sinks_close(&demuxer->packet_source);
|
||||||
end:
|
end:
|
||||||
demuxer->cbs->on_ended(demuxer, eos, demuxer->cbs_userdata);
|
demuxer->cbs->on_ended(demuxer, status, demuxer->cbs_userdata);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
|
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
|
||||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) {
|
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) {
|
||||||
|
assert(socket != SC_SOCKET_NONE);
|
||||||
|
|
||||||
|
demuxer->name = name; // statically allocated
|
||||||
demuxer->socket = socket;
|
demuxer->socket = socket;
|
||||||
demuxer->sink_count = 0;
|
sc_packet_source_init(&demuxer->packet_source);
|
||||||
|
|
||||||
assert(cbs && cbs->on_ended);
|
assert(cbs && cbs->on_ended);
|
||||||
|
|
||||||
@ -242,22 +232,14 @@ sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
|
|||||||
demuxer->cbs_userdata = cbs_userdata;
|
demuxer->cbs_userdata = cbs_userdata;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink) {
|
|
||||||
assert(demuxer->sink_count < SC_DEMUXER_MAX_SINKS);
|
|
||||||
assert(sink);
|
|
||||||
assert(sink->ops);
|
|
||||||
demuxer->sinks[demuxer->sink_count++] = sink;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
sc_demuxer_start(struct sc_demuxer *demuxer) {
|
sc_demuxer_start(struct sc_demuxer *demuxer) {
|
||||||
LOGD("Starting demuxer thread");
|
LOGD("Demuxer '%s': starting thread", demuxer->name);
|
||||||
|
|
||||||
bool ok = sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer",
|
bool ok = sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer",
|
||||||
demuxer);
|
demuxer);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
LOGE("Could not start demuxer thread");
|
LOGE("Demuxer '%s': could not start thread", demuxer->name);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -8,33 +8,38 @@
|
|||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
|
|
||||||
|
#include "trait/packet_source.h"
|
||||||
#include "trait/packet_sink.h"
|
#include "trait/packet_sink.h"
|
||||||
#include "util/net.h"
|
#include "util/net.h"
|
||||||
#include "util/thread.h"
|
#include "util/thread.h"
|
||||||
|
|
||||||
#define SC_DEMUXER_MAX_SINKS 2
|
|
||||||
|
|
||||||
struct sc_demuxer {
|
struct sc_demuxer {
|
||||||
|
struct sc_packet_source packet_source; // packet source trait
|
||||||
|
|
||||||
|
const char *name; // must be statically allocated (e.g. a string literal)
|
||||||
|
|
||||||
sc_socket socket;
|
sc_socket socket;
|
||||||
sc_thread thread;
|
sc_thread thread;
|
||||||
|
|
||||||
struct sc_packet_sink *sinks[SC_DEMUXER_MAX_SINKS];
|
|
||||||
unsigned sink_count;
|
|
||||||
|
|
||||||
const struct sc_demuxer_callbacks *cbs;
|
const struct sc_demuxer_callbacks *cbs;
|
||||||
void *cbs_userdata;
|
void *cbs_userdata;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct sc_demuxer_callbacks {
|
enum sc_demuxer_status {
|
||||||
void (*on_ended)(struct sc_demuxer *demuxer, bool eos, void *userdata);
|
SC_DEMUXER_STATUS_EOS,
|
||||||
|
SC_DEMUXER_STATUS_DISABLED,
|
||||||
|
SC_DEMUXER_STATUS_ERROR,
|
||||||
};
|
};
|
||||||
|
|
||||||
void
|
struct sc_demuxer_callbacks {
|
||||||
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
|
void (*on_ended)(struct sc_demuxer *demuxer, enum sc_demuxer_status,
|
||||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata);
|
void *userdata);
|
||||||
|
};
|
||||||
|
|
||||||
|
// The name must be statically allocated (e.g. a string literal)
|
||||||
void
|
void
|
||||||
sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink);
|
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
|
||||||
|
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata);
|
||||||
|
|
||||||
bool
|
bool
|
||||||
sc_demuxer_start(struct sc_demuxer *demuxer);
|
sc_demuxer_start(struct sc_demuxer *demuxer);
|
||||||
|
@ -4,3 +4,4 @@
|
|||||||
#define SC_EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3)
|
#define SC_EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3)
|
||||||
#define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)
|
#define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)
|
||||||
#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5)
|
#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5)
|
||||||
|
#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6)
|
||||||
|
@ -19,7 +19,7 @@ sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial,
|
|||||||
const char *push_target) {
|
const char *push_target) {
|
||||||
assert(serial);
|
assert(serial);
|
||||||
|
|
||||||
cbuf_init(&fp->queue);
|
sc_vecdeque_init(&fp->queue);
|
||||||
|
|
||||||
bool ok = sc_mutex_init(&fp->mutex);
|
bool ok = sc_mutex_init(&fp->mutex);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
@ -65,9 +65,10 @@ sc_file_pusher_destroy(struct sc_file_pusher *fp) {
|
|||||||
sc_intr_destroy(&fp->intr);
|
sc_intr_destroy(&fp->intr);
|
||||||
free(fp->serial);
|
free(fp->serial);
|
||||||
|
|
||||||
struct sc_file_pusher_request req;
|
while (!sc_vecdeque_is_empty(&fp->queue)) {
|
||||||
while (cbuf_take(&fp->queue, &req)) {
|
struct sc_file_pusher_request *req = sc_vecdeque_popref(&fp->queue);
|
||||||
sc_file_pusher_request_destroy(&req);
|
assert(req);
|
||||||
|
sc_file_pusher_request_destroy(req);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,13 +92,20 @@ sc_file_pusher_request(struct sc_file_pusher *fp,
|
|||||||
};
|
};
|
||||||
|
|
||||||
sc_mutex_lock(&fp->mutex);
|
sc_mutex_lock(&fp->mutex);
|
||||||
bool was_empty = cbuf_is_empty(&fp->queue);
|
bool was_empty = sc_vecdeque_is_empty(&fp->queue);
|
||||||
bool res = cbuf_push(&fp->queue, req);
|
bool res = sc_vecdeque_push(&fp->queue, req);
|
||||||
|
if (!res) {
|
||||||
|
LOG_OOM();
|
||||||
|
sc_mutex_unlock(&fp->mutex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (was_empty) {
|
if (was_empty) {
|
||||||
sc_cond_signal(&fp->event_cond);
|
sc_cond_signal(&fp->event_cond);
|
||||||
}
|
}
|
||||||
sc_mutex_unlock(&fp->mutex);
|
sc_mutex_unlock(&fp->mutex);
|
||||||
return res;
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
@ -113,7 +121,7 @@ run_file_pusher(void *data) {
|
|||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
sc_mutex_lock(&fp->mutex);
|
sc_mutex_lock(&fp->mutex);
|
||||||
while (!fp->stopped && cbuf_is_empty(&fp->queue)) {
|
while (!fp->stopped && sc_vecdeque_is_empty(&fp->queue)) {
|
||||||
sc_cond_wait(&fp->event_cond, &fp->mutex);
|
sc_cond_wait(&fp->event_cond, &fp->mutex);
|
||||||
}
|
}
|
||||||
if (fp->stopped) {
|
if (fp->stopped) {
|
||||||
@ -121,10 +129,9 @@ run_file_pusher(void *data) {
|
|||||||
sc_mutex_unlock(&fp->mutex);
|
sc_mutex_unlock(&fp->mutex);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
struct sc_file_pusher_request req;
|
|
||||||
bool non_empty = cbuf_take(&fp->queue, &req);
|
assert(!sc_vecdeque_is_empty(&fp->queue));
|
||||||
assert(non_empty);
|
struct sc_file_pusher_request req = sc_vecdeque_pop(&fp->queue);
|
||||||
(void) non_empty;
|
|
||||||
sc_mutex_unlock(&fp->mutex);
|
sc_mutex_unlock(&fp->mutex);
|
||||||
|
|
||||||
if (req.action == SC_FILE_PUSHER_ACTION_INSTALL_APK) {
|
if (req.action == SC_FILE_PUSHER_ACTION_INSTALL_APK) {
|
||||||
|
@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
#include "util/cbuf.h"
|
|
||||||
#include "util/thread.h"
|
|
||||||
#include "util/intr.h"
|
#include "util/intr.h"
|
||||||
|
#include "util/thread.h"
|
||||||
|
#include "util/vecdeque.h"
|
||||||
|
|
||||||
enum sc_file_pusher_action {
|
enum sc_file_pusher_action {
|
||||||
SC_FILE_PUSHER_ACTION_INSTALL_APK,
|
SC_FILE_PUSHER_ACTION_INSTALL_APK,
|
||||||
@ -19,7 +19,7 @@ struct sc_file_pusher_request {
|
|||||||
char *file;
|
char *file;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct sc_file_pusher_request_queue CBUF(struct sc_file_pusher_request, 16);
|
struct sc_file_pusher_request_queue SC_VECDEQUE(struct sc_file_pusher_request);
|
||||||
|
|
||||||
struct sc_file_pusher {
|
struct sc_file_pusher {
|
||||||
char *serial;
|
char *serial;
|
||||||
|
@ -75,6 +75,8 @@ main_scrcpy(int argc, char *argv[]) {
|
|||||||
return SCRCPY_EXIT_FAILURE;
|
return SCRCPY_EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sc_log_configure();
|
||||||
|
|
||||||
#ifdef HAVE_USB
|
#ifdef HAVE_USB
|
||||||
enum scrcpy_exit_code ret = args.opts.otg ? scrcpy_otg(&args.opts)
|
enum scrcpy_exit_code ret = args.opts.otg ? scrcpy_otg(&args.opts)
|
||||||
: scrcpy(&args.opts);
|
: scrcpy(&args.opts);
|
||||||
|
@ -7,13 +7,16 @@ const struct scrcpy_options scrcpy_options_default = {
|
|||||||
.window_title = NULL,
|
.window_title = NULL,
|
||||||
.push_target = NULL,
|
.push_target = NULL,
|
||||||
.render_driver = NULL,
|
.render_driver = NULL,
|
||||||
.codec_options = NULL,
|
.video_codec_options = NULL,
|
||||||
.encoder_name = NULL,
|
.audio_codec_options = NULL,
|
||||||
|
.video_encoder = NULL,
|
||||||
|
.audio_encoder = NULL,
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
.v4l2_device = NULL,
|
.v4l2_device = NULL,
|
||||||
#endif
|
#endif
|
||||||
.log_level = SC_LOG_LEVEL_INFO,
|
.log_level = SC_LOG_LEVEL_INFO,
|
||||||
.codec = SC_CODEC_H264,
|
.video_codec = SC_CODEC_H264,
|
||||||
|
.audio_codec = SC_CODEC_OPUS,
|
||||||
.record_format = SC_RECORD_FORMAT_AUTO,
|
.record_format = SC_RECORD_FORMAT_AUTO,
|
||||||
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT,
|
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT,
|
||||||
.mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT,
|
.mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT,
|
||||||
@ -28,7 +31,8 @@ const struct scrcpy_options scrcpy_options_default = {
|
|||||||
.count = 2,
|
.count = 2,
|
||||||
},
|
},
|
||||||
.max_size = 0,
|
.max_size = 0,
|
||||||
.bit_rate = DEFAULT_BIT_RATE,
|
.video_bit_rate = 0,
|
||||||
|
.audio_bit_rate = 0,
|
||||||
.max_fps = 0,
|
.max_fps = 0,
|
||||||
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
|
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
|
||||||
.rotation = 0,
|
.rotation = 0,
|
||||||
@ -39,6 +43,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
|||||||
.display_id = 0,
|
.display_id = 0,
|
||||||
.display_buffer = 0,
|
.display_buffer = 0,
|
||||||
.v4l2_buffer = 0,
|
.v4l2_buffer = 0,
|
||||||
|
.audio_buffer = 0,
|
||||||
#ifdef HAVE_USB
|
#ifdef HAVE_USB
|
||||||
.otg = false,
|
.otg = false,
|
||||||
#endif
|
#endif
|
||||||
@ -67,4 +72,8 @@ const struct scrcpy_options scrcpy_options_default = {
|
|||||||
.cleanup = true,
|
.cleanup = true,
|
||||||
.start_fps_counter = false,
|
.start_fps_counter = false,
|
||||||
.power_on = true,
|
.power_on = true,
|
||||||
|
.audio = true,
|
||||||
|
.require_audio = false,
|
||||||
|
.list_encoders = false,
|
||||||
|
.list_displays = false,
|
||||||
};
|
};
|
||||||
|
@ -27,6 +27,8 @@ enum sc_codec {
|
|||||||
SC_CODEC_H264,
|
SC_CODEC_H264,
|
||||||
SC_CODEC_H265,
|
SC_CODEC_H265,
|
||||||
SC_CODEC_AV1,
|
SC_CODEC_AV1,
|
||||||
|
SC_CODEC_OPUS,
|
||||||
|
SC_CODEC_AAC,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum sc_lock_video_orientation {
|
enum sc_lock_video_orientation {
|
||||||
@ -93,13 +95,16 @@ struct scrcpy_options {
|
|||||||
const char *window_title;
|
const char *window_title;
|
||||||
const char *push_target;
|
const char *push_target;
|
||||||
const char *render_driver;
|
const char *render_driver;
|
||||||
const char *codec_options;
|
const char *video_codec_options;
|
||||||
const char *encoder_name;
|
const char *audio_codec_options;
|
||||||
|
const char *video_encoder;
|
||||||
|
const char *audio_encoder;
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
const char *v4l2_device;
|
const char *v4l2_device;
|
||||||
#endif
|
#endif
|
||||||
enum sc_log_level log_level;
|
enum sc_log_level log_level;
|
||||||
enum sc_codec codec;
|
enum sc_codec video_codec;
|
||||||
|
enum sc_codec audio_codec;
|
||||||
enum sc_record_format record_format;
|
enum sc_record_format record_format;
|
||||||
enum sc_keyboard_input_mode keyboard_input_mode;
|
enum sc_keyboard_input_mode keyboard_input_mode;
|
||||||
enum sc_mouse_input_mode mouse_input_mode;
|
enum sc_mouse_input_mode mouse_input_mode;
|
||||||
@ -108,7 +113,8 @@ struct scrcpy_options {
|
|||||||
uint16_t tunnel_port;
|
uint16_t tunnel_port;
|
||||||
struct sc_shortcut_mods shortcut_mods;
|
struct sc_shortcut_mods shortcut_mods;
|
||||||
uint16_t max_size;
|
uint16_t max_size;
|
||||||
uint32_t bit_rate;
|
uint32_t video_bit_rate;
|
||||||
|
uint32_t audio_bit_rate;
|
||||||
uint16_t max_fps;
|
uint16_t max_fps;
|
||||||
enum sc_lock_video_orientation lock_video_orientation;
|
enum sc_lock_video_orientation lock_video_orientation;
|
||||||
uint8_t rotation;
|
uint8_t rotation;
|
||||||
@ -119,6 +125,7 @@ struct scrcpy_options {
|
|||||||
uint32_t display_id;
|
uint32_t display_id;
|
||||||
sc_tick display_buffer;
|
sc_tick display_buffer;
|
||||||
sc_tick v4l2_buffer;
|
sc_tick v4l2_buffer;
|
||||||
|
sc_tick audio_buffer;
|
||||||
#ifdef HAVE_USB
|
#ifdef HAVE_USB
|
||||||
bool otg;
|
bool otg;
|
||||||
#endif
|
#endif
|
||||||
@ -147,6 +154,10 @@ struct scrcpy_options {
|
|||||||
bool cleanup;
|
bool cleanup;
|
||||||
bool start_fps_counter;
|
bool start_fps_counter;
|
||||||
bool power_on;
|
bool power_on;
|
||||||
|
bool audio;
|
||||||
|
bool require_audio;
|
||||||
|
bool list_encoders;
|
||||||
|
bool list_displays;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern const struct scrcpy_options scrcpy_options_default;
|
extern const struct scrcpy_options scrcpy_options_default;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -9,45 +9,72 @@
|
|||||||
#include "coords.h"
|
#include "coords.h"
|
||||||
#include "options.h"
|
#include "options.h"
|
||||||
#include "trait/packet_sink.h"
|
#include "trait/packet_sink.h"
|
||||||
#include "util/queue.h"
|
|
||||||
#include "util/thread.h"
|
#include "util/thread.h"
|
||||||
|
#include "util/vecdeque.h"
|
||||||
|
|
||||||
struct sc_record_packet {
|
struct sc_recorder_queue SC_VECDEQUE(AVPacket *);
|
||||||
AVPacket *packet;
|
|
||||||
struct sc_record_packet *next;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_recorder_queue SC_QUEUE(struct sc_record_packet);
|
|
||||||
|
|
||||||
struct sc_recorder {
|
struct sc_recorder {
|
||||||
struct sc_packet_sink packet_sink; // packet sink trait
|
struct sc_packet_sink video_packet_sink;
|
||||||
|
struct sc_packet_sink audio_packet_sink;
|
||||||
|
|
||||||
|
/* The audio flag is unprotected:
|
||||||
|
* - it is initialized from sc_recorder_init() from the main thread;
|
||||||
|
* - it may be reset once from the recorder thread if the audio is
|
||||||
|
* disabled dynamically.
|
||||||
|
*
|
||||||
|
* Therefore, once the recorder thread is started, only the recorder thread
|
||||||
|
* may access it without data races.
|
||||||
|
*/
|
||||||
|
bool audio;
|
||||||
|
|
||||||
char *filename;
|
char *filename;
|
||||||
enum sc_record_format format;
|
enum sc_record_format format;
|
||||||
AVFormatContext *ctx;
|
AVFormatContext *ctx;
|
||||||
struct sc_size declared_frame_size;
|
struct sc_size declared_frame_size;
|
||||||
bool header_written;
|
|
||||||
|
|
||||||
uint64_t pts_origin;
|
|
||||||
|
|
||||||
sc_thread thread;
|
sc_thread thread;
|
||||||
sc_mutex mutex;
|
sc_mutex mutex;
|
||||||
sc_cond queue_cond;
|
sc_cond queue_cond;
|
||||||
bool stopped; // set on recorder_close()
|
// set on sc_recorder_stop(), packet_sink close or recording failure
|
||||||
bool failed; // set on packet write failure
|
bool stopped;
|
||||||
struct sc_recorder_queue queue;
|
struct sc_recorder_queue video_queue;
|
||||||
|
struct sc_recorder_queue audio_queue;
|
||||||
|
|
||||||
// we can write a packet only once we received the next one so that we can
|
// wake up the recorder thread once the video or audio codec is known
|
||||||
// set its duration (next_pts - current_pts)
|
sc_cond stream_cond;
|
||||||
// "previous" is only accessed from the recorder thread, so it does not
|
const AVCodec *video_codec;
|
||||||
// need to be protected by the mutex
|
const AVCodec *audio_codec;
|
||||||
struct sc_record_packet *previous;
|
// Instead of providing an audio_codec, the demuxer may notify that the
|
||||||
|
// stream is disabled if the device could not capture audio
|
||||||
|
bool audio_disabled;
|
||||||
|
|
||||||
|
int video_stream_index;
|
||||||
|
int audio_stream_index;
|
||||||
|
|
||||||
|
const struct sc_recorder_callbacks *cbs;
|
||||||
|
void *cbs_userdata;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct sc_recorder_callbacks {
|
||||||
|
void (*on_ended)(struct sc_recorder *recorder, bool success,
|
||||||
|
void *userdata);
|
||||||
};
|
};
|
||||||
|
|
||||||
bool
|
bool
|
||||||
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||||
enum sc_record_format format,
|
enum sc_record_format format, bool audio,
|
||||||
struct sc_size declared_frame_size);
|
struct sc_size declared_frame_size,
|
||||||
|
const struct sc_recorder_callbacks *cbs, void *cbs_userdata);
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_recorder_start(struct sc_recorder *recorder);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_recorder_stop(struct sc_recorder *recorder);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_recorder_join(struct sc_recorder *recorder);
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_recorder_destroy(struct sc_recorder *recorder);
|
sc_recorder_destroy(struct sc_recorder *recorder);
|
||||||
|
285
app/src/scrcpy.c
285
app/src/scrcpy.c
@ -13,8 +13,10 @@
|
|||||||
# include <windows.h>
|
# include <windows.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include "audio_player.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "decoder.h"
|
#include "decoder.h"
|
||||||
|
#include "delay_buffer.h"
|
||||||
#include "demuxer.h"
|
#include "demuxer.h"
|
||||||
#include "events.h"
|
#include "events.h"
|
||||||
#include "file_pusher.h"
|
#include "file_pusher.h"
|
||||||
@ -40,11 +42,17 @@
|
|||||||
struct scrcpy {
|
struct scrcpy {
|
||||||
struct sc_server server;
|
struct sc_server server;
|
||||||
struct sc_screen screen;
|
struct sc_screen screen;
|
||||||
struct sc_demuxer demuxer;
|
struct sc_audio_player audio_player;
|
||||||
struct sc_decoder decoder;
|
struct sc_delay_buffer audio_buffer;
|
||||||
|
struct sc_demuxer video_demuxer;
|
||||||
|
struct sc_demuxer audio_demuxer;
|
||||||
|
struct sc_decoder video_decoder;
|
||||||
|
struct sc_decoder audio_decoder;
|
||||||
struct sc_recorder recorder;
|
struct sc_recorder recorder;
|
||||||
|
struct sc_delay_buffer display_buffer;
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
struct sc_v4l2_sink v4l2_sink;
|
struct sc_v4l2_sink v4l2_sink;
|
||||||
|
struct sc_delay_buffer v4l2_buffer;
|
||||||
#endif
|
#endif
|
||||||
struct sc_controller controller;
|
struct sc_controller controller;
|
||||||
struct sc_file_pusher file_pusher;
|
struct sc_file_pusher file_pusher;
|
||||||
@ -161,6 +169,9 @@ event_loop(struct scrcpy *s) {
|
|||||||
case SC_EVENT_DEMUXER_ERROR:
|
case SC_EVENT_DEMUXER_ERROR:
|
||||||
LOGE("Demuxer error");
|
LOGE("Demuxer error");
|
||||||
return SCRCPY_EXIT_FAILURE;
|
return SCRCPY_EXIT_FAILURE;
|
||||||
|
case SC_EVENT_RECORDER_ERROR:
|
||||||
|
LOGE("Recorder error");
|
||||||
|
return SCRCPY_EXIT_FAILURE;
|
||||||
case SDL_QUIT:
|
case SDL_QUIT:
|
||||||
LOGD("User requested to quit");
|
LOGD("User requested to quit");
|
||||||
return SCRCPY_EXIT_SUCCESS;
|
return SCRCPY_EXIT_SUCCESS;
|
||||||
@ -179,15 +190,16 @@ await_for_server(bool *connected) {
|
|||||||
while (SDL_WaitEvent(&event)) {
|
while (SDL_WaitEvent(&event)) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case SDL_QUIT:
|
case SDL_QUIT:
|
||||||
LOGD("User requested to quit");
|
if (connected) {
|
||||||
*connected = false;
|
*connected = false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
case SC_EVENT_SERVER_CONNECTION_FAILED:
|
case SC_EVENT_SERVER_CONNECTION_FAILED:
|
||||||
LOGE("Server connection failed");
|
|
||||||
return false;
|
return false;
|
||||||
case SC_EVENT_SERVER_CONNECTED:
|
case SC_EVENT_SERVER_CONNECTED:
|
||||||
LOGD("Server connected");
|
if (connected) {
|
||||||
*connected = true;
|
*connected = true;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@ -198,55 +210,56 @@ await_for_server(bool *connected) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static SDL_LogPriority
|
static void
|
||||||
sdl_priority_from_av_level(int level) {
|
sc_recorder_on_ended(struct sc_recorder *recorder, bool success,
|
||||||
switch (level) {
|
void *userdata) {
|
||||||
case AV_LOG_PANIC:
|
(void) recorder;
|
||||||
case AV_LOG_FATAL:
|
(void) userdata;
|
||||||
return SDL_LOG_PRIORITY_CRITICAL;
|
|
||||||
case AV_LOG_ERROR:
|
if (!success) {
|
||||||
return SDL_LOG_PRIORITY_ERROR;
|
PUSH_EVENT(SC_EVENT_RECORDER_ERROR);
|
||||||
case AV_LOG_WARNING:
|
|
||||||
return SDL_LOG_PRIORITY_WARN;
|
|
||||||
case AV_LOG_INFO:
|
|
||||||
return SDL_LOG_PRIORITY_INFO;
|
|
||||||
}
|
}
|
||||||
// do not forward others, which are too verbose
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
sc_video_demuxer_on_ended(struct sc_demuxer *demuxer,
|
||||||
(void) avcl;
|
enum sc_demuxer_status status, void *userdata) {
|
||||||
SDL_LogPriority priority = sdl_priority_from_av_level(level);
|
|
||||||
if (priority == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t fmt_len = strlen(fmt);
|
|
||||||
char *local_fmt = malloc(fmt_len + 10);
|
|
||||||
if (!local_fmt) {
|
|
||||||
LOG_OOM();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
memcpy(local_fmt, "[FFmpeg] ", 9); // do not write the final '\0'
|
|
||||||
memcpy(local_fmt + 9, fmt, fmt_len + 1); // include '\0'
|
|
||||||
SDL_LogMessageV(SDL_LOG_CATEGORY_VIDEO, priority, local_fmt, vl);
|
|
||||||
free(local_fmt);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
sc_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos, void *userdata) {
|
|
||||||
(void) demuxer;
|
(void) demuxer;
|
||||||
(void) userdata;
|
(void) userdata;
|
||||||
|
|
||||||
if (eos) {
|
// The device may not decide to disable the video
|
||||||
|
assert(status != SC_DEMUXER_STATUS_DISABLED);
|
||||||
|
|
||||||
|
if (status == SC_DEMUXER_STATUS_EOS) {
|
||||||
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
|
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
|
||||||
} else {
|
} else {
|
||||||
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
|
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer,
|
||||||
|
enum sc_demuxer_status status, void *userdata) {
|
||||||
|
(void) demuxer;
|
||||||
|
|
||||||
|
const struct scrcpy_options *options = userdata;
|
||||||
|
|
||||||
|
// Contrary to the video demuxer, keep mirroring if only the audio fails
|
||||||
|
// (unless --require-audio is set).
|
||||||
|
// 'eos' is true on end-of-stream, including when audio capture is not
|
||||||
|
// possible on the device (so that scrcpy continue to mirror video without
|
||||||
|
// failing).
|
||||||
|
// However, if an audio configuration failure occurs (for example the user
|
||||||
|
// explicitly selected an unknown audio encoder), 'eos' is false and scrcpy
|
||||||
|
// must exit.
|
||||||
|
|
||||||
|
if (status == SC_DEMUXER_STATUS_ERROR
|
||||||
|
|| (status == SC_DEMUXER_STATUS_DISABLED
|
||||||
|
&& options->require_audio)) {
|
||||||
|
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
sc_server_on_connection_failed(struct sc_server *server, void *userdata) {
|
sc_server_on_connection_failed(struct sc_server *server, void *userdata) {
|
||||||
(void) server;
|
(void) server;
|
||||||
@ -300,10 +313,12 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
bool server_started = false;
|
bool server_started = false;
|
||||||
bool file_pusher_initialized = false;
|
bool file_pusher_initialized = false;
|
||||||
bool recorder_initialized = false;
|
bool recorder_initialized = false;
|
||||||
|
bool recorder_started = false;
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
bool v4l2_sink_initialized = false;
|
bool v4l2_sink_initialized = false;
|
||||||
#endif
|
#endif
|
||||||
bool demuxer_started = false;
|
bool video_demuxer_started = false;
|
||||||
|
bool audio_demuxer_started = false;
|
||||||
#ifdef HAVE_USB
|
#ifdef HAVE_USB
|
||||||
bool aoa_hid_initialized = false;
|
bool aoa_hid_initialized = false;
|
||||||
bool hid_keyboard_initialized = false;
|
bool hid_keyboard_initialized = false;
|
||||||
@ -323,21 +338,26 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
.select_usb = options->select_usb,
|
.select_usb = options->select_usb,
|
||||||
.select_tcpip = options->select_tcpip,
|
.select_tcpip = options->select_tcpip,
|
||||||
.log_level = options->log_level,
|
.log_level = options->log_level,
|
||||||
.codec = options->codec,
|
.video_codec = options->video_codec,
|
||||||
|
.audio_codec = options->audio_codec,
|
||||||
.crop = options->crop,
|
.crop = options->crop,
|
||||||
.port_range = options->port_range,
|
.port_range = options->port_range,
|
||||||
.tunnel_host = options->tunnel_host,
|
.tunnel_host = options->tunnel_host,
|
||||||
.tunnel_port = options->tunnel_port,
|
.tunnel_port = options->tunnel_port,
|
||||||
.max_size = options->max_size,
|
.max_size = options->max_size,
|
||||||
.bit_rate = options->bit_rate,
|
.video_bit_rate = options->video_bit_rate,
|
||||||
|
.audio_bit_rate = options->audio_bit_rate,
|
||||||
.max_fps = options->max_fps,
|
.max_fps = options->max_fps,
|
||||||
.lock_video_orientation = options->lock_video_orientation,
|
.lock_video_orientation = options->lock_video_orientation,
|
||||||
.control = options->control,
|
.control = options->control,
|
||||||
.display_id = options->display_id,
|
.display_id = options->display_id,
|
||||||
|
.audio = options->audio,
|
||||||
.show_touches = options->show_touches,
|
.show_touches = options->show_touches,
|
||||||
.stay_awake = options->stay_awake,
|
.stay_awake = options->stay_awake,
|
||||||
.codec_options = options->codec_options,
|
.video_codec_options = options->video_codec_options,
|
||||||
.encoder_name = options->encoder_name,
|
.audio_codec_options = options->audio_codec_options,
|
||||||
|
.video_encoder = options->video_encoder,
|
||||||
|
.audio_encoder = options->audio_encoder,
|
||||||
.force_adb_forward = options->force_adb_forward,
|
.force_adb_forward = options->force_adb_forward,
|
||||||
.power_off_on_close = options->power_off_on_close,
|
.power_off_on_close = options->power_off_on_close,
|
||||||
.clipboard_autosync = options->clipboard_autosync,
|
.clipboard_autosync = options->clipboard_autosync,
|
||||||
@ -346,6 +366,8 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
.tcpip_dst = options->tcpip_dst,
|
.tcpip_dst = options->tcpip_dst,
|
||||||
.cleanup = options->cleanup,
|
.cleanup = options->cleanup,
|
||||||
.power_on = options->power_on,
|
.power_on = options->power_on,
|
||||||
|
.list_encoders = options->list_encoders,
|
||||||
|
.list_displays = options->list_displays,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const struct sc_server_callbacks cbs = {
|
static const struct sc_server_callbacks cbs = {
|
||||||
@ -363,14 +385,27 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
|
|
||||||
server_started = true;
|
server_started = true;
|
||||||
|
|
||||||
|
if (options->list_encoders || options->list_displays) {
|
||||||
|
bool ok = await_for_server(NULL);
|
||||||
|
ret = ok ? SCRCPY_EXIT_SUCCESS : SCRCPY_EXIT_FAILURE;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
if (options->display) {
|
if (options->display) {
|
||||||
sdl_set_hints(options->render_driver);
|
sdl_set_hints(options->render_driver);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize SDL video in addition if display is enabled
|
// Initialize SDL video in addition if display is enabled
|
||||||
if (options->display && SDL_Init(SDL_INIT_VIDEO)) {
|
if (options->display) {
|
||||||
LOGE("Could not initialize SDL: %s", SDL_GetError());
|
if (SDL_Init(SDL_INIT_VIDEO)) {
|
||||||
goto end;
|
LOGE("Could not initialize SDL video: %s", SDL_GetError());
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options->audio && SDL_Init(SDL_INIT_AUDIO)) {
|
||||||
|
LOGE("Could not initialize SDL audio: %s", SDL_GetError());
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sdl_configure(options->display, options->disable_screensaver);
|
sdl_configure(options->display, options->disable_screensaver);
|
||||||
@ -378,15 +413,19 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
// Await for server without blocking Ctrl+C handling
|
// Await for server without blocking Ctrl+C handling
|
||||||
bool connected;
|
bool connected;
|
||||||
if (!await_for_server(&connected)) {
|
if (!await_for_server(&connected)) {
|
||||||
|
LOGE("Server connection failed");
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
// This is not an error, user requested to quit
|
// This is not an error, user requested to quit
|
||||||
|
LOGD("User requested to quit");
|
||||||
ret = SCRCPY_EXIT_SUCCESS;
|
ret = SCRCPY_EXIT_SUCCESS;
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOGD("Server connected");
|
||||||
|
|
||||||
// It is necessarily initialized here, since the device is connected
|
// It is necessarily initialized here, since the device is connected
|
||||||
struct sc_server_info *info = &s->server.info;
|
struct sc_server_info *info = &s->server.info;
|
||||||
|
|
||||||
@ -404,41 +443,58 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
file_pusher_initialized = true;
|
file_pusher_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct sc_decoder *dec = NULL;
|
static const struct sc_demuxer_callbacks video_demuxer_cbs = {
|
||||||
bool needs_decoder = options->display;
|
.on_ended = sc_video_demuxer_on_ended,
|
||||||
#ifdef HAVE_V4L2
|
};
|
||||||
needs_decoder |= !!options->v4l2_device;
|
sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket,
|
||||||
#endif
|
&video_demuxer_cbs, NULL);
|
||||||
if (needs_decoder) {
|
|
||||||
sc_decoder_init(&s->decoder);
|
if (options->audio) {
|
||||||
dec = &s->decoder;
|
static const struct sc_demuxer_callbacks audio_demuxer_cbs = {
|
||||||
|
.on_ended = sc_audio_demuxer_on_ended,
|
||||||
|
};
|
||||||
|
sc_demuxer_init(&s->audio_demuxer, "audio", s->server.audio_socket,
|
||||||
|
&audio_demuxer_cbs, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool needs_video_decoder = options->display;
|
||||||
|
bool needs_audio_decoder = options->audio && options->display;
|
||||||
|
#ifdef HAVE_V4L2
|
||||||
|
needs_video_decoder |= !!options->v4l2_device;
|
||||||
|
#endif
|
||||||
|
if (needs_video_decoder) {
|
||||||
|
sc_decoder_init(&s->video_decoder, "video");
|
||||||
|
sc_packet_source_add_sink(&s->video_demuxer.packet_source,
|
||||||
|
&s->video_decoder.packet_sink);
|
||||||
|
}
|
||||||
|
if (needs_audio_decoder) {
|
||||||
|
sc_decoder_init(&s->audio_decoder, "audio");
|
||||||
|
sc_packet_source_add_sink(&s->audio_demuxer.packet_source,
|
||||||
|
&s->audio_decoder.packet_sink);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct sc_recorder *rec = NULL;
|
|
||||||
if (options->record_filename) {
|
if (options->record_filename) {
|
||||||
if (!sc_recorder_init(&s->recorder,
|
static const struct sc_recorder_callbacks recorder_cbs = {
|
||||||
options->record_filename,
|
.on_ended = sc_recorder_on_ended,
|
||||||
options->record_format,
|
};
|
||||||
info->frame_size)) {
|
if (!sc_recorder_init(&s->recorder, options->record_filename,
|
||||||
|
options->record_format, options->audio,
|
||||||
|
info->frame_size, &recorder_cbs, NULL)) {
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
rec = &s->recorder;
|
|
||||||
recorder_initialized = true;
|
recorder_initialized = true;
|
||||||
}
|
|
||||||
|
|
||||||
av_log_set_callback(av_log_callback);
|
if (!sc_recorder_start(&s->recorder)) {
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
recorder_started = true;
|
||||||
|
|
||||||
static const struct sc_demuxer_callbacks demuxer_cbs = {
|
sc_packet_source_add_sink(&s->video_demuxer.packet_source,
|
||||||
.on_ended = sc_demuxer_on_ended,
|
&s->recorder.video_packet_sink);
|
||||||
};
|
if (options->audio) {
|
||||||
sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL);
|
sc_packet_source_add_sink(&s->audio_demuxer.packet_source,
|
||||||
|
&s->recorder.audio_packet_sink);
|
||||||
if (dec) {
|
}
|
||||||
sc_demuxer_add_sink(&s->demuxer, &dec->packet_sink);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rec) {
|
|
||||||
sc_demuxer_add_sink(&s->demuxer, &rec->packet_sink);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct sc_controller *controller = NULL;
|
struct sc_controller *controller = NULL;
|
||||||
@ -621,7 +677,6 @@ aoa_hid_end:
|
|||||||
.mipmaps = options->mipmaps,
|
.mipmaps = options->mipmaps,
|
||||||
.fullscreen = options->fullscreen,
|
.fullscreen = options->fullscreen,
|
||||||
.start_fps_counter = options->start_fps_counter,
|
.start_fps_counter = options->start_fps_counter,
|
||||||
.buffering_time = options->display_buffer,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!sc_screen_init(&s->screen, &screen_params)) {
|
if (!sc_screen_init(&s->screen, &screen_params)) {
|
||||||
@ -629,34 +684,69 @@ aoa_hid_end:
|
|||||||
}
|
}
|
||||||
screen_initialized = true;
|
screen_initialized = true;
|
||||||
|
|
||||||
sc_decoder_add_sink(&s->decoder, &s->screen.frame_sink);
|
struct sc_frame_source *src = &s->video_decoder.frame_source;
|
||||||
|
if (options->display_buffer) {
|
||||||
|
sc_delay_buffer_init(&s->display_buffer, options->display_buffer,
|
||||||
|
true);
|
||||||
|
sc_frame_source_add_sink(src, &s->display_buffer.frame_sink);
|
||||||
|
src = &s->display_buffer.frame_source;
|
||||||
|
}
|
||||||
|
|
||||||
|
sc_frame_source_add_sink(src, &s->screen.frame_sink);
|
||||||
|
|
||||||
|
if (options->audio) {
|
||||||
|
struct sc_frame_source *src = &s->audio_decoder.frame_source;
|
||||||
|
if (options->audio_buffer) {
|
||||||
|
sc_delay_buffer_init(&s->audio_buffer, options->audio_buffer,
|
||||||
|
false);
|
||||||
|
sc_frame_source_add_sink(src, &s->audio_buffer.frame_sink);
|
||||||
|
src = &s->audio_buffer.frame_source;
|
||||||
|
}
|
||||||
|
|
||||||
|
sc_audio_player_init(&s->audio_player);
|
||||||
|
sc_frame_source_add_sink(src, &s->audio_player.frame_sink);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
if (options->v4l2_device) {
|
if (options->v4l2_device) {
|
||||||
if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device,
|
if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device,
|
||||||
info->frame_size, options->v4l2_buffer)) {
|
info->frame_size)) {
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
sc_decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink);
|
struct sc_frame_source *src = &s->video_decoder.frame_source;
|
||||||
|
if (options->v4l2_buffer) {
|
||||||
|
sc_delay_buffer_init(&s->v4l2_buffer, options->v4l2_buffer, true);
|
||||||
|
sc_frame_source_add_sink(src, &s->v4l2_buffer.frame_sink);
|
||||||
|
src = &s->v4l2_buffer.frame_source;
|
||||||
|
}
|
||||||
|
|
||||||
|
sc_frame_source_add_sink(src, &s->v4l2_sink.frame_sink);
|
||||||
|
|
||||||
v4l2_sink_initialized = true;
|
v4l2_sink_initialized = true;
|
||||||
}
|
}
|
||||||
#endif
|
#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 demuxer
|
// start the video demuxer
|
||||||
if (!sc_demuxer_start(&s->demuxer)) {
|
if (!sc_demuxer_start(&s->video_demuxer)) {
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
demuxer_started = true;
|
video_demuxer_started = true;
|
||||||
|
|
||||||
|
if (options->audio) {
|
||||||
|
if (!sc_demuxer_start(&s->audio_demuxer)) {
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
audio_demuxer_started = true;
|
||||||
|
}
|
||||||
|
|
||||||
ret = event_loop(s);
|
ret = event_loop(s);
|
||||||
LOGD("quit...");
|
LOGD("quit...");
|
||||||
|
|
||||||
// Close the window immediately on closing, because screen_destroy() may
|
// Close the window immediately on closing, because screen_destroy() may
|
||||||
// only be called once the demuxer thread is joined (it may take time)
|
// only be called once the video demuxer thread is joined (it may take time)
|
||||||
sc_screen_hide_window(&s->screen);
|
sc_screen_hide_window(&s->screen);
|
||||||
|
|
||||||
end:
|
end:
|
||||||
@ -683,6 +773,9 @@ end:
|
|||||||
if (file_pusher_initialized) {
|
if (file_pusher_initialized) {
|
||||||
sc_file_pusher_stop(&s->file_pusher);
|
sc_file_pusher_stop(&s->file_pusher);
|
||||||
}
|
}
|
||||||
|
if (recorder_initialized) {
|
||||||
|
sc_recorder_stop(&s->recorder);
|
||||||
|
}
|
||||||
if (screen_initialized) {
|
if (screen_initialized) {
|
||||||
sc_screen_interrupt(&s->screen);
|
sc_screen_interrupt(&s->screen);
|
||||||
}
|
}
|
||||||
@ -694,8 +787,12 @@ end:
|
|||||||
|
|
||||||
// now that the sockets are shutdown, the demuxer and controller are
|
// now that the sockets are shutdown, the demuxer and controller are
|
||||||
// interrupted, we can join them
|
// interrupted, we can join them
|
||||||
if (demuxer_started) {
|
if (video_demuxer_started) {
|
||||||
sc_demuxer_join(&s->demuxer);
|
sc_demuxer_join(&s->video_demuxer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audio_demuxer_started) {
|
||||||
|
sc_demuxer_join(&s->audio_demuxer);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
@ -714,8 +811,9 @@ end:
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Destroy the screen only after the demuxer is guaranteed to be finished,
|
// Destroy the screen only after the video demuxer is guaranteed to be
|
||||||
// because otherwise the screen could receive new frames after destruction
|
// finished, because otherwise the screen could receive new frames after
|
||||||
|
// destruction
|
||||||
if (screen_initialized) {
|
if (screen_initialized) {
|
||||||
sc_screen_join(&s->screen);
|
sc_screen_join(&s->screen);
|
||||||
sc_screen_destroy(&s->screen);
|
sc_screen_destroy(&s->screen);
|
||||||
@ -728,6 +826,9 @@ end:
|
|||||||
sc_controller_destroy(&s->controller);
|
sc_controller_destroy(&s->controller);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (recorder_started) {
|
||||||
|
sc_recorder_join(&s->recorder);
|
||||||
|
}
|
||||||
if (recorder_initialized) {
|
if (recorder_initialized) {
|
||||||
sc_recorder_destroy(&s->recorder);
|
sc_recorder_destroy(&s->recorder);
|
||||||
}
|
}
|
||||||
@ -737,6 +838,10 @@ end:
|
|||||||
sc_file_pusher_destroy(&s->file_pusher);
|
sc_file_pusher_destroy(&s->file_pusher);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (server_started) {
|
||||||
|
sc_server_join(&s->server);
|
||||||
|
}
|
||||||
|
|
||||||
sc_server_destroy(&s->server);
|
sc_server_destroy(&s->server);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
#include "events.h"
|
#include "events.h"
|
||||||
#include "icon.h"
|
#include "icon.h"
|
||||||
#include "options.h"
|
#include "options.h"
|
||||||
#include "video_buffer.h"
|
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
#define DISPLAY_MARGINS 96
|
#define DISPLAY_MARGINS 96
|
||||||
@ -330,7 +329,11 @@ event_watcher(void *data, SDL_Event *event) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
sc_screen_frame_sink_open(struct sc_frame_sink *sink) {
|
sc_screen_frame_sink_open(struct sc_frame_sink *sink,
|
||||||
|
const AVCodecContext *ctx) {
|
||||||
|
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
|
||||||
|
(void) ctx;
|
||||||
|
|
||||||
struct sc_screen *screen = DOWNCAST(sink);
|
struct sc_screen *screen = DOWNCAST(sink);
|
||||||
(void) screen;
|
(void) screen;
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
@ -355,30 +358,18 @@ sc_screen_frame_sink_close(struct sc_frame_sink *sink) {
|
|||||||
static bool
|
static bool
|
||||||
sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
|
sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
|
||||||
struct sc_screen *screen = DOWNCAST(sink);
|
struct sc_screen *screen = DOWNCAST(sink);
|
||||||
return sc_video_buffer_push(&screen->vb, frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
bool previous_skipped;
|
||||||
sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
|
bool ok = sc_frame_buffer_push(&screen->fb, frame, &previous_skipped);
|
||||||
void *userdata) {
|
if (!ok) {
|
||||||
(void) vb;
|
return false;
|
||||||
struct sc_screen *screen = userdata;
|
}
|
||||||
|
|
||||||
// event_failed implies previous_skipped (the previous frame may not have
|
|
||||||
// been consumed if the event was not sent)
|
|
||||||
assert(!screen->event_failed || previous_skipped);
|
|
||||||
|
|
||||||
bool need_new_event;
|
|
||||||
if (previous_skipped) {
|
if (previous_skipped) {
|
||||||
sc_fps_counter_add_skipped_frame(&screen->fps_counter);
|
sc_fps_counter_add_skipped_frame(&screen->fps_counter);
|
||||||
// The SC_EVENT_NEW_FRAME triggered for the previous frame will consume
|
// The SC_EVENT_NEW_FRAME triggered for the previous frame will consume
|
||||||
// this new frame instead, unless the previous event failed
|
// this new frame instead
|
||||||
need_new_event = screen->event_failed;
|
|
||||||
} else {
|
} else {
|
||||||
need_new_event = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (need_new_event) {
|
|
||||||
static SDL_Event new_frame_event = {
|
static SDL_Event new_frame_event = {
|
||||||
.type = SC_EVENT_NEW_FRAME,
|
.type = SC_EVENT_NEW_FRAME,
|
||||||
};
|
};
|
||||||
@ -387,11 +378,11 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
|
|||||||
int ret = SDL_PushEvent(&new_frame_event);
|
int ret = SDL_PushEvent(&new_frame_event);
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
LOGW("Could not post new frame event: %s", SDL_GetError());
|
LOGW("Could not post new frame event: %s", SDL_GetError());
|
||||||
screen->event_failed = true;
|
return false;
|
||||||
} else {
|
|
||||||
screen->event_failed = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
@ -401,7 +392,6 @@ sc_screen_init(struct sc_screen *screen,
|
|||||||
screen->has_frame = false;
|
screen->has_frame = false;
|
||||||
screen->fullscreen = false;
|
screen->fullscreen = false;
|
||||||
screen->maximized = false;
|
screen->maximized = false;
|
||||||
screen->event_failed = false;
|
|
||||||
screen->mouse_capture_key_pressed = 0;
|
screen->mouse_capture_key_pressed = 0;
|
||||||
|
|
||||||
screen->req.x = params->window_x;
|
screen->req.x = params->window_x;
|
||||||
@ -411,23 +401,13 @@ sc_screen_init(struct sc_screen *screen,
|
|||||||
screen->req.fullscreen = params->fullscreen;
|
screen->req.fullscreen = params->fullscreen;
|
||||||
screen->req.start_fps_counter = params->start_fps_counter;
|
screen->req.start_fps_counter = params->start_fps_counter;
|
||||||
|
|
||||||
static const struct sc_video_buffer_callbacks cbs = {
|
bool ok = sc_frame_buffer_init(&screen->fb);
|
||||||
.on_new_frame = sc_video_buffer_on_new_frame,
|
|
||||||
};
|
|
||||||
|
|
||||||
bool ok = sc_video_buffer_init(&screen->vb, params->buffering_time, &cbs,
|
|
||||||
screen);
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = sc_video_buffer_start(&screen->vb);
|
|
||||||
if (!ok) {
|
|
||||||
goto error_destroy_video_buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sc_fps_counter_init(&screen->fps_counter)) {
|
if (!sc_fps_counter_init(&screen->fps_counter)) {
|
||||||
goto error_stop_and_join_video_buffer;
|
goto error_destroy_frame_buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
screen->frame_size = params->frame_size;
|
screen->frame_size = params->frame_size;
|
||||||
@ -559,11 +539,8 @@ error_destroy_window:
|
|||||||
SDL_DestroyWindow(screen->window);
|
SDL_DestroyWindow(screen->window);
|
||||||
error_destroy_fps_counter:
|
error_destroy_fps_counter:
|
||||||
sc_fps_counter_destroy(&screen->fps_counter);
|
sc_fps_counter_destroy(&screen->fps_counter);
|
||||||
error_stop_and_join_video_buffer:
|
error_destroy_frame_buffer:
|
||||||
sc_video_buffer_stop(&screen->vb);
|
sc_frame_buffer_destroy(&screen->fb);
|
||||||
sc_video_buffer_join(&screen->vb);
|
|
||||||
error_destroy_video_buffer:
|
|
||||||
sc_video_buffer_destroy(&screen->vb);
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -600,13 +577,11 @@ sc_screen_hide_window(struct sc_screen *screen) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
sc_screen_interrupt(struct sc_screen *screen) {
|
sc_screen_interrupt(struct sc_screen *screen) {
|
||||||
sc_video_buffer_stop(&screen->vb);
|
|
||||||
sc_fps_counter_interrupt(&screen->fps_counter);
|
sc_fps_counter_interrupt(&screen->fps_counter);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_screen_join(struct sc_screen *screen) {
|
sc_screen_join(struct sc_screen *screen) {
|
||||||
sc_video_buffer_join(&screen->vb);
|
|
||||||
sc_fps_counter_join(&screen->fps_counter);
|
sc_fps_counter_join(&screen->fps_counter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -620,7 +595,7 @@ sc_screen_destroy(struct sc_screen *screen) {
|
|||||||
SDL_DestroyRenderer(screen->renderer);
|
SDL_DestroyRenderer(screen->renderer);
|
||||||
SDL_DestroyWindow(screen->window);
|
SDL_DestroyWindow(screen->window);
|
||||||
sc_fps_counter_destroy(&screen->fps_counter);
|
sc_fps_counter_destroy(&screen->fps_counter);
|
||||||
sc_video_buffer_destroy(&screen->vb);
|
sc_frame_buffer_destroy(&screen->fb);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@ -726,7 +701,7 @@ update_texture(struct sc_screen *screen, const AVFrame *frame) {
|
|||||||
static bool
|
static bool
|
||||||
sc_screen_update_frame(struct sc_screen *screen) {
|
sc_screen_update_frame(struct sc_screen *screen) {
|
||||||
av_frame_unref(screen->frame);
|
av_frame_unref(screen->frame);
|
||||||
sc_video_buffer_consume(&screen->vb, screen->frame);
|
sc_frame_buffer_consume(&screen->fb, screen->frame);
|
||||||
AVFrame *frame = screen->frame;
|
AVFrame *frame = screen->frame;
|
||||||
|
|
||||||
sc_fps_counter_add_rendered_frame(&screen->fps_counter);
|
sc_fps_counter_add_rendered_frame(&screen->fps_counter);
|
||||||
|
@ -10,12 +10,12 @@
|
|||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "coords.h"
|
#include "coords.h"
|
||||||
#include "fps_counter.h"
|
#include "fps_counter.h"
|
||||||
|
#include "frame_buffer.h"
|
||||||
#include "input_manager.h"
|
#include "input_manager.h"
|
||||||
#include "opengl.h"
|
#include "opengl.h"
|
||||||
#include "trait/key_processor.h"
|
#include "trait/key_processor.h"
|
||||||
#include "trait/frame_sink.h"
|
#include "trait/frame_sink.h"
|
||||||
#include "trait/mouse_processor.h"
|
#include "trait/mouse_processor.h"
|
||||||
#include "video_buffer.h"
|
|
||||||
|
|
||||||
struct sc_screen {
|
struct sc_screen {
|
||||||
struct sc_frame_sink frame_sink; // frame sink trait
|
struct sc_frame_sink frame_sink; // frame sink trait
|
||||||
@ -25,7 +25,7 @@ struct sc_screen {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
struct sc_input_manager im;
|
struct sc_input_manager im;
|
||||||
struct sc_video_buffer vb;
|
struct sc_frame_buffer fb;
|
||||||
struct sc_fps_counter fps_counter;
|
struct sc_fps_counter fps_counter;
|
||||||
|
|
||||||
// The initial requested window properties
|
// The initial requested window properties
|
||||||
@ -59,8 +59,6 @@ struct sc_screen {
|
|||||||
bool maximized;
|
bool maximized;
|
||||||
bool mipmaps;
|
bool mipmaps;
|
||||||
|
|
||||||
bool event_failed; // in case SDL_PushEvent() returned an error
|
|
||||||
|
|
||||||
// To enable/disable mouse capture, a mouse capture key (LALT, LGUI or
|
// To enable/disable mouse capture, a mouse capture key (LALT, LGUI or
|
||||||
// RGUI) must be pressed. This variable tracks the pressed capture key.
|
// RGUI) must be pressed. This variable tracks the pressed capture key.
|
||||||
SDL_Keycode mouse_capture_key_pressed;
|
SDL_Keycode mouse_capture_key_pressed;
|
||||||
@ -95,8 +93,6 @@ struct sc_screen_params {
|
|||||||
|
|
||||||
bool fullscreen;
|
bool fullscreen;
|
||||||
bool start_fps_counter;
|
bool start_fps_counter;
|
||||||
|
|
||||||
sc_tick buffering_time;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// initialize screen, create window, renderer and texture (window is hidden)
|
// initialize screen, create window, renderer and texture (window is hidden)
|
||||||
|
120
app/src/server.c
120
app/src/server.c
@ -71,8 +71,10 @@ sc_server_params_destroy(struct sc_server_params *params) {
|
|||||||
// The server stores a copy of the params provided by the user
|
// The server stores a copy of the params provided by the user
|
||||||
free((char *) params->req_serial);
|
free((char *) params->req_serial);
|
||||||
free((char *) params->crop);
|
free((char *) params->crop);
|
||||||
free((char *) params->codec_options);
|
free((char *) params->video_codec_options);
|
||||||
free((char *) params->encoder_name);
|
free((char *) params->audio_codec_options);
|
||||||
|
free((char *) params->video_encoder);
|
||||||
|
free((char *) params->audio_encoder);
|
||||||
free((char *) params->tcpip_dst);
|
free((char *) params->tcpip_dst);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,8 +97,10 @@ sc_server_params_copy(struct sc_server_params *dst,
|
|||||||
|
|
||||||
COPY(req_serial);
|
COPY(req_serial);
|
||||||
COPY(crop);
|
COPY(crop);
|
||||||
COPY(codec_options);
|
COPY(video_codec_options);
|
||||||
COPY(encoder_name);
|
COPY(audio_codec_options);
|
||||||
|
COPY(video_encoder);
|
||||||
|
COPY(audio_encoder);
|
||||||
COPY(tcpip_dst);
|
COPY(tcpip_dst);
|
||||||
#undef COPY
|
#undef COPY
|
||||||
|
|
||||||
@ -165,6 +169,10 @@ sc_server_get_codec_name(enum sc_codec codec) {
|
|||||||
return "h265";
|
return "h265";
|
||||||
case SC_CODEC_AV1:
|
case SC_CODEC_AV1:
|
||||||
return "av1";
|
return "av1";
|
||||||
|
case SC_CODEC_OPUS:
|
||||||
|
return "opus";
|
||||||
|
case SC_CODEC_AAC:
|
||||||
|
return "aac";
|
||||||
default:
|
default:
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@ -215,10 +223,22 @@ execute_server(struct sc_server *server,
|
|||||||
|
|
||||||
ADD_PARAM("scid=%08x", params->scid);
|
ADD_PARAM("scid=%08x", params->scid);
|
||||||
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
|
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
|
||||||
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate);
|
|
||||||
|
|
||||||
if (params->codec != SC_CODEC_H264) {
|
if (params->video_bit_rate) {
|
||||||
ADD_PARAM("codec=%s", sc_server_get_codec_name(params->codec));
|
ADD_PARAM("video_bit_rate=%" PRIu32, params->video_bit_rate);
|
||||||
|
}
|
||||||
|
if (!params->audio) {
|
||||||
|
ADD_PARAM("audio=false");
|
||||||
|
} else if (params->audio_bit_rate) {
|
||||||
|
ADD_PARAM("audio_bit_rate=%" PRIu32, params->audio_bit_rate);
|
||||||
|
}
|
||||||
|
if (params->video_codec != SC_CODEC_H264) {
|
||||||
|
ADD_PARAM("video_codec=%s",
|
||||||
|
sc_server_get_codec_name(params->video_codec));
|
||||||
|
}
|
||||||
|
if (params->audio_codec != SC_CODEC_OPUS) {
|
||||||
|
ADD_PARAM("audio_codec=%s",
|
||||||
|
sc_server_get_codec_name(params->audio_codec));
|
||||||
}
|
}
|
||||||
if (params->max_size) {
|
if (params->max_size) {
|
||||||
ADD_PARAM("max_size=%" PRIu16, params->max_size);
|
ADD_PARAM("max_size=%" PRIu16, params->max_size);
|
||||||
@ -249,11 +269,17 @@ execute_server(struct sc_server *server,
|
|||||||
if (params->stay_awake) {
|
if (params->stay_awake) {
|
||||||
ADD_PARAM("stay_awake=true");
|
ADD_PARAM("stay_awake=true");
|
||||||
}
|
}
|
||||||
if (params->codec_options) {
|
if (params->video_codec_options) {
|
||||||
ADD_PARAM("codec_options=%s", params->codec_options);
|
ADD_PARAM("video_codec_options=%s", params->video_codec_options);
|
||||||
}
|
}
|
||||||
if (params->encoder_name) {
|
if (params->audio_codec_options) {
|
||||||
ADD_PARAM("encoder_name=%s", params->encoder_name);
|
ADD_PARAM("audio_codec_options=%s", params->audio_codec_options);
|
||||||
|
}
|
||||||
|
if (params->video_encoder) {
|
||||||
|
ADD_PARAM("video_encoder=%s", params->video_encoder);
|
||||||
|
}
|
||||||
|
if (params->audio_encoder) {
|
||||||
|
ADD_PARAM("audio_encoder=%s", params->audio_encoder);
|
||||||
}
|
}
|
||||||
if (params->power_off_on_close) {
|
if (params->power_off_on_close) {
|
||||||
ADD_PARAM("power_off_on_close=true");
|
ADD_PARAM("power_off_on_close=true");
|
||||||
@ -274,6 +300,12 @@ execute_server(struct sc_server *server,
|
|||||||
// By default, power_on is true
|
// By default, power_on is true
|
||||||
ADD_PARAM("power_on=false");
|
ADD_PARAM("power_on=false");
|
||||||
}
|
}
|
||||||
|
if (params->list_encoders) {
|
||||||
|
ADD_PARAM("list_encoders=true");
|
||||||
|
}
|
||||||
|
if (params->list_displays) {
|
||||||
|
ADD_PARAM("list_displays=true");
|
||||||
|
}
|
||||||
|
|
||||||
#undef ADD_PARAM
|
#undef ADD_PARAM
|
||||||
|
|
||||||
@ -388,6 +420,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
|
|||||||
server->stopped = false;
|
server->stopped = false;
|
||||||
|
|
||||||
server->video_socket = SC_SOCKET_NONE;
|
server->video_socket = SC_SOCKET_NONE;
|
||||||
|
server->audio_socket = SC_SOCKET_NONE;
|
||||||
server->control_socket = SC_SOCKET_NONE;
|
server->control_socket = SC_SOCKET_NONE;
|
||||||
|
|
||||||
sc_adb_tunnel_init(&server->tunnel);
|
sc_adb_tunnel_init(&server->tunnel);
|
||||||
@ -431,9 +464,11 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
|||||||
const char *serial = server->serial;
|
const char *serial = server->serial;
|
||||||
assert(serial);
|
assert(serial);
|
||||||
|
|
||||||
|
bool audio = server->params.audio;
|
||||||
bool control = server->params.control;
|
bool control = server->params.control;
|
||||||
|
|
||||||
sc_socket video_socket = SC_SOCKET_NONE;
|
sc_socket video_socket = SC_SOCKET_NONE;
|
||||||
|
sc_socket audio_socket = SC_SOCKET_NONE;
|
||||||
sc_socket control_socket = SC_SOCKET_NONE;
|
sc_socket control_socket = SC_SOCKET_NONE;
|
||||||
if (!tunnel->forward) {
|
if (!tunnel->forward) {
|
||||||
video_socket = net_accept_intr(&server->intr, tunnel->server_socket);
|
video_socket = net_accept_intr(&server->intr, tunnel->server_socket);
|
||||||
@ -441,6 +476,14 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
|||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (audio) {
|
||||||
|
audio_socket =
|
||||||
|
net_accept_intr(&server->intr, tunnel->server_socket);
|
||||||
|
if (audio_socket == SC_SOCKET_NONE) {
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (control) {
|
if (control) {
|
||||||
control_socket =
|
control_socket =
|
||||||
net_accept_intr(&server->intr, tunnel->server_socket);
|
net_accept_intr(&server->intr, tunnel->server_socket);
|
||||||
@ -467,6 +510,18 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
|||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (audio) {
|
||||||
|
audio_socket = net_socket();
|
||||||
|
if (audio_socket == SC_SOCKET_NONE) {
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
bool ok = net_connect_intr(&server->intr, audio_socket, tunnel_host,
|
||||||
|
tunnel_port);
|
||||||
|
if (!ok) {
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (control) {
|
if (control) {
|
||||||
// we know that the device is listening, we don't need several
|
// we know that the device is listening, we don't need several
|
||||||
// attempts
|
// attempts
|
||||||
@ -493,9 +548,11 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert(video_socket != SC_SOCKET_NONE);
|
assert(video_socket != SC_SOCKET_NONE);
|
||||||
|
assert(!audio || audio_socket != SC_SOCKET_NONE);
|
||||||
assert(!control || control_socket != SC_SOCKET_NONE);
|
assert(!control || control_socket != SC_SOCKET_NONE);
|
||||||
|
|
||||||
server->video_socket = video_socket;
|
server->video_socket = video_socket;
|
||||||
|
server->audio_socket = audio_socket;
|
||||||
server->control_socket = control_socket;
|
server->control_socket = control_socket;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -507,6 +564,12 @@ fail:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (audio_socket != SC_SOCKET_NONE) {
|
||||||
|
if (!net_close(audio_socket)) {
|
||||||
|
LOGW("Could not close audio socket");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (control_socket != SC_SOCKET_NONE) {
|
if (control_socket != SC_SOCKET_NONE) {
|
||||||
if (!net_close(control_socket)) {
|
if (!net_close(control_socket)) {
|
||||||
LOGW("Could not close control socket");
|
LOGW("Could not close control socket");
|
||||||
@ -791,6 +854,25 @@ run_server(void *data) {
|
|||||||
assert(serial);
|
assert(serial);
|
||||||
LOGD("Device serial: %s", serial);
|
LOGD("Device serial: %s", serial);
|
||||||
|
|
||||||
|
ok = push_server(&server->intr, serial);
|
||||||
|
if (!ok) {
|
||||||
|
goto error_connection_failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If --list-* is passed, then the server just prints the requested data
|
||||||
|
// then exits.
|
||||||
|
if (params->list_encoders || params->list_displays) {
|
||||||
|
sc_pid pid = execute_server(server, params);
|
||||||
|
if (pid == SC_PROCESS_NONE) {
|
||||||
|
goto error_connection_failed;
|
||||||
|
}
|
||||||
|
sc_process_wait(pid, NULL); // ignore exit code
|
||||||
|
sc_process_close(pid);
|
||||||
|
// Wake up await_for_server()
|
||||||
|
server->cbs->on_connected(server, server->cbs_userdata);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int r = asprintf(&server->device_socket_name, SC_SOCKET_NAME_PREFIX "%08x",
|
int r = asprintf(&server->device_socket_name, SC_SOCKET_NAME_PREFIX "%08x",
|
||||||
params->scid);
|
params->scid);
|
||||||
if (r == -1) {
|
if (r == -1) {
|
||||||
@ -800,11 +882,6 @@ run_server(void *data) {
|
|||||||
assert(r == sizeof(SC_SOCKET_NAME_PREFIX) - 1 + 8);
|
assert(r == sizeof(SC_SOCKET_NAME_PREFIX) - 1 + 8);
|
||||||
assert(server->device_socket_name);
|
assert(server->device_socket_name);
|
||||||
|
|
||||||
ok = push_server(&server->intr, serial);
|
|
||||||
if (!ok) {
|
|
||||||
goto error_connection_failed;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial,
|
ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial,
|
||||||
server->device_socket_name, params->port_range,
|
server->device_socket_name, params->port_range,
|
||||||
params->force_adb_forward);
|
params->force_adb_forward);
|
||||||
@ -857,6 +934,11 @@ run_server(void *data) {
|
|||||||
assert(server->video_socket != SC_SOCKET_NONE);
|
assert(server->video_socket != SC_SOCKET_NONE);
|
||||||
net_interrupt(server->video_socket);
|
net_interrupt(server->video_socket);
|
||||||
|
|
||||||
|
if (server->audio_socket != SC_SOCKET_NONE) {
|
||||||
|
// There is no audio_socket if --no-audio is set
|
||||||
|
net_interrupt(server->audio_socket);
|
||||||
|
}
|
||||||
|
|
||||||
if (server->control_socket != SC_SOCKET_NONE) {
|
if (server->control_socket != SC_SOCKET_NONE) {
|
||||||
// There is no control_socket if --no-control is set
|
// There is no control_socket if --no-control is set
|
||||||
net_interrupt(server->control_socket);
|
net_interrupt(server->control_socket);
|
||||||
@ -909,7 +991,10 @@ sc_server_stop(struct sc_server *server) {
|
|||||||
sc_cond_signal(&server->cond_stopped);
|
sc_cond_signal(&server->cond_stopped);
|
||||||
sc_intr_interrupt(&server->intr);
|
sc_intr_interrupt(&server->intr);
|
||||||
sc_mutex_unlock(&server->mutex);
|
sc_mutex_unlock(&server->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_server_join(struct sc_server *server) {
|
||||||
sc_thread_join(&server->thread, NULL);
|
sc_thread_join(&server->thread, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -918,6 +1003,9 @@ sc_server_destroy(struct sc_server *server) {
|
|||||||
if (server->video_socket != SC_SOCKET_NONE) {
|
if (server->video_socket != SC_SOCKET_NONE) {
|
||||||
net_close(server->video_socket);
|
net_close(server->video_socket);
|
||||||
}
|
}
|
||||||
|
if (server->audio_socket != SC_SOCKET_NONE) {
|
||||||
|
net_close(server->audio_socket);
|
||||||
|
}
|
||||||
if (server->control_socket != SC_SOCKET_NONE) {
|
if (server->control_socket != SC_SOCKET_NONE) {
|
||||||
net_close(server->control_socket);
|
net_close(server->control_socket);
|
||||||
}
|
}
|
||||||
|
@ -25,19 +25,24 @@ struct sc_server_params {
|
|||||||
uint32_t scid;
|
uint32_t scid;
|
||||||
const char *req_serial;
|
const char *req_serial;
|
||||||
enum sc_log_level log_level;
|
enum sc_log_level log_level;
|
||||||
enum sc_codec codec;
|
enum sc_codec video_codec;
|
||||||
|
enum sc_codec audio_codec;
|
||||||
const char *crop;
|
const char *crop;
|
||||||
const char *codec_options;
|
const char *video_codec_options;
|
||||||
const char *encoder_name;
|
const char *audio_codec_options;
|
||||||
|
const char *video_encoder;
|
||||||
|
const char *audio_encoder;
|
||||||
struct sc_port_range port_range;
|
struct sc_port_range port_range;
|
||||||
uint32_t tunnel_host;
|
uint32_t tunnel_host;
|
||||||
uint16_t tunnel_port;
|
uint16_t tunnel_port;
|
||||||
uint16_t max_size;
|
uint16_t max_size;
|
||||||
uint32_t bit_rate;
|
uint32_t video_bit_rate;
|
||||||
|
uint32_t audio_bit_rate;
|
||||||
uint16_t max_fps;
|
uint16_t max_fps;
|
||||||
int8_t lock_video_orientation;
|
int8_t lock_video_orientation;
|
||||||
bool control;
|
bool control;
|
||||||
uint32_t display_id;
|
uint32_t display_id;
|
||||||
|
bool audio;
|
||||||
bool show_touches;
|
bool show_touches;
|
||||||
bool stay_awake;
|
bool stay_awake;
|
||||||
bool force_adb_forward;
|
bool force_adb_forward;
|
||||||
@ -50,6 +55,8 @@ struct sc_server_params {
|
|||||||
bool select_tcpip;
|
bool select_tcpip;
|
||||||
bool cleanup;
|
bool cleanup;
|
||||||
bool power_on;
|
bool power_on;
|
||||||
|
bool list_encoders;
|
||||||
|
bool list_displays;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct sc_server {
|
struct sc_server {
|
||||||
@ -69,6 +76,7 @@ struct sc_server {
|
|||||||
struct sc_adb_tunnel tunnel;
|
struct sc_adb_tunnel tunnel;
|
||||||
|
|
||||||
sc_socket video_socket;
|
sc_socket video_socket;
|
||||||
|
sc_socket audio_socket;
|
||||||
sc_socket control_socket;
|
sc_socket control_socket;
|
||||||
|
|
||||||
const struct sc_server_callbacks *cbs;
|
const struct sc_server_callbacks *cbs;
|
||||||
@ -108,6 +116,10 @@ sc_server_start(struct sc_server *server);
|
|||||||
void
|
void
|
||||||
sc_server_stop(struct sc_server *server);
|
sc_server_stop(struct sc_server *server);
|
||||||
|
|
||||||
|
// join the server thread
|
||||||
|
void
|
||||||
|
sc_server_join(struct sc_server *server);
|
||||||
|
|
||||||
// close and release sockets
|
// close and release sockets
|
||||||
void
|
void
|
||||||
sc_server_destroy(struct sc_server *server);
|
sc_server_destroy(struct sc_server *server);
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <libavcodec/avcodec.h>
|
||||||
|
|
||||||
typedef struct AVFrame AVFrame;
|
typedef struct AVFrame AVFrame;
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ struct sc_frame_sink {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct sc_frame_sink_ops {
|
struct sc_frame_sink_ops {
|
||||||
bool (*open)(struct sc_frame_sink *sink);
|
bool (*open)(struct sc_frame_sink *sink, const AVCodecContext *ctx);
|
||||||
void (*close)(struct sc_frame_sink *sink);
|
void (*close)(struct sc_frame_sink *sink);
|
||||||
bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame);
|
bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame);
|
||||||
};
|
};
|
||||||
|
59
app/src/trait/frame_source.c
Normal file
59
app/src/trait/frame_source.c
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
#include "frame_source.h"
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_frame_source_init(struct sc_frame_source *source) {
|
||||||
|
source->sink_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_frame_source_add_sink(struct sc_frame_source *source,
|
||||||
|
struct sc_frame_sink *sink) {
|
||||||
|
assert(source->sink_count < SC_FRAME_SOURCE_MAX_SINKS);
|
||||||
|
assert(sink);
|
||||||
|
assert(sink->ops);
|
||||||
|
source->sinks[source->sink_count++] = sink;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_frame_source_sinks_close_firsts(struct sc_frame_source *source,
|
||||||
|
unsigned count) {
|
||||||
|
while (count) {
|
||||||
|
struct sc_frame_sink *sink = source->sinks[--count];
|
||||||
|
sink->ops->close(sink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_frame_source_sinks_open(struct sc_frame_source *source,
|
||||||
|
const AVCodecContext *ctx) {
|
||||||
|
assert(source->sink_count);
|
||||||
|
for (unsigned i = 0; i < source->sink_count; ++i) {
|
||||||
|
struct sc_frame_sink *sink = source->sinks[i];
|
||||||
|
if (!sink->ops->open(sink, ctx)) {
|
||||||
|
sc_frame_source_sinks_close_firsts(source, i);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_frame_source_sinks_close(struct sc_frame_source *source) {
|
||||||
|
assert(source->sink_count);
|
||||||
|
sc_frame_source_sinks_close_firsts(source, source->sink_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_frame_source_sinks_push(struct sc_frame_source *source,
|
||||||
|
const AVFrame *frame) {
|
||||||
|
assert(source->sink_count);
|
||||||
|
for (unsigned i = 0; i < source->sink_count; ++i) {
|
||||||
|
struct sc_frame_sink *sink = source->sinks[i];
|
||||||
|
if (!sink->ops->push(sink, frame)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
38
app/src/trait/frame_source.h
Normal file
38
app/src/trait/frame_source.h
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#ifndef SC_FRAME_SOURCE_H
|
||||||
|
#define SC_FRAME_SOURCE_H
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include "frame_sink.h"
|
||||||
|
|
||||||
|
#define SC_FRAME_SOURCE_MAX_SINKS 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frame source trait
|
||||||
|
*
|
||||||
|
* Component able to send AVFrames should implement this trait.
|
||||||
|
*/
|
||||||
|
struct sc_frame_source {
|
||||||
|
struct sc_frame_sink *sinks[SC_FRAME_SOURCE_MAX_SINKS];
|
||||||
|
unsigned sink_count;
|
||||||
|
};
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_frame_source_init(struct sc_frame_source *source);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_frame_source_add_sink(struct sc_frame_source *source,
|
||||||
|
struct sc_frame_sink *sink);
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_frame_source_sinks_open(struct sc_frame_source *source,
|
||||||
|
const AVCodecContext *ctx);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_frame_source_sinks_close(struct sc_frame_source *source);
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_frame_source_sinks_push(struct sc_frame_source *source,
|
||||||
|
const AVFrame *frame);
|
||||||
|
|
||||||
|
#endif
|
@ -19,9 +19,20 @@ struct sc_packet_sink {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct sc_packet_sink_ops {
|
struct sc_packet_sink_ops {
|
||||||
|
/* The codec instance is static, it is valid until the end of the program */
|
||||||
bool (*open)(struct sc_packet_sink *sink, const AVCodec *codec);
|
bool (*open)(struct sc_packet_sink *sink, const AVCodec *codec);
|
||||||
void (*close)(struct sc_packet_sink *sink);
|
void (*close)(struct sc_packet_sink *sink);
|
||||||
bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet);
|
bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet);
|
||||||
|
|
||||||
|
/*/
|
||||||
|
* Called when the input stream has been disabled at runtime.
|
||||||
|
*
|
||||||
|
* If it is called, then open(), close() and push() will never be called.
|
||||||
|
*
|
||||||
|
* It is useful to notify the recorder that the requested audio stream has
|
||||||
|
* finally been disabled because the device could not capture it.
|
||||||
|
*/
|
||||||
|
void (*disable)(struct sc_packet_sink *sink);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
70
app/src/trait/packet_source.c
Normal file
70
app/src/trait/packet_source.c
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
#include "packet_source.h"
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_packet_source_init(struct sc_packet_source *source) {
|
||||||
|
source->sink_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_packet_source_add_sink(struct sc_packet_source *source,
|
||||||
|
struct sc_packet_sink *sink) {
|
||||||
|
assert(source->sink_count < SC_PACKET_SOURCE_MAX_SINKS);
|
||||||
|
assert(sink);
|
||||||
|
assert(sink->ops);
|
||||||
|
source->sinks[source->sink_count++] = sink;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_packet_source_sinks_close_firsts(struct sc_packet_source *source,
|
||||||
|
unsigned count) {
|
||||||
|
while (count) {
|
||||||
|
struct sc_packet_sink *sink = source->sinks[--count];
|
||||||
|
sink->ops->close(sink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_packet_source_sinks_open(struct sc_packet_source *source,
|
||||||
|
const AVCodec *codec) {
|
||||||
|
assert(source->sink_count);
|
||||||
|
for (unsigned i = 0; i < source->sink_count; ++i) {
|
||||||
|
struct sc_packet_sink *sink = source->sinks[i];
|
||||||
|
if (!sink->ops->open(sink, codec)) {
|
||||||
|
sc_packet_source_sinks_close_firsts(source, i);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_packet_source_sinks_close(struct sc_packet_source *source) {
|
||||||
|
assert(source->sink_count);
|
||||||
|
sc_packet_source_sinks_close_firsts(source, source->sink_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_packet_source_sinks_push(struct sc_packet_source *source,
|
||||||
|
const AVPacket *packet) {
|
||||||
|
assert(source->sink_count);
|
||||||
|
for (unsigned i = 0; i < source->sink_count; ++i) {
|
||||||
|
struct sc_packet_sink *sink = source->sinks[i];
|
||||||
|
if (!sink->ops->push(sink, packet)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_packet_source_sinks_disable(struct sc_packet_source *source) {
|
||||||
|
assert(source->sink_count);
|
||||||
|
for (unsigned i = 0; i < source->sink_count; ++i) {
|
||||||
|
struct sc_packet_sink *sink = source->sinks[i];
|
||||||
|
if (sink->ops->disable) {
|
||||||
|
sink->ops->disable(sink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
app/src/trait/packet_source.h
Normal file
41
app/src/trait/packet_source.h
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#ifndef SC_PACKET_SOURCE_H
|
||||||
|
#define SC_PACKET_SOURCE_H
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include "packet_sink.h"
|
||||||
|
|
||||||
|
#define SC_PACKET_SOURCE_MAX_SINKS 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Packet source trait
|
||||||
|
*
|
||||||
|
* Component able to send AVPackets should implement this trait.
|
||||||
|
*/
|
||||||
|
struct sc_packet_source {
|
||||||
|
struct sc_packet_sink *sinks[SC_PACKET_SOURCE_MAX_SINKS];
|
||||||
|
unsigned sink_count;
|
||||||
|
};
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_packet_source_init(struct sc_packet_source *source);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_packet_source_add_sink(struct sc_packet_source *source,
|
||||||
|
struct sc_packet_sink *sink);
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_packet_source_sinks_open(struct sc_packet_source *source,
|
||||||
|
const AVCodec *codec);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_packet_source_sinks_close(struct sc_packet_source *source);
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_packet_source_sinks_push(struct sc_packet_source *source,
|
||||||
|
const AVPacket *packet);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_packet_source_sinks_disable(struct sc_packet_source *source);
|
||||||
|
|
||||||
|
#endif
|
@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
#define DEFAULT_TIMEOUT 1000
|
#define DEFAULT_TIMEOUT 1000
|
||||||
|
|
||||||
|
#define SC_HID_EVENT_QUEUE_MAX 64
|
||||||
|
|
||||||
static void
|
static void
|
||||||
sc_hid_event_log(const struct sc_hid_event *event) {
|
sc_hid_event_log(const struct sc_hid_event *event) {
|
||||||
// HID Event: [00] FF FF FF FF...
|
// HID Event: [00] FF FF FF FF...
|
||||||
@ -48,14 +50,20 @@ sc_hid_event_destroy(struct sc_hid_event *hid_event) {
|
|||||||
bool
|
bool
|
||||||
sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb,
|
sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb,
|
||||||
struct sc_acksync *acksync) {
|
struct sc_acksync *acksync) {
|
||||||
cbuf_init(&aoa->queue);
|
sc_vecdeque_init(&aoa->queue);
|
||||||
|
|
||||||
|
if (!sc_vecdeque_reserve(&aoa->queue, SC_HID_EVENT_QUEUE_MAX)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!sc_mutex_init(&aoa->mutex)) {
|
if (!sc_mutex_init(&aoa->mutex)) {
|
||||||
|
sc_vecdeque_destroy(&aoa->queue);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sc_cond_init(&aoa->event_cond)) {
|
if (!sc_cond_init(&aoa->event_cond)) {
|
||||||
sc_mutex_destroy(&aoa->mutex);
|
sc_mutex_destroy(&aoa->mutex);
|
||||||
|
sc_vecdeque_destroy(&aoa->queue);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,9 +77,10 @@ sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb,
|
|||||||
void
|
void
|
||||||
sc_aoa_destroy(struct sc_aoa *aoa) {
|
sc_aoa_destroy(struct sc_aoa *aoa) {
|
||||||
// Destroy remaining events
|
// Destroy remaining events
|
||||||
struct sc_hid_event event;
|
while (!sc_vecdeque_is_empty(&aoa->queue)) {
|
||||||
while (cbuf_take(&aoa->queue, &event)) {
|
struct sc_hid_event *event = sc_vecdeque_popref(&aoa->queue);
|
||||||
sc_hid_event_destroy(&event);
|
assert(event);
|
||||||
|
sc_hid_event_destroy(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
sc_cond_destroy(&aoa->event_cond);
|
sc_cond_destroy(&aoa->event_cond);
|
||||||
@ -212,13 +221,19 @@ sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sc_mutex_lock(&aoa->mutex);
|
sc_mutex_lock(&aoa->mutex);
|
||||||
bool was_empty = cbuf_is_empty(&aoa->queue);
|
bool full = sc_vecdeque_is_full(&aoa->queue);
|
||||||
bool res = cbuf_push(&aoa->queue, *event);
|
if (!full) {
|
||||||
if (was_empty) {
|
bool was_empty = sc_vecdeque_is_empty(&aoa->queue);
|
||||||
sc_cond_signal(&aoa->event_cond);
|
sc_vecdeque_push_noresize(&aoa->queue, *event);
|
||||||
|
if (was_empty) {
|
||||||
|
sc_cond_signal(&aoa->event_cond);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Otherwise (if the queue is full), the event is discarded
|
||||||
|
|
||||||
sc_mutex_unlock(&aoa->mutex);
|
sc_mutex_unlock(&aoa->mutex);
|
||||||
return res;
|
|
||||||
|
return !full;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
@ -227,7 +242,7 @@ run_aoa_thread(void *data) {
|
|||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
sc_mutex_lock(&aoa->mutex);
|
sc_mutex_lock(&aoa->mutex);
|
||||||
while (!aoa->stopped && cbuf_is_empty(&aoa->queue)) {
|
while (!aoa->stopped && sc_vecdeque_is_empty(&aoa->queue)) {
|
||||||
sc_cond_wait(&aoa->event_cond, &aoa->mutex);
|
sc_cond_wait(&aoa->event_cond, &aoa->mutex);
|
||||||
}
|
}
|
||||||
if (aoa->stopped) {
|
if (aoa->stopped) {
|
||||||
@ -235,11 +250,9 @@ run_aoa_thread(void *data) {
|
|||||||
sc_mutex_unlock(&aoa->mutex);
|
sc_mutex_unlock(&aoa->mutex);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
struct sc_hid_event event;
|
|
||||||
bool non_empty = cbuf_take(&aoa->queue, &event);
|
|
||||||
assert(non_empty);
|
|
||||||
(void) non_empty;
|
|
||||||
|
|
||||||
|
assert(!sc_vecdeque_is_empty(&aoa->queue));
|
||||||
|
struct sc_hid_event event = sc_vecdeque_pop(&aoa->queue);
|
||||||
uint64_t ack_to_wait = event.ack_to_wait;
|
uint64_t ack_to_wait = event.ack_to_wait;
|
||||||
sc_mutex_unlock(&aoa->mutex);
|
sc_mutex_unlock(&aoa->mutex);
|
||||||
|
|
||||||
|
@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
#include "usb.h"
|
#include "usb.h"
|
||||||
#include "util/acksync.h"
|
#include "util/acksync.h"
|
||||||
#include "util/cbuf.h"
|
|
||||||
#include "util/thread.h"
|
#include "util/thread.h"
|
||||||
#include "util/tick.h"
|
#include "util/tick.h"
|
||||||
|
#include "util/vecdeque.h"
|
||||||
|
|
||||||
struct sc_hid_event {
|
struct sc_hid_event {
|
||||||
uint16_t accessory_id;
|
uint16_t accessory_id;
|
||||||
@ -27,7 +27,7 @@ sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id,
|
|||||||
void
|
void
|
||||||
sc_hid_event_destroy(struct sc_hid_event *hid_event);
|
sc_hid_event_destroy(struct sc_hid_event *hid_event);
|
||||||
|
|
||||||
struct sc_hid_event_queue CBUF(struct sc_hid_event, 64);
|
struct sc_hid_event_queue SC_VECDEQUE(struct sc_hid_event);
|
||||||
|
|
||||||
struct sc_aoa {
|
struct sc_aoa {
|
||||||
struct sc_usb *usb;
|
struct sc_usb *usb;
|
||||||
|
26
app/src/util/average.c
Normal file
26
app/src/util/average.c
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#include "average.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_average_init(struct sc_average *avg, unsigned range) {
|
||||||
|
avg->range = range;
|
||||||
|
avg->avg = 0;
|
||||||
|
avg->count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_average_push(struct sc_average *avg, float value) {
|
||||||
|
if (avg->count < avg->range) {
|
||||||
|
++avg->count;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(avg->count);
|
||||||
|
avg->avg = ((avg->count - 1) * avg->avg + value) / avg->count;
|
||||||
|
}
|
||||||
|
|
||||||
|
float
|
||||||
|
sc_average_get(struct sc_average *avg) {
|
||||||
|
assert(avg->count);
|
||||||
|
return avg->avg;
|
||||||
|
}
|
40
app/src/util/average.h
Normal file
40
app/src/util/average.h
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#ifndef SC_AVERAGE
|
||||||
|
#define SC_AVERAGE
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
struct sc_average {
|
||||||
|
// Current average value
|
||||||
|
float avg;
|
||||||
|
|
||||||
|
// Target range, to update the average as follow:
|
||||||
|
// avg = ((range - 1) * avg + new_value) / range
|
||||||
|
unsigned range;
|
||||||
|
|
||||||
|
// Number of values pushed when less than range (count <= range).
|
||||||
|
// The purpose is to handle the first (range - 1) values properly.
|
||||||
|
unsigned count;
|
||||||
|
};
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_average_init(struct sc_average *avg, unsigned range);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push a new value to update the "rolling" average
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
sc_average_push(struct sc_average *avg, float value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current average value
|
||||||
|
*
|
||||||
|
* It is an error to call this function if sc_average_push() has not been
|
||||||
|
* called at least once.
|
||||||
|
*/
|
||||||
|
float
|
||||||
|
sc_average_get(struct sc_average *avg);
|
||||||
|
|
||||||
|
#endif
|
105
app/src/util/bytebuf.c
Normal file
105
app/src/util/bytebuf.c
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
#include "bytebuf.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "util/log.h"
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size) {
|
||||||
|
assert(alloc_size);
|
||||||
|
// sufficient, but use more for alignment.
|
||||||
|
buf->data = malloc(alloc_size);
|
||||||
|
if (!buf->data) {
|
||||||
|
LOG_OOM();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf->alloc_size = alloc_size;
|
||||||
|
buf->head = 0;
|
||||||
|
buf->tail = 0;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_bytebuf_destroy(struct sc_bytebuf *buf) {
|
||||||
|
free(buf->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) {
|
||||||
|
assert(len);
|
||||||
|
assert(len <= sc_bytebuf_read_available(buf));
|
||||||
|
assert(buf->tail != buf->head); // the buffer could not be empty
|
||||||
|
|
||||||
|
size_t right_limit = buf->tail < buf->head ? buf->head : buf->alloc_size;
|
||||||
|
size_t right_len = right_limit - buf->tail;
|
||||||
|
if (len < right_len) {
|
||||||
|
right_len = len;
|
||||||
|
}
|
||||||
|
memcpy(to, buf->data + buf->tail, right_len);
|
||||||
|
|
||||||
|
if (len > right_len) {
|
||||||
|
memcpy(to + right_len, buf->data, len - right_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
buf->tail = (buf->tail + len) % buf->alloc_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len) {
|
||||||
|
assert(len);
|
||||||
|
assert(len <= sc_bytebuf_read_available(buf));
|
||||||
|
assert(buf->tail != buf->head); // the buffer could not be empty
|
||||||
|
|
||||||
|
buf->tail = (buf->tail + len) % buf->alloc_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
sc_bytebuf_write_step0(struct sc_bytebuf *buf, const uint8_t *from,
|
||||||
|
size_t len) {
|
||||||
|
size_t right_len = buf->alloc_size - buf->head;
|
||||||
|
if (len < right_len) {
|
||||||
|
right_len = len;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(buf->data + buf->head, from, right_len);
|
||||||
|
if (len > right_len) {
|
||||||
|
memcpy(buf->data, from + right_len, len - right_len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
sc_bytebuf_write_step1(struct sc_bytebuf *buf, size_t len) {
|
||||||
|
buf->head = (buf->head + len) % buf->alloc_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) {
|
||||||
|
assert(len);
|
||||||
|
assert(len <= sc_bytebuf_write_available(buf));
|
||||||
|
|
||||||
|
sc_bytebuf_write_step0(buf, from, len);
|
||||||
|
sc_bytebuf_write_step1(buf, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
|
||||||
|
size_t len) {
|
||||||
|
// *This function MUST NOT access buf->tail (even in assert()).*
|
||||||
|
// The purpose of this function is to allow a reader and a writer to access
|
||||||
|
// different parts of the buffer in parallel simultaneously. It is intended
|
||||||
|
// to be called without lock (only sc_bytebuf_commit_write() is intended to
|
||||||
|
// be called with lock held).
|
||||||
|
|
||||||
|
assert(len < buf->alloc_size - 1);
|
||||||
|
sc_bytebuf_write_step0(buf, from, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len) {
|
||||||
|
assert(len <= sc_bytebuf_write_available(buf));
|
||||||
|
sc_bytebuf_write_step1(buf, len);
|
||||||
|
}
|
116
app/src/util/bytebuf.h
Normal file
116
app/src/util/bytebuf.h
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
#ifndef SC_BYTEBUF_H
|
||||||
|
#define SC_BYTEBUF_H
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
struct sc_bytebuf {
|
||||||
|
uint8_t *data;
|
||||||
|
// The actual capacity is (allocated - 1) so that head == tail is
|
||||||
|
// non-ambiguous
|
||||||
|
size_t alloc_size;
|
||||||
|
size_t head; // writter cursor
|
||||||
|
size_t tail; // reader cursor
|
||||||
|
// empty: tail == head
|
||||||
|
// full: (tail + 1) % allocated == head
|
||||||
|
};
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy from the bytebuf to a user-provided array
|
||||||
|
*
|
||||||
|
* The caller must check that len <= sc_bytebuf_read_available() (it is an
|
||||||
|
* error to attempt to read more bytes than available).
|
||||||
|
*
|
||||||
|
* This function is guaranteed not to write to buf->head.
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop len bytes from the buffer
|
||||||
|
*
|
||||||
|
* The caller must check that len <= sc_bytebuf_read_available() (it is an
|
||||||
|
* error to attempt to skip more bytes than available).
|
||||||
|
*
|
||||||
|
* This function is guaranteed not to change the head.
|
||||||
|
*
|
||||||
|
* This function is guaranteed to not change the head.
|
||||||
|
*
|
||||||
|
* It is equivalent to call sc_bytebuf_read() to some array and discard the
|
||||||
|
* array (but more efficient since there is no copy).
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy the user-provided array to the bytebuf
|
||||||
|
*
|
||||||
|
* The caller must check that len <= sc_bytebuf_write_available() (it is an
|
||||||
|
* error to write more bytes than the remaining available space).
|
||||||
|
*
|
||||||
|
* This function is guaranteed not to write to buf->tail.
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy the user-provided array to the bytebuf, but do not advance the cursor
|
||||||
|
*
|
||||||
|
* The caller must check that len <= sc_bytebuf_write_available() (it is an
|
||||||
|
* error to write more bytes than the remaining available space).
|
||||||
|
*
|
||||||
|
* After this function is called, the write must be committed with
|
||||||
|
* sc_bytebuf_commit_write().
|
||||||
|
*
|
||||||
|
* The purpose of this mechanism is to acquire a lock only to commit the write,
|
||||||
|
* but not to perform the actual copy.
|
||||||
|
*
|
||||||
|
* This function is guaranteed not to access buf->tail.
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
|
||||||
|
size_t len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit a prepared write
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the number of bytes which can be read
|
||||||
|
*
|
||||||
|
* It is an error to read more bytes than available.
|
||||||
|
*/
|
||||||
|
static inline size_t
|
||||||
|
sc_bytebuf_read_available(struct sc_bytebuf *buf) {
|
||||||
|
return (buf->alloc_size + buf->head - buf->tail) % buf->alloc_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the number of bytes which can be written
|
||||||
|
*
|
||||||
|
* It is an error to write more bytes than available.
|
||||||
|
*/
|
||||||
|
static inline size_t
|
||||||
|
sc_bytebuf_write_available(struct sc_bytebuf *buf) {
|
||||||
|
return (buf->alloc_size + buf->tail - buf->head - 1) % buf->alloc_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the actual capacity of the buffer (read available + write available)
|
||||||
|
*/
|
||||||
|
static inline size_t
|
||||||
|
sc_bytebuf_capacity(struct sc_bytebuf *buf) {
|
||||||
|
return buf->alloc_size - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_bytebuf_destroy(struct sc_bytebuf *buf);
|
||||||
|
|
||||||
|
#endif
|
@ -1,52 +0,0 @@
|
|||||||
// generic circular buffer (bounded queue) implementation
|
|
||||||
#ifndef SC_CBUF_H
|
|
||||||
#define SC_CBUF_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
// To define a circular buffer type of 20 ints:
|
|
||||||
// struct cbuf_int CBUF(int, 20);
|
|
||||||
//
|
|
||||||
// data has length CAP + 1 to distinguish empty vs full.
|
|
||||||
#define CBUF(TYPE, CAP) { \
|
|
||||||
TYPE data[(CAP) + 1]; \
|
|
||||||
size_t head; \
|
|
||||||
size_t tail; \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define cbuf_size_(PCBUF) \
|
|
||||||
(sizeof((PCBUF)->data) / sizeof(*(PCBUF)->data))
|
|
||||||
|
|
||||||
#define cbuf_is_empty(PCBUF) \
|
|
||||||
((PCBUF)->head == (PCBUF)->tail)
|
|
||||||
|
|
||||||
#define cbuf_is_full(PCBUF) \
|
|
||||||
(((PCBUF)->head + 1) % cbuf_size_(PCBUF) == (PCBUF)->tail)
|
|
||||||
|
|
||||||
#define cbuf_init(PCBUF) \
|
|
||||||
(void) ((PCBUF)->head = (PCBUF)->tail = 0)
|
|
||||||
|
|
||||||
#define cbuf_push(PCBUF, ITEM) \
|
|
||||||
({ \
|
|
||||||
bool ok = !cbuf_is_full(PCBUF); \
|
|
||||||
if (ok) { \
|
|
||||||
(PCBUF)->data[(PCBUF)->head] = (ITEM); \
|
|
||||||
(PCBUF)->head = ((PCBUF)->head + 1) % cbuf_size_(PCBUF); \
|
|
||||||
} \
|
|
||||||
ok; \
|
|
||||||
})
|
|
||||||
|
|
||||||
#define cbuf_take(PCBUF, PITEM) \
|
|
||||||
({ \
|
|
||||||
bool ok = !cbuf_is_empty(PCBUF); \
|
|
||||||
if (ok) { \
|
|
||||||
*(PITEM) = (PCBUF)->data[(PCBUF)->tail]; \
|
|
||||||
(PCBUF)->tail = ((PCBUF)->tail + 1) % cbuf_size_(PCBUF); \
|
|
||||||
} \
|
|
||||||
ok; \
|
|
||||||
})
|
|
||||||
|
|
||||||
#endif
|
|
@ -4,6 +4,7 @@
|
|||||||
# include <windows.h>
|
# include <windows.h>
|
||||||
#endif
|
#endif
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
#include <libavformat/avformat.h>
|
||||||
|
|
||||||
static SDL_LogPriority
|
static SDL_LogPriority
|
||||||
log_level_sc_to_sdl(enum sc_log_level level) {
|
log_level_sc_to_sdl(enum sc_log_level level) {
|
||||||
@ -47,6 +48,7 @@ void
|
|||||||
sc_set_log_level(enum sc_log_level level) {
|
sc_set_log_level(enum sc_log_level level) {
|
||||||
SDL_LogPriority sdl_log = log_level_sc_to_sdl(level);
|
SDL_LogPriority sdl_log = log_level_sc_to_sdl(level);
|
||||||
SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log);
|
SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log);
|
||||||
|
SDL_LogSetPriority(SDL_LOG_CATEGORY_CUSTOM, sdl_log);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum sc_log_level
|
enum sc_log_level
|
||||||
@ -85,3 +87,46 @@ sc_log_windows_error(const char *prefix, int error) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
static SDL_LogPriority
|
||||||
|
sdl_priority_from_av_level(int level) {
|
||||||
|
switch (level) {
|
||||||
|
case AV_LOG_PANIC:
|
||||||
|
case AV_LOG_FATAL:
|
||||||
|
return SDL_LOG_PRIORITY_CRITICAL;
|
||||||
|
case AV_LOG_ERROR:
|
||||||
|
return SDL_LOG_PRIORITY_ERROR;
|
||||||
|
case AV_LOG_WARNING:
|
||||||
|
return SDL_LOG_PRIORITY_WARN;
|
||||||
|
case AV_LOG_INFO:
|
||||||
|
return SDL_LOG_PRIORITY_INFO;
|
||||||
|
}
|
||||||
|
// do not forward others, which are too verbose
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
||||||
|
(void) avcl;
|
||||||
|
SDL_LogPriority priority = sdl_priority_from_av_level(level);
|
||||||
|
if (priority == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t fmt_len = strlen(fmt);
|
||||||
|
char *local_fmt = malloc(fmt_len + 10);
|
||||||
|
if (!local_fmt) {
|
||||||
|
LOG_OOM();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
memcpy(local_fmt, "[FFmpeg] ", 9); // do not write the final '\0'
|
||||||
|
memcpy(local_fmt + 9, fmt, fmt_len + 1); // include '\0'
|
||||||
|
SDL_LogMessageV(SDL_LOG_CATEGORY_CUSTOM, priority, local_fmt, vl);
|
||||||
|
free(local_fmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_log_configure() {
|
||||||
|
// Redirect FFmpeg logs to SDL logs
|
||||||
|
av_log_set_callback(sc_av_log_callback);
|
||||||
|
}
|
||||||
|
@ -35,4 +35,7 @@ bool
|
|||||||
sc_log_windows_error(const char *prefix, int error);
|
sc_log_windows_error(const char *prefix, int error);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_log_configure();
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
14
app/src/util/memory.c
Normal file
14
app/src/util/memory.c
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#include "memory.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
void *
|
||||||
|
sc_allocarray(size_t nmemb, size_t size) {
|
||||||
|
size_t bytes;
|
||||||
|
if (__builtin_mul_overflow(nmemb, size, &bytes)) {
|
||||||
|
errno = ENOMEM;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return malloc(bytes);
|
||||||
|
}
|
12
app/src/util/memory.h
Normal file
12
app/src/util/memory.h
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#ifndef SC_MEMORY_H
|
||||||
|
#define SC_MEMORY_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/* Like calloc(), but without initialization.
|
||||||
|
* Like reallocarray(), but without reallocation.
|
||||||
|
*/
|
||||||
|
void *
|
||||||
|
sc_allocarray(size_t nmemb, size_t size);
|
||||||
|
|
||||||
|
#endif
|
@ -1,77 +0,0 @@
|
|||||||
// generic intrusive FIFO queue
|
|
||||||
#ifndef SC_QUEUE_H
|
|
||||||
#define SC_QUEUE_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stddef.h>
|
|
||||||
|
|
||||||
// To define a queue type of "struct foo":
|
|
||||||
// struct queue_foo QUEUE(struct foo);
|
|
||||||
#define SC_QUEUE(TYPE) { \
|
|
||||||
TYPE *first; \
|
|
||||||
TYPE *last; \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define sc_queue_init(PQ) \
|
|
||||||
(void) ((PQ)->first = (PQ)->last = NULL)
|
|
||||||
|
|
||||||
#define sc_queue_is_empty(PQ) \
|
|
||||||
!(PQ)->first
|
|
||||||
|
|
||||||
// NEXTFIELD is the field in the ITEM type used for intrusive linked-list
|
|
||||||
//
|
|
||||||
// For example:
|
|
||||||
// struct foo {
|
|
||||||
// int value;
|
|
||||||
// struct foo *next;
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// // define the type "struct my_queue"
|
|
||||||
// struct my_queue SC_QUEUE(struct foo);
|
|
||||||
//
|
|
||||||
// struct my_queue queue;
|
|
||||||
// sc_queue_init(&queue);
|
|
||||||
//
|
|
||||||
// struct foo v1 = { .value = 42 };
|
|
||||||
// struct foo v2 = { .value = 27 };
|
|
||||||
//
|
|
||||||
// sc_queue_push(&queue, next, v1);
|
|
||||||
// sc_queue_push(&queue, next, v2);
|
|
||||||
//
|
|
||||||
// struct foo *foo;
|
|
||||||
// sc_queue_take(&queue, next, &foo);
|
|
||||||
// assert(foo->value == 42);
|
|
||||||
// sc_queue_take(&queue, next, &foo);
|
|
||||||
// assert(foo->value == 27);
|
|
||||||
// assert(sc_queue_is_empty(&queue));
|
|
||||||
//
|
|
||||||
|
|
||||||
// push a new item into the queue
|
|
||||||
#define sc_queue_push(PQ, NEXTFIELD, ITEM) \
|
|
||||||
(void) ({ \
|
|
||||||
(ITEM)->NEXTFIELD = NULL; \
|
|
||||||
if (sc_queue_is_empty(PQ)) { \
|
|
||||||
(PQ)->first = (PQ)->last = (ITEM); \
|
|
||||||
} else { \
|
|
||||||
(PQ)->last->NEXTFIELD = (ITEM); \
|
|
||||||
(PQ)->last = (ITEM); \
|
|
||||||
} \
|
|
||||||
})
|
|
||||||
|
|
||||||
// take the next item and remove it from the queue (the queue must not be empty)
|
|
||||||
// the result is stored in *(PITEM)
|
|
||||||
// (without typeof(), we could not store a local variable having the correct
|
|
||||||
// type so that we can "return" it)
|
|
||||||
#define sc_queue_take(PQ, NEXTFIELD, PITEM) \
|
|
||||||
(void) ({ \
|
|
||||||
assert(!sc_queue_is_empty(PQ)); \
|
|
||||||
*(PITEM) = (PQ)->first; \
|
|
||||||
(PQ)->first = (PQ)->first->NEXTFIELD; \
|
|
||||||
})
|
|
||||||
// no need to update (PQ)->last if the queue is left empty:
|
|
||||||
// (PQ)->last is undefined if !(PQ)->first anyway
|
|
||||||
|
|
||||||
#endif
|
|
379
app/src/util/vecdeque.h
Normal file
379
app/src/util/vecdeque.h
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
#ifndef SC_VECDEQUE_H
|
||||||
|
#define SC_VECDEQUE_H
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A double-ended queue implemented with a growable ring buffer.
|
||||||
|
*
|
||||||
|
* Inspired from the Rust VecDeque type:
|
||||||
|
* <https://doc.rust-lang.org/std/collections/struct.VecDeque.html>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VecDeque struct body
|
||||||
|
*
|
||||||
|
* A VecDeque is a dynamic ring-buffer, managed by the sc_vecdeque_* helpers.
|
||||||
|
*
|
||||||
|
* It is generic over the type of its items, so it is implemented via macros.
|
||||||
|
*
|
||||||
|
* To use a VecDeque, a new type must be defined:
|
||||||
|
*
|
||||||
|
* struct vecdeque_int SC_VECDEQUE(int);
|
||||||
|
*
|
||||||
|
* The struct may be anonymous:
|
||||||
|
*
|
||||||
|
* struct SC_VECDEQUE(const char *) names;
|
||||||
|
*
|
||||||
|
* Functions and macros having name ending with '_' are private.
|
||||||
|
*/
|
||||||
|
#define SC_VECDEQUE(type) { \
|
||||||
|
size_t cap; \
|
||||||
|
size_t origin; \
|
||||||
|
size_t size; \
|
||||||
|
type *data; \
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static initializer for a VecDeque
|
||||||
|
*/
|
||||||
|
#define SC_VECDEQUE_INITIALIZER { 0, 0, 0, NULL }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize an empty VecDeque
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_init(pv) \
|
||||||
|
({ \
|
||||||
|
(pv)->data = NULL; \
|
||||||
|
(pv)->cap = 0; \
|
||||||
|
(pv)->origin = 0; \
|
||||||
|
(pv)->size = 0; \
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy a VecDeque
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_destroy(pv) \
|
||||||
|
free((pv)->data)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear a VecDeque
|
||||||
|
*
|
||||||
|
* Remove all items.
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_clear(pv) \
|
||||||
|
(void) ({ \
|
||||||
|
sc_vecdeque_destroy(pv); \
|
||||||
|
sc_vecdeque_init(pv); \
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content size
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_size(pv) \
|
||||||
|
(pv)->size
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether the VecDeque is empty (i.e. its size is 0)
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_is_empty(pv) \
|
||||||
|
((pv)->size == 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether the VecDeque is full
|
||||||
|
*
|
||||||
|
* A VecDeque is full when its size equals its current capacity. However, it
|
||||||
|
* does not prevent to push a new item (with sc_vecdeque_push()), since this
|
||||||
|
* will increase its capacity.
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_is_full(pv) \
|
||||||
|
((pv)->size == (pv)->cap)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimal allocation size, in number of items
|
||||||
|
*
|
||||||
|
* Private.
|
||||||
|
*/
|
||||||
|
#define SC_VECDEQUE_MINCAP_ ((size_t) 10)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximal allocation size, in number of items
|
||||||
|
*
|
||||||
|
* Use SIZE_MAX/2 to fit in ssize_t, and so that cap*1.5 does not overflow.
|
||||||
|
*
|
||||||
|
* Private.
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_max_cap_(pv) (SIZE_MAX / 2 / sizeof(*(pv)->data))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Realloc the internal array to a specific capacity
|
||||||
|
*
|
||||||
|
* On reallocation success, update the VecDeque capacity (`*pcap`) and origin
|
||||||
|
* (`*porigin`), and return the reallocated data.
|
||||||
|
*
|
||||||
|
* On reallocation failure, return NULL without any change.
|
||||||
|
*
|
||||||
|
* Private.
|
||||||
|
*
|
||||||
|
* \param ptr the current `data` field of the SC_VECDEQUE to realloc
|
||||||
|
* \param newcap the requested capacity, in number of items
|
||||||
|
* \param item_size the size of one item (the generic type is unknown from this
|
||||||
|
* function)
|
||||||
|
* \param pcap a pointer to the `cap` field of the SC_VECDEQUE [IN/OUT]
|
||||||
|
* \param porigin a pointer to pv->origin (will be read and updated)
|
||||||
|
* \param size the `size` field of the SC_VECDEQUE
|
||||||
|
* \return the new array to assign to the `data` field of the SC_VECDEQUE (if
|
||||||
|
* not NULL)
|
||||||
|
*/
|
||||||
|
static inline void *
|
||||||
|
sc_vecdeque_reallocdata_(void *ptr, size_t newcap, size_t item_size,
|
||||||
|
size_t *pcap, size_t *porigin, size_t size) {
|
||||||
|
|
||||||
|
size_t oldcap = *pcap;
|
||||||
|
size_t oldorigin = *porigin;
|
||||||
|
|
||||||
|
assert(newcap > oldcap); // Could only grow
|
||||||
|
|
||||||
|
if (oldorigin + size <= oldcap) {
|
||||||
|
// The current content will stay in place, just realloc
|
||||||
|
//
|
||||||
|
// As an example, here is the content of a ring-buffer (oldcap=10)
|
||||||
|
// before the realloc:
|
||||||
|
//
|
||||||
|
// _ _ 2 3 4 5 6 7 _ _
|
||||||
|
// ^
|
||||||
|
// origin
|
||||||
|
//
|
||||||
|
// It is resized (newcap=15), e.g. with sc_vecdeque_reserve():
|
||||||
|
//
|
||||||
|
// _ _ 2 3 4 5 6 7 _ _ _ _ _ _ _
|
||||||
|
// ^
|
||||||
|
// origin
|
||||||
|
|
||||||
|
void *newptr = reallocarray(ptr, newcap, item_size);
|
||||||
|
if (!newptr) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
*pcap = newcap;
|
||||||
|
return newptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the current content to the new array
|
||||||
|
//
|
||||||
|
// As an example, here is the content of a ring-buffer (oldcap=10) before
|
||||||
|
// the realloc:
|
||||||
|
//
|
||||||
|
// 5 6 7 _ _ 0 1 2 3 4
|
||||||
|
// ^
|
||||||
|
// origin
|
||||||
|
//
|
||||||
|
// It is resized (newcap=15), e.g. with sc_vecdeque_reserve():
|
||||||
|
//
|
||||||
|
// 0 1 2 3 4 5 6 7 _ _ _ _ _ _ _
|
||||||
|
// ^
|
||||||
|
// origin
|
||||||
|
|
||||||
|
assert(size);
|
||||||
|
void *newptr = sc_allocarray(newcap, item_size);
|
||||||
|
if (!newptr) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t right_len = MIN(size, oldcap - oldorigin);
|
||||||
|
assert(right_len);
|
||||||
|
memcpy(newptr, ptr + (oldorigin * item_size), right_len * item_size);
|
||||||
|
|
||||||
|
if (size > right_len) {
|
||||||
|
memcpy(newptr + (right_len * item_size), ptr,
|
||||||
|
(size - right_len) * item_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(ptr);
|
||||||
|
|
||||||
|
*pcap = newcap;
|
||||||
|
*porigin = 0;
|
||||||
|
return newptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Macro to realloc the internal data to a new capacity
|
||||||
|
*
|
||||||
|
* Private.
|
||||||
|
*
|
||||||
|
* \retval true on success
|
||||||
|
* \retval false on allocation failure (the VecDeque is left untouched)
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_realloc_(pv, newcap) \
|
||||||
|
({ \
|
||||||
|
void *p = sc_vecdeque_reallocdata_((pv)->data, newcap, \
|
||||||
|
sizeof(*(pv)->data), &(pv)->cap, \
|
||||||
|
&(pv)->origin, (pv)->size); \
|
||||||
|
if (p) { \
|
||||||
|
(pv)->data = p; \
|
||||||
|
} \
|
||||||
|
(bool) p; \
|
||||||
|
});
|
||||||
|
|
||||||
|
static inline size_t
|
||||||
|
sc_vecdeque_growsize_(size_t value)
|
||||||
|
{
|
||||||
|
/* integer multiplication by 1.5 */
|
||||||
|
return value + (value >> 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increase the capacity of the VecDeque to at least `mincap`
|
||||||
|
*
|
||||||
|
* \param pv a pointer to the VecDeque
|
||||||
|
* \param mincap (`size_t`) the requested capacity
|
||||||
|
* \retval true on success
|
||||||
|
* \retval false on allocation failure (the VecDeque is left untouched)
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_reserve(pv, mincap) \
|
||||||
|
({ \
|
||||||
|
assert(mincap <= sc_vecdeque_max_cap_(pv)); \
|
||||||
|
bool ok; \
|
||||||
|
/* avoid to allocate tiny arrays (< SC_VECDEQUE_MINCAP_) */ \
|
||||||
|
size_t mincap_ = MAX(mincap, SC_VECDEQUE_MINCAP_); \
|
||||||
|
if (mincap_ <= (pv)->cap) { \
|
||||||
|
/* nothing to do */ \
|
||||||
|
ok = true; \
|
||||||
|
} else if (mincap_ <= sc_vecdeque_max_cap_(pv)) { \
|
||||||
|
/* not too big */ \
|
||||||
|
size_t newsize = sc_vecdeque_growsize_((pv)->cap); \
|
||||||
|
newsize = CLAMP(newsize, mincap_, sc_vecdeque_max_cap_(pv)); \
|
||||||
|
ok = sc_vecdeque_realloc_(pv, newsize); \
|
||||||
|
} else { \
|
||||||
|
ok = false; \
|
||||||
|
} \
|
||||||
|
ok; \
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically grow the VecDeque capacity
|
||||||
|
*
|
||||||
|
* Private.
|
||||||
|
*
|
||||||
|
* \retval true on success
|
||||||
|
* \retval false on allocation failure (the VecDeque is left untouched)
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_grow_(pv) \
|
||||||
|
({ \
|
||||||
|
bool ok; \
|
||||||
|
if ((pv)->cap < sc_vecdeque_max_cap_(pv)) { \
|
||||||
|
size_t newsize = sc_vecdeque_growsize_((pv)->cap); \
|
||||||
|
newsize = CLAMP(newsize, SC_VECDEQUE_MINCAP_, \
|
||||||
|
sc_vecdeque_max_cap_(pv)); \
|
||||||
|
ok = sc_vecdeque_realloc_(pv, newsize); \
|
||||||
|
} else { \
|
||||||
|
ok = false; \
|
||||||
|
} \
|
||||||
|
ok; \
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grow the VecDeque capacity if it is full
|
||||||
|
*
|
||||||
|
* Private.
|
||||||
|
*
|
||||||
|
* \retval true on success
|
||||||
|
* \retval false on allocation failure (the VecDeque is left untouched)
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_grow_if_needed_(pv) \
|
||||||
|
(!sc_vecdeque_is_full(pv) || sc_vecdeque_grow_(pv))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push an uninitialized item, and return a pointer to it
|
||||||
|
*
|
||||||
|
* It does not attempt to resize the VecDeque. It is an error to this function
|
||||||
|
* if the VecDeque is full.
|
||||||
|
*
|
||||||
|
* This function may not fail. It returns a valid non-NULL pointer to the
|
||||||
|
* uninitialized item just pushed.
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_push_hole_noresize(pv) \
|
||||||
|
({ \
|
||||||
|
assert(!sc_vecdeque_is_full(pv)); \
|
||||||
|
++(pv)->size; \
|
||||||
|
&(pv)->data[((pv)->origin + (pv)->size - 1) % (pv)->cap]; \
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push an uninitialized item, and return a pointer to it
|
||||||
|
*
|
||||||
|
* If the VecDeque is full, it is resized.
|
||||||
|
*
|
||||||
|
* This function returns either a valid non-nULL pointer to the uninitialized
|
||||||
|
* item just pushed, or NULL on reallocation failure.
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_push_hole(pv) \
|
||||||
|
(sc_vecdeque_grow_if_needed_(pv) ? \
|
||||||
|
sc_vecdeque_push_hole_noresize(pv) : NULL)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push an item
|
||||||
|
*
|
||||||
|
* It does not attempt to resize the VecDeque. It is an error to this function
|
||||||
|
* if the VecDeque is full.
|
||||||
|
*
|
||||||
|
* This function may not fail.
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_push_noresize(pv, item) \
|
||||||
|
(void) ({ \
|
||||||
|
assert(!sc_vecdeque_is_full(pv)); \
|
||||||
|
++(pv)->size; \
|
||||||
|
(pv)->data[((pv)->origin + (pv)->size - 1) % (pv)->cap] = item; \
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push an item
|
||||||
|
*
|
||||||
|
* If the VecDeque is full, it is resized.
|
||||||
|
*
|
||||||
|
* \retval true on success
|
||||||
|
* \retval false on allocation failure (the VecDeque is left untouched)
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_push(pv, item) \
|
||||||
|
({ \
|
||||||
|
bool ok = sc_vecdeque_grow_if_needed_(pv); \
|
||||||
|
if (ok) { \
|
||||||
|
sc_vecdeque_push_noresize(pv, item); \
|
||||||
|
} \
|
||||||
|
ok; \
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pop an item and return a pointer to it (still in the VecDeque)
|
||||||
|
*
|
||||||
|
* Returning a pointer allows the caller to destroy it in place without copy
|
||||||
|
* (especially if the item type is big).
|
||||||
|
*
|
||||||
|
* It is an error to call this function if the VecDeque is empty.
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_popref(pv) \
|
||||||
|
({ \
|
||||||
|
assert(!sc_vecdeque_is_empty(pv)); \
|
||||||
|
size_t pos = (pv)->origin; \
|
||||||
|
(pv)->origin = ((pv)->origin + 1) % (pv)->cap; \
|
||||||
|
--(pv)->size; \
|
||||||
|
&(pv)->data[pos]; \
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pop an item and returns it
|
||||||
|
*
|
||||||
|
* It is an error to call this function if the VecDeque is empty.
|
||||||
|
*/
|
||||||
|
#define sc_vecdeque_pop(pv) \
|
||||||
|
(*sc_vecdeque_popref(pv))
|
||||||
|
|
||||||
|
#endif
|
@ -118,7 +118,7 @@ static inline void *
|
|||||||
sc_vector_reallocdata_(void *ptr, size_t count, size_t size,
|
sc_vector_reallocdata_(void *ptr, size_t count, size_t size,
|
||||||
size_t *restrict pcap, size_t *restrict psize)
|
size_t *restrict pcap, size_t *restrict psize)
|
||||||
{
|
{
|
||||||
void *p = realloc(ptr, count * size);
|
void *p = reallocarray(ptr, count, size);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
@ -126,7 +126,7 @@ run_v4l2_sink(void *data) {
|
|||||||
vs->has_frame = false;
|
vs->has_frame = false;
|
||||||
sc_mutex_unlock(&vs->mutex);
|
sc_mutex_unlock(&vs->mutex);
|
||||||
|
|
||||||
sc_video_buffer_consume(&vs->vb, vs->frame);
|
sc_frame_buffer_consume(&vs->fb, vs->frame);
|
||||||
|
|
||||||
bool ok = encode_and_write_frame(vs, vs->frame);
|
bool ok = encode_and_write_frame(vs, vs->frame);
|
||||||
av_frame_unref(vs->frame);
|
av_frame_unref(vs->frame);
|
||||||
@ -141,39 +141,19 @@ run_v4l2_sink(void *data) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
|
||||||
sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
|
|
||||||
void *userdata) {
|
|
||||||
(void) vb;
|
|
||||||
struct sc_v4l2_sink *vs = userdata;
|
|
||||||
|
|
||||||
if (!previous_skipped) {
|
|
||||||
sc_mutex_lock(&vs->mutex);
|
|
||||||
vs->has_frame = true;
|
|
||||||
sc_cond_signal(&vs->cond);
|
|
||||||
sc_mutex_unlock(&vs->mutex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
|
sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
|
||||||
static const struct sc_video_buffer_callbacks cbs = {
|
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
|
||||||
.on_new_frame = sc_video_buffer_on_new_frame,
|
(void) ctx;
|
||||||
};
|
|
||||||
|
|
||||||
bool ok = sc_video_buffer_init(&vs->vb, vs->buffering_time, &cbs, vs);
|
bool ok = sc_frame_buffer_init(&vs->fb);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = sc_video_buffer_start(&vs->vb);
|
|
||||||
if (!ok) {
|
|
||||||
goto error_video_buffer_destroy;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = sc_mutex_init(&vs->mutex);
|
ok = sc_mutex_init(&vs->mutex);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
goto error_video_buffer_stop_and_join;
|
goto error_frame_buffer_destroy;
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = sc_cond_init(&vs->cond);
|
ok = sc_cond_init(&vs->cond);
|
||||||
@ -298,11 +278,8 @@ error_cond_destroy:
|
|||||||
sc_cond_destroy(&vs->cond);
|
sc_cond_destroy(&vs->cond);
|
||||||
error_mutex_destroy:
|
error_mutex_destroy:
|
||||||
sc_mutex_destroy(&vs->mutex);
|
sc_mutex_destroy(&vs->mutex);
|
||||||
error_video_buffer_stop_and_join:
|
error_frame_buffer_destroy:
|
||||||
sc_video_buffer_stop(&vs->vb);
|
sc_frame_buffer_destroy(&vs->fb);
|
||||||
sc_video_buffer_join(&vs->vb);
|
|
||||||
error_video_buffer_destroy:
|
|
||||||
sc_video_buffer_destroy(&vs->vb);
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -314,10 +291,7 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) {
|
|||||||
sc_cond_signal(&vs->cond);
|
sc_cond_signal(&vs->cond);
|
||||||
sc_mutex_unlock(&vs->mutex);
|
sc_mutex_unlock(&vs->mutex);
|
||||||
|
|
||||||
sc_video_buffer_stop(&vs->vb);
|
|
||||||
|
|
||||||
sc_thread_join(&vs->thread, NULL);
|
sc_thread_join(&vs->thread, NULL);
|
||||||
sc_video_buffer_join(&vs->vb);
|
|
||||||
|
|
||||||
av_packet_free(&vs->packet);
|
av_packet_free(&vs->packet);
|
||||||
av_frame_free(&vs->frame);
|
av_frame_free(&vs->frame);
|
||||||
@ -327,18 +301,31 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) {
|
|||||||
avformat_free_context(vs->format_ctx);
|
avformat_free_context(vs->format_ctx);
|
||||||
sc_cond_destroy(&vs->cond);
|
sc_cond_destroy(&vs->cond);
|
||||||
sc_mutex_destroy(&vs->mutex);
|
sc_mutex_destroy(&vs->mutex);
|
||||||
sc_video_buffer_destroy(&vs->vb);
|
sc_frame_buffer_destroy(&vs->fb);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) {
|
sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) {
|
||||||
return sc_video_buffer_push(&vs->vb, frame);
|
bool previous_skipped;
|
||||||
|
bool ok = sc_frame_buffer_push(&vs->fb, frame, &previous_skipped);
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!previous_skipped) {
|
||||||
|
sc_mutex_lock(&vs->mutex);
|
||||||
|
vs->has_frame = true;
|
||||||
|
sc_cond_signal(&vs->cond);
|
||||||
|
sc_mutex_unlock(&vs->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
sc_v4l2_frame_sink_open(struct sc_frame_sink *sink) {
|
sc_v4l2_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx) {
|
||||||
struct sc_v4l2_sink *vs = DOWNCAST(sink);
|
struct sc_v4l2_sink *vs = DOWNCAST(sink);
|
||||||
return sc_v4l2_sink_open(vs);
|
return sc_v4l2_sink_open(vs, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@ -355,7 +342,7 @@ sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
|
|||||||
|
|
||||||
bool
|
bool
|
||||||
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
|
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
|
||||||
struct sc_size frame_size, sc_tick buffering_time) {
|
struct sc_size frame_size) {
|
||||||
vs->device_name = strdup(device_name);
|
vs->device_name = strdup(device_name);
|
||||||
if (!vs->device_name) {
|
if (!vs->device_name) {
|
||||||
LOGE("Could not strdup v4l2 device name");
|
LOGE("Could not strdup v4l2 device name");
|
||||||
@ -363,7 +350,6 @@ sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
|
|||||||
}
|
}
|
||||||
|
|
||||||
vs->frame_size = frame_size;
|
vs->frame_size = frame_size;
|
||||||
vs->buffering_time = buffering_time;
|
|
||||||
|
|
||||||
static const struct sc_frame_sink_ops ops = {
|
static const struct sc_frame_sink_ops ops = {
|
||||||
.open = sc_v4l2_frame_sink_open,
|
.open = sc_v4l2_frame_sink_open,
|
||||||
|
@ -8,19 +8,18 @@
|
|||||||
|
|
||||||
#include "coords.h"
|
#include "coords.h"
|
||||||
#include "trait/frame_sink.h"
|
#include "trait/frame_sink.h"
|
||||||
#include "video_buffer.h"
|
#include "frame_buffer.h"
|
||||||
#include "util/tick.h"
|
#include "util/tick.h"
|
||||||
|
|
||||||
struct sc_v4l2_sink {
|
struct sc_v4l2_sink {
|
||||||
struct sc_frame_sink frame_sink; // frame sink trait
|
struct sc_frame_sink frame_sink; // frame sink trait
|
||||||
|
|
||||||
struct sc_video_buffer vb;
|
struct sc_frame_buffer fb;
|
||||||
AVFormatContext *format_ctx;
|
AVFormatContext *format_ctx;
|
||||||
AVCodecContext *encoder_ctx;
|
AVCodecContext *encoder_ctx;
|
||||||
|
|
||||||
char *device_name;
|
char *device_name;
|
||||||
struct sc_size frame_size;
|
struct sc_size frame_size;
|
||||||
sc_tick buffering_time;
|
|
||||||
|
|
||||||
sc_thread thread;
|
sc_thread thread;
|
||||||
sc_mutex mutex;
|
sc_mutex mutex;
|
||||||
@ -35,7 +34,7 @@ struct sc_v4l2_sink {
|
|||||||
|
|
||||||
bool
|
bool
|
||||||
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
|
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
|
||||||
struct sc_size frame_size, sc_tick buffering_time);
|
struct sc_size frame_size);
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs);
|
sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs);
|
||||||
|
@ -1,254 +0,0 @@
|
|||||||
#include "video_buffer.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
#include <libavutil/avutil.h>
|
|
||||||
#include <libavformat/avformat.h>
|
|
||||||
|
|
||||||
#include "util/log.h"
|
|
||||||
|
|
||||||
#define SC_BUFFERING_NDEBUG // comment to debug
|
|
||||||
|
|
||||||
static struct sc_video_buffer_frame *
|
|
||||||
sc_video_buffer_frame_new(const AVFrame *frame) {
|
|
||||||
struct sc_video_buffer_frame *vb_frame = malloc(sizeof(*vb_frame));
|
|
||||||
if (!vb_frame) {
|
|
||||||
LOG_OOM();
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
vb_frame->frame = av_frame_alloc();
|
|
||||||
if (!vb_frame->frame) {
|
|
||||||
LOG_OOM();
|
|
||||||
free(vb_frame);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (av_frame_ref(vb_frame->frame, frame)) {
|
|
||||||
av_frame_free(&vb_frame->frame);
|
|
||||||
free(vb_frame);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return vb_frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
sc_video_buffer_frame_delete(struct sc_video_buffer_frame *vb_frame) {
|
|
||||||
av_frame_unref(vb_frame->frame);
|
|
||||||
av_frame_free(&vb_frame->frame);
|
|
||||||
free(vb_frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_video_buffer_offer(struct sc_video_buffer *vb, const AVFrame *frame) {
|
|
||||||
bool previous_skipped;
|
|
||||||
bool ok = sc_frame_buffer_push(&vb->fb, frame, &previous_skipped);
|
|
||||||
if (!ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
vb->cbs->on_new_frame(vb, previous_skipped, vb->cbs_userdata);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
run_buffering(void *data) {
|
|
||||||
struct sc_video_buffer *vb = data;
|
|
||||||
|
|
||||||
assert(vb->buffering_time > 0);
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
sc_mutex_lock(&vb->b.mutex);
|
|
||||||
|
|
||||||
while (!vb->b.stopped && sc_queue_is_empty(&vb->b.queue)) {
|
|
||||||
sc_cond_wait(&vb->b.queue_cond, &vb->b.mutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vb->b.stopped) {
|
|
||||||
sc_mutex_unlock(&vb->b.mutex);
|
|
||||||
goto stopped;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct sc_video_buffer_frame *vb_frame;
|
|
||||||
sc_queue_take(&vb->b.queue, next, &vb_frame);
|
|
||||||
|
|
||||||
sc_tick max_deadline = sc_tick_now() + vb->buffering_time;
|
|
||||||
// PTS (written by the server) are expressed in microseconds
|
|
||||||
sc_tick pts = SC_TICK_TO_US(vb_frame->frame->pts);
|
|
||||||
|
|
||||||
bool timed_out = false;
|
|
||||||
while (!vb->b.stopped && !timed_out) {
|
|
||||||
sc_tick deadline = sc_clock_to_system_time(&vb->b.clock, pts)
|
|
||||||
+ vb->buffering_time;
|
|
||||||
if (deadline > max_deadline) {
|
|
||||||
deadline = max_deadline;
|
|
||||||
}
|
|
||||||
|
|
||||||
timed_out =
|
|
||||||
!sc_cond_timedwait(&vb->b.wait_cond, &vb->b.mutex, deadline);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vb->b.stopped) {
|
|
||||||
sc_video_buffer_frame_delete(vb_frame);
|
|
||||||
sc_mutex_unlock(&vb->b.mutex);
|
|
||||||
goto stopped;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_mutex_unlock(&vb->b.mutex);
|
|
||||||
|
|
||||||
#ifndef SC_BUFFERING_NDEBUG
|
|
||||||
LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick,
|
|
||||||
pts, vb_frame->push_date, sc_tick_now());
|
|
||||||
#endif
|
|
||||||
|
|
||||||
sc_video_buffer_offer(vb, vb_frame->frame);
|
|
||||||
|
|
||||||
sc_video_buffer_frame_delete(vb_frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
stopped:
|
|
||||||
// Flush queue
|
|
||||||
while (!sc_queue_is_empty(&vb->b.queue)) {
|
|
||||||
struct sc_video_buffer_frame *vb_frame;
|
|
||||||
sc_queue_take(&vb->b.queue, next, &vb_frame);
|
|
||||||
sc_video_buffer_frame_delete(vb_frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGD("Buffering thread ended");
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time,
|
|
||||||
const struct sc_video_buffer_callbacks *cbs,
|
|
||||||
void *cbs_userdata) {
|
|
||||||
bool ok = sc_frame_buffer_init(&vb->fb);
|
|
||||||
if (!ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(buffering_time >= 0);
|
|
||||||
if (buffering_time) {
|
|
||||||
ok = sc_mutex_init(&vb->b.mutex);
|
|
||||||
if (!ok) {
|
|
||||||
sc_frame_buffer_destroy(&vb->fb);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = sc_cond_init(&vb->b.queue_cond);
|
|
||||||
if (!ok) {
|
|
||||||
sc_mutex_destroy(&vb->b.mutex);
|
|
||||||
sc_frame_buffer_destroy(&vb->fb);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = sc_cond_init(&vb->b.wait_cond);
|
|
||||||
if (!ok) {
|
|
||||||
sc_cond_destroy(&vb->b.queue_cond);
|
|
||||||
sc_mutex_destroy(&vb->b.mutex);
|
|
||||||
sc_frame_buffer_destroy(&vb->fb);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_clock_init(&vb->b.clock);
|
|
||||||
sc_queue_init(&vb->b.queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(cbs);
|
|
||||||
assert(cbs->on_new_frame);
|
|
||||||
|
|
||||||
vb->buffering_time = buffering_time;
|
|
||||||
vb->cbs = cbs;
|
|
||||||
vb->cbs_userdata = cbs_userdata;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_video_buffer_start(struct sc_video_buffer *vb) {
|
|
||||||
if (vb->buffering_time) {
|
|
||||||
bool ok =
|
|
||||||
sc_thread_create(&vb->b.thread, run_buffering, "scrcpy-vbuf", vb);
|
|
||||||
if (!ok) {
|
|
||||||
LOGE("Could not start buffering thread");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_video_buffer_stop(struct sc_video_buffer *vb) {
|
|
||||||
if (vb->buffering_time) {
|
|
||||||
sc_mutex_lock(&vb->b.mutex);
|
|
||||||
vb->b.stopped = true;
|
|
||||||
sc_cond_signal(&vb->b.queue_cond);
|
|
||||||
sc_cond_signal(&vb->b.wait_cond);
|
|
||||||
sc_mutex_unlock(&vb->b.mutex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_video_buffer_join(struct sc_video_buffer *vb) {
|
|
||||||
if (vb->buffering_time) {
|
|
||||||
sc_thread_join(&vb->b.thread, NULL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_video_buffer_destroy(struct sc_video_buffer *vb) {
|
|
||||||
sc_frame_buffer_destroy(&vb->fb);
|
|
||||||
if (vb->buffering_time) {
|
|
||||||
sc_cond_destroy(&vb->b.wait_cond);
|
|
||||||
sc_cond_destroy(&vb->b.queue_cond);
|
|
||||||
sc_mutex_destroy(&vb->b.mutex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) {
|
|
||||||
if (!vb->buffering_time) {
|
|
||||||
// No buffering
|
|
||||||
return sc_video_buffer_offer(vb, frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_mutex_lock(&vb->b.mutex);
|
|
||||||
|
|
||||||
sc_tick pts = SC_TICK_FROM_US(frame->pts);
|
|
||||||
sc_clock_update(&vb->b.clock, sc_tick_now(), pts);
|
|
||||||
sc_cond_signal(&vb->b.wait_cond);
|
|
||||||
|
|
||||||
if (vb->b.clock.count == 1) {
|
|
||||||
sc_mutex_unlock(&vb->b.mutex);
|
|
||||||
// First frame, offer it immediately, for two reasons:
|
|
||||||
// - not to delay the opening of the scrcpy window
|
|
||||||
// - the buffering estimation needs at least two clock points, so it
|
|
||||||
// could not handle the first frame
|
|
||||||
return sc_video_buffer_offer(vb, frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct sc_video_buffer_frame *vb_frame = sc_video_buffer_frame_new(frame);
|
|
||||||
if (!vb_frame) {
|
|
||||||
sc_mutex_unlock(&vb->b.mutex);
|
|
||||||
LOG_OOM();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifndef SC_BUFFERING_NDEBUG
|
|
||||||
vb_frame->push_date = sc_tick_now();
|
|
||||||
#endif
|
|
||||||
sc_queue_push(&vb->b.queue, next, vb_frame);
|
|
||||||
sc_cond_signal(&vb->b.queue_cond);
|
|
||||||
|
|
||||||
sc_mutex_unlock(&vb->b.mutex);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst) {
|
|
||||||
sc_frame_buffer_consume(&vb->fb, dst);
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
#ifndef SC_VIDEO_BUFFER_H
|
|
||||||
#define SC_VIDEO_BUFFER_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
|
|
||||||
#include "clock.h"
|
|
||||||
#include "frame_buffer.h"
|
|
||||||
#include "util/queue.h"
|
|
||||||
#include "util/thread.h"
|
|
||||||
#include "util/tick.h"
|
|
||||||
|
|
||||||
// forward declarations
|
|
||||||
typedef struct AVFrame AVFrame;
|
|
||||||
|
|
||||||
struct sc_video_buffer_frame {
|
|
||||||
AVFrame *frame;
|
|
||||||
struct sc_video_buffer_frame *next;
|
|
||||||
#ifndef NDEBUG
|
|
||||||
sc_tick push_date;
|
|
||||||
#endif
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_video_buffer_frame_queue SC_QUEUE(struct sc_video_buffer_frame);
|
|
||||||
|
|
||||||
struct sc_video_buffer {
|
|
||||||
struct sc_frame_buffer fb;
|
|
||||||
|
|
||||||
sc_tick buffering_time;
|
|
||||||
|
|
||||||
// only if buffering_time > 0
|
|
||||||
struct {
|
|
||||||
sc_thread thread;
|
|
||||||
sc_mutex mutex;
|
|
||||||
sc_cond queue_cond;
|
|
||||||
sc_cond wait_cond;
|
|
||||||
|
|
||||||
struct sc_clock clock;
|
|
||||||
struct sc_video_buffer_frame_queue queue;
|
|
||||||
bool stopped;
|
|
||||||
} b; // buffering
|
|
||||||
|
|
||||||
const struct sc_video_buffer_callbacks *cbs;
|
|
||||||
void *cbs_userdata;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_video_buffer_callbacks {
|
|
||||||
void (*on_new_frame)(struct sc_video_buffer *vb, bool previous_skipped,
|
|
||||||
void *userdata);
|
|
||||||
};
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time,
|
|
||||||
const struct sc_video_buffer_callbacks *cbs,
|
|
||||||
void *cbs_userdata);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_video_buffer_start(struct sc_video_buffer *vb);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_video_buffer_stop(struct sc_video_buffer *vb);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_video_buffer_join(struct sc_video_buffer *vb);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_video_buffer_destroy(struct sc_video_buffer *vb);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst);
|
|
||||||
|
|
||||||
#endif
|
|
126
app/tests/test_bytebuf.c
Normal file
126
app/tests/test_bytebuf.c
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "util/bytebuf.h"
|
||||||
|
|
||||||
|
void test_bytebuf_simple(void) {
|
||||||
|
struct sc_bytebuf buf;
|
||||||
|
uint8_t data[20];
|
||||||
|
|
||||||
|
bool ok = sc_bytebuf_init(&buf, 20);
|
||||||
|
assert(ok);
|
||||||
|
|
||||||
|
sc_bytebuf_write(&buf, (uint8_t *) "hello", sizeof("hello") - 1);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 5);
|
||||||
|
|
||||||
|
sc_bytebuf_read(&buf, data, 4);
|
||||||
|
assert(!strncmp((char *) data, "hell", 4));
|
||||||
|
|
||||||
|
sc_bytebuf_write(&buf, (uint8_t *) " world", sizeof(" world") - 1);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 7);
|
||||||
|
|
||||||
|
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 8);
|
||||||
|
|
||||||
|
sc_bytebuf_read(&buf, &data[4], 8);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 0);
|
||||||
|
|
||||||
|
data[12] = '\0';
|
||||||
|
assert(!strcmp((char *) data, "hello world!"));
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 0);
|
||||||
|
|
||||||
|
sc_bytebuf_destroy(&buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_bytebuf_boundaries(void) {
|
||||||
|
struct sc_bytebuf buf;
|
||||||
|
uint8_t data[20];
|
||||||
|
|
||||||
|
bool ok = sc_bytebuf_init(&buf, 20);
|
||||||
|
assert(ok);
|
||||||
|
|
||||||
|
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 6);
|
||||||
|
|
||||||
|
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 12);
|
||||||
|
|
||||||
|
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 18);
|
||||||
|
|
||||||
|
sc_bytebuf_read(&buf, data, 9);
|
||||||
|
assert(!strncmp((char *) data, "hello hel", 9));
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 9);
|
||||||
|
|
||||||
|
sc_bytebuf_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 14);
|
||||||
|
|
||||||
|
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 15);
|
||||||
|
|
||||||
|
sc_bytebuf_skip(&buf, 3);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 12);
|
||||||
|
|
||||||
|
sc_bytebuf_read(&buf, data, 12);
|
||||||
|
data[12] = '\0';
|
||||||
|
assert(!strcmp((char *) data, "hello world!"));
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 0);
|
||||||
|
|
||||||
|
sc_bytebuf_destroy(&buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_bytebuf_two_steps_write(void) {
|
||||||
|
struct sc_bytebuf buf;
|
||||||
|
uint8_t data[20];
|
||||||
|
|
||||||
|
bool ok = sc_bytebuf_init(&buf, 20);
|
||||||
|
assert(ok);
|
||||||
|
|
||||||
|
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 6);
|
||||||
|
|
||||||
|
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 12);
|
||||||
|
|
||||||
|
sc_bytebuf_prepare_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 12); // write not committed yet
|
||||||
|
|
||||||
|
sc_bytebuf_read(&buf, data, 9);
|
||||||
|
assert(!strncmp((char *) data, "hello hel", 3));
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 3);
|
||||||
|
|
||||||
|
sc_bytebuf_commit_write(&buf, sizeof("hello ") - 1);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 9);
|
||||||
|
|
||||||
|
sc_bytebuf_prepare_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 9); // write not committed yet
|
||||||
|
|
||||||
|
sc_bytebuf_commit_write(&buf, sizeof("world") - 1);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 14);
|
||||||
|
|
||||||
|
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 15);
|
||||||
|
|
||||||
|
sc_bytebuf_skip(&buf, 3);
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 12);
|
||||||
|
|
||||||
|
sc_bytebuf_read(&buf, data, 12);
|
||||||
|
data[12] = '\0';
|
||||||
|
assert(!strcmp((char *) data, "hello world!"));
|
||||||
|
assert(sc_bytebuf_read_available(&buf) == 0);
|
||||||
|
|
||||||
|
sc_bytebuf_destroy(&buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
(void) argc;
|
||||||
|
(void) argv;
|
||||||
|
|
||||||
|
test_bytebuf_simple();
|
||||||
|
test_bytebuf_boundaries();
|
||||||
|
test_bytebuf_two_steps_write();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
@ -1,78 +0,0 @@
|
|||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include "util/cbuf.h"
|
|
||||||
|
|
||||||
struct int_queue CBUF(int, 32);
|
|
||||||
|
|
||||||
static void test_cbuf_empty(void) {
|
|
||||||
struct int_queue queue;
|
|
||||||
cbuf_init(&queue);
|
|
||||||
|
|
||||||
assert(cbuf_is_empty(&queue));
|
|
||||||
|
|
||||||
bool push_ok = cbuf_push(&queue, 42);
|
|
||||||
assert(push_ok);
|
|
||||||
assert(!cbuf_is_empty(&queue));
|
|
||||||
|
|
||||||
int item;
|
|
||||||
bool take_ok = cbuf_take(&queue, &item);
|
|
||||||
assert(take_ok);
|
|
||||||
assert(cbuf_is_empty(&queue));
|
|
||||||
|
|
||||||
bool take_empty_ok = cbuf_take(&queue, &item);
|
|
||||||
assert(!take_empty_ok); // the queue is empty
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_cbuf_full(void) {
|
|
||||||
struct int_queue queue;
|
|
||||||
cbuf_init(&queue);
|
|
||||||
|
|
||||||
assert(!cbuf_is_full(&queue));
|
|
||||||
|
|
||||||
// fill the queue
|
|
||||||
for (int i = 0; i < 32; ++i) {
|
|
||||||
bool ok = cbuf_push(&queue, i);
|
|
||||||
assert(ok);
|
|
||||||
}
|
|
||||||
bool ok = cbuf_push(&queue, 42);
|
|
||||||
assert(!ok); // the queue if full
|
|
||||||
|
|
||||||
int item;
|
|
||||||
bool take_ok = cbuf_take(&queue, &item);
|
|
||||||
assert(take_ok);
|
|
||||||
assert(!cbuf_is_full(&queue));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_cbuf_push_take(void) {
|
|
||||||
struct int_queue queue;
|
|
||||||
cbuf_init(&queue);
|
|
||||||
|
|
||||||
bool push1_ok = cbuf_push(&queue, 42);
|
|
||||||
assert(push1_ok);
|
|
||||||
|
|
||||||
bool push2_ok = cbuf_push(&queue, 35);
|
|
||||||
assert(push2_ok);
|
|
||||||
|
|
||||||
int item;
|
|
||||||
|
|
||||||
bool take1_ok = cbuf_take(&queue, &item);
|
|
||||||
assert(take1_ok);
|
|
||||||
assert(item == 42);
|
|
||||||
|
|
||||||
bool take2_ok = cbuf_take(&queue, &item);
|
|
||||||
assert(take2_ok);
|
|
||||||
assert(item == 35);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
(void) argc;
|
|
||||||
(void) argv;
|
|
||||||
|
|
||||||
test_cbuf_empty();
|
|
||||||
test_cbuf_full();
|
|
||||||
test_cbuf_push_take();
|
|
||||||
return 0;
|
|
||||||
}
|
|
@ -46,7 +46,7 @@ static void test_options(void) {
|
|||||||
char *argv[] = {
|
char *argv[] = {
|
||||||
"scrcpy",
|
"scrcpy",
|
||||||
"--always-on-top",
|
"--always-on-top",
|
||||||
"--bit-rate", "5M",
|
"--video-bit-rate", "5M",
|
||||||
"--crop", "100:200:300:400",
|
"--crop", "100:200:300:400",
|
||||||
"--fullscreen",
|
"--fullscreen",
|
||||||
"--max-fps", "30",
|
"--max-fps", "30",
|
||||||
@ -75,7 +75,7 @@ static void test_options(void) {
|
|||||||
|
|
||||||
const struct scrcpy_options *opts = &args.opts;
|
const struct scrcpy_options *opts = &args.opts;
|
||||||
assert(opts->always_on_top);
|
assert(opts->always_on_top);
|
||||||
assert(opts->bit_rate == 5000000);
|
assert(opts->video_bit_rate == 5000000);
|
||||||
assert(!strcmp(opts->crop, "100:200:300:400"));
|
assert(!strcmp(opts->crop, "100:200:300:400"));
|
||||||
assert(opts->fullscreen);
|
assert(opts->fullscreen);
|
||||||
assert(opts->max_fps == 30);
|
assert(opts->max_fps == 30);
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
|
|
||||||
#include "util/queue.h"
|
|
||||||
|
|
||||||
struct foo {
|
|
||||||
int value;
|
|
||||||
struct foo *next;
|
|
||||||
};
|
|
||||||
|
|
||||||
static void test_queue(void) {
|
|
||||||
struct my_queue SC_QUEUE(struct foo) queue;
|
|
||||||
sc_queue_init(&queue);
|
|
||||||
|
|
||||||
assert(sc_queue_is_empty(&queue));
|
|
||||||
|
|
||||||
struct foo v1 = { .value = 42 };
|
|
||||||
struct foo v2 = { .value = 27 };
|
|
||||||
|
|
||||||
sc_queue_push(&queue, next, &v1);
|
|
||||||
sc_queue_push(&queue, next, &v2);
|
|
||||||
|
|
||||||
struct foo *foo;
|
|
||||||
|
|
||||||
assert(!sc_queue_is_empty(&queue));
|
|
||||||
sc_queue_take(&queue, next, &foo);
|
|
||||||
assert(foo->value == 42);
|
|
||||||
|
|
||||||
assert(!sc_queue_is_empty(&queue));
|
|
||||||
sc_queue_take(&queue, next, &foo);
|
|
||||||
assert(foo->value == 27);
|
|
||||||
|
|
||||||
assert(sc_queue_is_empty(&queue));
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
(void) argc;
|
|
||||||
(void) argv;
|
|
||||||
|
|
||||||
test_queue();
|
|
||||||
return 0;
|
|
||||||
}
|
|
197
app/tests/test_vecdeque.c
Normal file
197
app/tests/test_vecdeque.c
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
#include "util/vecdeque.h"
|
||||||
|
|
||||||
|
#define pr(pv) \
|
||||||
|
({ \
|
||||||
|
fprintf(stderr, "cap=%lu origin=%lu size=%lu\n", (pv)->cap, (pv)->origin, (pv)->size); \
|
||||||
|
for (size_t i = 0; i < (pv)->cap; ++i) \
|
||||||
|
fprintf(stderr, "%d ", (pv)->data[i]); \
|
||||||
|
fprintf(stderr, "\n"); \
|
||||||
|
})
|
||||||
|
|
||||||
|
static void test_vecdeque_push_pop(void) {
|
||||||
|
struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER;
|
||||||
|
|
||||||
|
assert(sc_vecdeque_is_empty(&vdq));
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 0);
|
||||||
|
|
||||||
|
bool ok = sc_vecdeque_push(&vdq, 5);
|
||||||
|
assert(ok);
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 1);
|
||||||
|
|
||||||
|
ok = sc_vecdeque_push(&vdq, 12);
|
||||||
|
assert(ok);
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 2);
|
||||||
|
|
||||||
|
int v = sc_vecdeque_pop(&vdq);
|
||||||
|
assert(v == 5);
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 1);
|
||||||
|
|
||||||
|
ok = sc_vecdeque_push(&vdq, 7);
|
||||||
|
assert(ok);
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 2);
|
||||||
|
|
||||||
|
int *p = sc_vecdeque_popref(&vdq);
|
||||||
|
assert(p);
|
||||||
|
assert(*p == 12);
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 1);
|
||||||
|
|
||||||
|
v = sc_vecdeque_pop(&vdq);
|
||||||
|
assert(v == 7);
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 0);
|
||||||
|
assert(sc_vecdeque_is_empty(&vdq));
|
||||||
|
|
||||||
|
sc_vecdeque_destroy(&vdq);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_vecdeque_reserve(void) {
|
||||||
|
struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER;
|
||||||
|
|
||||||
|
bool ok = sc_vecdeque_reserve(&vdq, 20);
|
||||||
|
assert(ok);
|
||||||
|
assert(vdq.cap == 20);
|
||||||
|
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 0);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < 20; ++i) {
|
||||||
|
ok = sc_vecdeque_push(&vdq, i);
|
||||||
|
assert(ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 20);
|
||||||
|
|
||||||
|
// It is now full
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; ++i) {
|
||||||
|
int v = sc_vecdeque_pop(&vdq);
|
||||||
|
assert(v == i);
|
||||||
|
}
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 15);
|
||||||
|
|
||||||
|
for (int i = 20; i < 25; ++i) {
|
||||||
|
ok = sc_vecdeque_push(&vdq, i);
|
||||||
|
assert(ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 20);
|
||||||
|
assert(vdq.cap == 20);
|
||||||
|
|
||||||
|
// Now, the content wraps around the ring buffer:
|
||||||
|
// 20 21 22 23 24 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
||||||
|
// ^
|
||||||
|
// origin
|
||||||
|
|
||||||
|
// It is now full, let's reserve some space
|
||||||
|
ok = sc_vecdeque_reserve(&vdq, 30);
|
||||||
|
assert(ok);
|
||||||
|
assert(vdq.cap == 30);
|
||||||
|
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 20);
|
||||||
|
|
||||||
|
for (int i = 0; i < 20; ++i) {
|
||||||
|
// We should retrieve the items we inserted in order
|
||||||
|
int v = sc_vecdeque_pop(&vdq);
|
||||||
|
assert(v == i + 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 0);
|
||||||
|
|
||||||
|
sc_vecdeque_destroy(&vdq);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_vecdeque_grow() {
|
||||||
|
struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER;
|
||||||
|
|
||||||
|
bool ok = sc_vecdeque_reserve(&vdq, 20);
|
||||||
|
assert(ok);
|
||||||
|
assert(vdq.cap == 20);
|
||||||
|
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < 500; ++i) {
|
||||||
|
ok = sc_vecdeque_push(&vdq, i);
|
||||||
|
assert(ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 500);
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; ++i) {
|
||||||
|
int v = sc_vecdeque_pop(&vdq);
|
||||||
|
assert(v == i);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 400);
|
||||||
|
|
||||||
|
for (int i = 500; i < 1000; ++i) {
|
||||||
|
ok = sc_vecdeque_push(&vdq, i);
|
||||||
|
assert(ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 900);
|
||||||
|
|
||||||
|
for (int i = 100; i < 1000; ++i) {
|
||||||
|
int v = sc_vecdeque_pop(&vdq);
|
||||||
|
assert(v == i);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 0);
|
||||||
|
|
||||||
|
sc_vecdeque_destroy(&vdq);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_vecdeque_push_hole() {
|
||||||
|
struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER;
|
||||||
|
|
||||||
|
bool ok = sc_vecdeque_reserve(&vdq, 20);
|
||||||
|
assert(ok);
|
||||||
|
assert(vdq.cap == 20);
|
||||||
|
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < 20; ++i) {
|
||||||
|
int *p = sc_vecdeque_push_hole(&vdq);
|
||||||
|
assert(p);
|
||||||
|
*p = i * 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 20);
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; ++i) {
|
||||||
|
int v = sc_vecdeque_pop(&vdq);
|
||||||
|
assert(v == i * 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 10);
|
||||||
|
|
||||||
|
for (int i = 20; i < 30; ++i) {
|
||||||
|
int *p = sc_vecdeque_push_hole(&vdq);
|
||||||
|
assert(p);
|
||||||
|
*p = i * 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 20);
|
||||||
|
|
||||||
|
for (int i = 10; i < 30; ++i) {
|
||||||
|
int v = sc_vecdeque_pop(&vdq);
|
||||||
|
assert(v == i * 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(sc_vecdeque_size(&vdq) == 0);
|
||||||
|
|
||||||
|
sc_vecdeque_destroy(&vdq);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
(void) argc;
|
||||||
|
(void) argv;
|
||||||
|
|
||||||
|
test_vecdeque_push_pop();
|
||||||
|
test_vecdeque_reserve();
|
||||||
|
test_vecdeque_grow();
|
||||||
|
test_vecdeque_push_hole();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
@ -7,7 +7,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.2.2'
|
classpath 'com.android.tools.build:gradle:7.4.0'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
@ -19,6 +19,7 @@ endian = 'little'
|
|||||||
ffmpeg_avcodec = 'avcodec-58'
|
ffmpeg_avcodec = 'avcodec-58'
|
||||||
ffmpeg_avformat = 'avformat-58'
|
ffmpeg_avformat = 'avformat-58'
|
||||||
ffmpeg_avutil = 'avutil-56'
|
ffmpeg_avutil = 'avutil-56'
|
||||||
|
ffmpeg_swresample = 'swresample-3'
|
||||||
prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1'
|
prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1'
|
||||||
prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32'
|
prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32'
|
||||||
prebuilt_libusb_root = 'libusb-1.0.26'
|
prebuilt_libusb_root = 'libusb-1.0.26'
|
||||||
|
@ -19,6 +19,7 @@ endian = 'little'
|
|||||||
ffmpeg_avcodec = 'avcodec-59'
|
ffmpeg_avcodec = 'avcodec-59'
|
||||||
ffmpeg_avformat = 'avformat-59'
|
ffmpeg_avformat = 'avformat-59'
|
||||||
ffmpeg_avutil = 'avutil-57'
|
ffmpeg_avutil = 'avutil-57'
|
||||||
|
ffmpeg_swresample = 'swresample-4'
|
||||||
prebuilt_ffmpeg = 'ffmpeg-win64-5.1.2'
|
prebuilt_ffmpeg = 'ffmpeg-win64-5.1.2'
|
||||||
prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32'
|
prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32'
|
||||||
prebuilt_libusb_root = 'libusb-1.0.26'
|
prebuilt_libusb_root = 'libusb-1.0.26'
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
namespace 'com.genymobile.scrcpy'
|
||||||
compileSdkVersion 33
|
compileSdkVersion 33
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.genymobile.scrcpy"
|
applicationId "com.genymobile.scrcpy"
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
<!-- not a real Android application, it is run by app_process manually -->
|
<!-- not a real Android application, it is run by app_process manually -->
|
||||||
<manifest package="com.genymobile.scrcpy"/>
|
<manifest />
|
||||||
|
47
server/src/main/java/com/genymobile/scrcpy/AudioCodec.java
Normal file
47
server/src/main/java/com/genymobile/scrcpy/AudioCodec.java
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import android.media.MediaFormat;
|
||||||
|
|
||||||
|
public enum AudioCodec implements Codec {
|
||||||
|
OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS),
|
||||||
|
AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC);
|
||||||
|
|
||||||
|
private final int id; // 4-byte ASCII representation of the name
|
||||||
|
private final String name;
|
||||||
|
private final String mimeType;
|
||||||
|
|
||||||
|
AudioCodec(int id, String name, String mimeType) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.mimeType = mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type getType() {
|
||||||
|
return Type.AUDIO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMimeType() {
|
||||||
|
return mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AudioCodec findByName(String name) {
|
||||||
|
for (AudioCodec codec : values()) {
|
||||||
|
if (codec.name.equals(name)) {
|
||||||
|
return codec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
424
server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java
Normal file
424
server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java
Normal file
@ -0,0 +1,424 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.media.AudioFormat;
|
||||||
|
import android.media.AudioRecord;
|
||||||
|
import android.media.AudioTimestamp;
|
||||||
|
import android.media.MediaCodec;
|
||||||
|
import android.media.MediaFormat;
|
||||||
|
import android.media.MediaRecorder;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.HandlerThread;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
|
||||||
|
public final class AudioEncoder {
|
||||||
|
|
||||||
|
private static class InputTask {
|
||||||
|
private final int index;
|
||||||
|
|
||||||
|
InputTask(int index) {
|
||||||
|
this.index = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class OutputTask {
|
||||||
|
private final int index;
|
||||||
|
private final MediaCodec.BufferInfo bufferInfo;
|
||||||
|
|
||||||
|
OutputTask(int index, MediaCodec.BufferInfo bufferInfo) {
|
||||||
|
this.index = index;
|
||||||
|
this.bufferInfo = bufferInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int SAMPLE_RATE = 48000;
|
||||||
|
private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
|
||||||
|
private static final int CHANNELS = 2;
|
||||||
|
private static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT;
|
||||||
|
private static final int BYTES_PER_SAMPLE = 2;
|
||||||
|
|
||||||
|
private static final int BUFFER_MS = 5; // milliseconds
|
||||||
|
private static final int BUFFER_SIZE = SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * BUFFER_MS / 1000;
|
||||||
|
|
||||||
|
private final Streamer streamer;
|
||||||
|
private final int bitRate;
|
||||||
|
private final List<CodecOption> codecOptions;
|
||||||
|
private final String encoderName;
|
||||||
|
|
||||||
|
// Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4).
|
||||||
|
// So many pending tasks would lead to an unacceptable delay anyway.
|
||||||
|
private final BlockingQueue<InputTask> inputTasks = new ArrayBlockingQueue<>(64);
|
||||||
|
private final BlockingQueue<OutputTask> outputTasks = new ArrayBlockingQueue<>(64);
|
||||||
|
|
||||||
|
private Thread thread;
|
||||||
|
private HandlerThread mediaCodecThread;
|
||||||
|
|
||||||
|
private Thread inputThread;
|
||||||
|
private Thread outputThread;
|
||||||
|
|
||||||
|
private boolean ended;
|
||||||
|
|
||||||
|
public AudioEncoder(Streamer streamer, int bitRate, List<CodecOption> codecOptions, String encoderName) {
|
||||||
|
this.streamer = streamer;
|
||||||
|
this.bitRate = bitRate;
|
||||||
|
this.codecOptions = codecOptions;
|
||||||
|
this.encoderName = encoderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AudioFormat createAudioFormat() {
|
||||||
|
AudioFormat.Builder builder = new AudioFormat.Builder();
|
||||||
|
builder.setEncoding(FORMAT);
|
||||||
|
builder.setSampleRate(SAMPLE_RATE);
|
||||||
|
builder.setChannelMask(CHANNEL_CONFIG);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.M)
|
||||||
|
@SuppressLint({"WrongConstant", "MissingPermission"})
|
||||||
|
private static AudioRecord createAudioRecord() {
|
||||||
|
AudioRecord.Builder builder = new AudioRecord.Builder();
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
|
||||||
|
builder.setContext(FakeContext.get());
|
||||||
|
}
|
||||||
|
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
|
||||||
|
builder.setAudioFormat(createAudioFormat());
|
||||||
|
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT);
|
||||||
|
builder.setBufferSizeInBytes(minBufferSize);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaFormat createFormat(String mimeType, int bitRate, List<CodecOption> codecOptions) {
|
||||||
|
MediaFormat format = new MediaFormat();
|
||||||
|
format.setString(MediaFormat.KEY_MIME, mimeType);
|
||||||
|
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
||||||
|
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS);
|
||||||
|
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE);
|
||||||
|
|
||||||
|
if (codecOptions != null) {
|
||||||
|
for (CodecOption option : codecOptions) {
|
||||||
|
String key = option.getKey();
|
||||||
|
Object value = option.getValue();
|
||||||
|
CodecUtils.setCodecOption(format, key, value);
|
||||||
|
Ln.d("Audio codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
|
private void inputThread(MediaCodec mediaCodec, AudioRecord recorder) throws IOException, InterruptedException {
|
||||||
|
final AudioTimestamp timestamp = new AudioTimestamp();
|
||||||
|
long previousPts = 0;
|
||||||
|
long nextPts = 0;
|
||||||
|
|
||||||
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
|
InputTask task = inputTasks.take();
|
||||||
|
ByteBuffer buffer = mediaCodec.getInputBuffer(task.index);
|
||||||
|
int r = recorder.read(buffer, BUFFER_SIZE);
|
||||||
|
if (r < 0) {
|
||||||
|
throw new IOException("Could not read audio: " + r);
|
||||||
|
}
|
||||||
|
|
||||||
|
long pts;
|
||||||
|
|
||||||
|
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
|
||||||
|
if (ret == AudioRecord.SUCCESS) {
|
||||||
|
pts = timestamp.nanoTime / 1000;
|
||||||
|
} else {
|
||||||
|
if (nextPts == 0) {
|
||||||
|
Ln.w("Could not get any audio timestamp");
|
||||||
|
}
|
||||||
|
// compute from previous timestamp and packet size
|
||||||
|
pts = nextPts;
|
||||||
|
}
|
||||||
|
|
||||||
|
long durationMs = r * 1000 / CHANNELS / SAMPLE_RATE;
|
||||||
|
nextPts = pts + durationMs;
|
||||||
|
|
||||||
|
if (previousPts != 0 && pts < previousPts) {
|
||||||
|
// Audio PTS may come from two sources:
|
||||||
|
// - recorder.getTimestamp() if the call works;
|
||||||
|
// - an estimation from the previous PTS and the packet size as a fallback.
|
||||||
|
//
|
||||||
|
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
|
||||||
|
pts = previousPts + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaCodec.queueInputBuffer(task.index, 0, r, pts, 0);
|
||||||
|
|
||||||
|
previousPts = pts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void outputThread(MediaCodec mediaCodec) throws IOException, InterruptedException {
|
||||||
|
streamer.writeHeader();
|
||||||
|
|
||||||
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
|
OutputTask task = outputTasks.take();
|
||||||
|
ByteBuffer buffer = mediaCodec.getOutputBuffer(task.index);
|
||||||
|
try {
|
||||||
|
streamer.writePacket(buffer, task.bufferInfo);
|
||||||
|
} finally {
|
||||||
|
mediaCodec.releaseOutputBuffer(task.index, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
thread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
encode();
|
||||||
|
} catch (ConfigurationException e) {
|
||||||
|
// Do not print stack trace, a user-friendly error-message has already been logged
|
||||||
|
} catch (IOException e) {
|
||||||
|
Ln.e("Audio encoding error", e);
|
||||||
|
} finally {
|
||||||
|
Ln.d("Audio encoder stopped");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
if (thread != null) {
|
||||||
|
// Just wake up the blocking wait from the thread, so that it properly releases all its resources and terminates
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void join() throws InterruptedException {
|
||||||
|
if (thread != null) {
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void end() {
|
||||||
|
ended = true;
|
||||||
|
notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void waitEnded() {
|
||||||
|
try {
|
||||||
|
while (!ended) {
|
||||||
|
wait();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void startWorkaroundAndroid11() {
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||||
|
// Android 11 requires Apps to be at foreground to record audio.
|
||||||
|
// Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
|
||||||
|
// But Scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android
|
||||||
|
// shell ("com.android.shell").
|
||||||
|
// If there is an Activity from Android shell running at foreground, then the permission system will believe Scrcpy is also in the
|
||||||
|
// foreground.
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||||
|
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
|
||||||
|
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent);
|
||||||
|
// Wait for activity to start
|
||||||
|
SystemClock.sleep(150);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void stopWorkaroundAndroid11() {
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||||
|
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.M)
|
||||||
|
public void encode() throws IOException, ConfigurationException {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
|
Ln.w("Audio disabled: it is not supported before Android 11");
|
||||||
|
streamer.writeDisableStream(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaCodec mediaCodec = null;
|
||||||
|
AudioRecord recorder = null;
|
||||||
|
|
||||||
|
boolean mediaCodecStarted = false;
|
||||||
|
boolean recorderStarted = false;
|
||||||
|
try {
|
||||||
|
Codec codec = streamer.getCodec();
|
||||||
|
mediaCodec = createMediaCodec(codec, encoderName);
|
||||||
|
|
||||||
|
mediaCodecThread = new HandlerThread("AudioEncoder");
|
||||||
|
mediaCodecThread.start();
|
||||||
|
|
||||||
|
MediaFormat format = createFormat(codec.getMimeType(), bitRate, codecOptions);
|
||||||
|
mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper()));
|
||||||
|
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||||
|
|
||||||
|
startWorkaroundAndroid11();
|
||||||
|
try {
|
||||||
|
recorder = createAudioRecord();
|
||||||
|
recorder.startRecording();
|
||||||
|
} catch (UnsupportedOperationException e) {
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||||
|
Ln.e("Failed to start audio capture");
|
||||||
|
Ln.e("On Android 11, it is only possible to capture in foreground, make sure that the device is unlocked when starting scrcpy.");
|
||||||
|
// Continue to mirror without audio (configurationError is left to false)
|
||||||
|
streamer.writeDisableStream(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
stopWorkaroundAndroid11();
|
||||||
|
}
|
||||||
|
recorderStarted = true;
|
||||||
|
|
||||||
|
final MediaCodec mediaCodecRef = mediaCodec;
|
||||||
|
final AudioRecord recorderRef = recorder;
|
||||||
|
inputThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
inputThread(mediaCodecRef, recorderRef);
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
Ln.e("Audio capture error", e);
|
||||||
|
} finally {
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
outputThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
outputThread(mediaCodecRef);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// this is expected on close
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Broken pipe is expected on close, because the socket is closed by the client
|
||||||
|
if (!IO.isBrokenPipe(e)) {
|
||||||
|
Ln.e("Audio encoding error", e);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mediaCodec.start();
|
||||||
|
mediaCodecStarted = true;
|
||||||
|
inputThread.start();
|
||||||
|
outputThread.start();
|
||||||
|
|
||||||
|
waitEnded();
|
||||||
|
} catch (ConfigurationException e) {
|
||||||
|
// Notify the error to make scrcpy exit
|
||||||
|
streamer.writeDisableStream(true);
|
||||||
|
throw e;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
// Notify the client that the audio could not be captured
|
||||||
|
streamer.writeDisableStream(false);
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
// Cleanup everything (either at the end or on error at any step of the initialization)
|
||||||
|
if (mediaCodecThread != null) {
|
||||||
|
Looper looper = mediaCodecThread.getLooper();
|
||||||
|
if (looper != null) {
|
||||||
|
looper.quitSafely();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inputThread != null) {
|
||||||
|
inputThread.interrupt();
|
||||||
|
}
|
||||||
|
if (outputThread != null) {
|
||||||
|
outputThread.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (mediaCodecThread != null) {
|
||||||
|
mediaCodecThread.join();
|
||||||
|
}
|
||||||
|
if (inputThread != null) {
|
||||||
|
inputThread.join();
|
||||||
|
}
|
||||||
|
if (outputThread != null) {
|
||||||
|
outputThread.join();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Should never happen
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaCodec != null) {
|
||||||
|
if (mediaCodecStarted) {
|
||||||
|
mediaCodec.stop();
|
||||||
|
}
|
||||||
|
mediaCodec.release();
|
||||||
|
}
|
||||||
|
if (recorder != null) {
|
||||||
|
if (recorderStarted) {
|
||||||
|
recorder.stop();
|
||||||
|
}
|
||||||
|
recorder.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException {
|
||||||
|
if (encoderName != null) {
|
||||||
|
Ln.d("Creating audio encoder by name: '" + encoderName + "'");
|
||||||
|
try {
|
||||||
|
return MediaCodec.createByCodecName(encoderName);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage());
|
||||||
|
throw new ConfigurationException("Unknown encoder: " + encoderName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
||||||
|
Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'");
|
||||||
|
return mediaCodec;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class EncoderCallback extends MediaCodec.Callback {
|
||||||
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
|
@Override
|
||||||
|
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
||||||
|
try {
|
||||||
|
inputTasks.put(new InputTask(index));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo bufferInfo) {
|
||||||
|
try {
|
||||||
|
outputTasks.put(new OutputTask(index, bufferInfo));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
|
||||||
|
Ln.e("MediaCodec error", e);
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -139,7 +139,7 @@ public final class CleanUp {
|
|||||||
builder.start();
|
builder.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void unlinkSelf() {
|
public static void unlinkSelf() {
|
||||||
try {
|
try {
|
||||||
new File(SERVER_PATH).delete();
|
new File(SERVER_PATH).delete();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
17
server/src/main/java/com/genymobile/scrcpy/Codec.java
Normal file
17
server/src/main/java/com/genymobile/scrcpy/Codec.java
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
public interface Codec {
|
||||||
|
|
||||||
|
enum Type {
|
||||||
|
VIDEO,
|
||||||
|
AUDIO,
|
||||||
|
}
|
||||||
|
|
||||||
|
Type getType();
|
||||||
|
|
||||||
|
int getId();
|
||||||
|
|
||||||
|
String getName();
|
||||||
|
|
||||||
|
String getMimeType();
|
||||||
|
}
|
78
server/src/main/java/com/genymobile/scrcpy/CodecUtils.java
Normal file
78
server/src/main/java/com/genymobile/scrcpy/CodecUtils.java
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import android.media.MediaCodecInfo;
|
||||||
|
import android.media.MediaCodecList;
|
||||||
|
import android.media.MediaFormat;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class CodecUtils {
|
||||||
|
|
||||||
|
public static final class DeviceEncoder {
|
||||||
|
private final Codec codec;
|
||||||
|
private final MediaCodecInfo info;
|
||||||
|
|
||||||
|
DeviceEncoder(Codec codec, MediaCodecInfo info) {
|
||||||
|
this.codec = codec;
|
||||||
|
this.info = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Codec getCodec() {
|
||||||
|
return codec;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaCodecInfo getInfo() {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CodecUtils() {
|
||||||
|
// not instantiable
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setCodecOption(MediaFormat format, String key, Object value) {
|
||||||
|
if (value instanceof Integer) {
|
||||||
|
format.setInteger(key, (Integer) value);
|
||||||
|
} else if (value instanceof Long) {
|
||||||
|
format.setLong(key, (Long) value);
|
||||||
|
} else if (value instanceof Float) {
|
||||||
|
format.setFloat(key, (Float) value);
|
||||||
|
} else if (value instanceof String) {
|
||||||
|
format.setString(key, (String) value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaCodecInfo[] getEncoders(MediaCodecList codecs, String mimeType) {
|
||||||
|
List<MediaCodecInfo> result = new ArrayList<>();
|
||||||
|
for (MediaCodecInfo codecInfo : codecs.getCodecInfos()) {
|
||||||
|
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(mimeType)) {
|
||||||
|
result.add(codecInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toArray(new MediaCodecInfo[result.size()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<DeviceEncoder> listVideoEncoders() {
|
||||||
|
List<DeviceEncoder> encoders = new ArrayList<>();
|
||||||
|
MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||||
|
for (VideoCodec codec : VideoCodec.values()) {
|
||||||
|
for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) {
|
||||||
|
encoders.add(new DeviceEncoder(codec, info));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return encoders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<DeviceEncoder> listAudioEncoders() {
|
||||||
|
List<DeviceEncoder> encoders = new ArrayList<>();
|
||||||
|
MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||||
|
for (AudioCodec codec : AudioCodec.values()) {
|
||||||
|
for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) {
|
||||||
|
encoders.add(new DeviceEncoder(codec, info));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return encoders;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
public class ConfigurationException extends Exception {
|
||||||
|
public ConfigurationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,9 @@ public final class DesktopConnection implements Closeable {
|
|||||||
private final LocalSocket videoSocket;
|
private final LocalSocket videoSocket;
|
||||||
private final FileDescriptor videoFd;
|
private final FileDescriptor videoFd;
|
||||||
|
|
||||||
|
private final LocalSocket audioSocket;
|
||||||
|
private final FileDescriptor audioFd;
|
||||||
|
|
||||||
private final LocalSocket controlSocket;
|
private final LocalSocket controlSocket;
|
||||||
private final InputStream controlInputStream;
|
private final InputStream controlInputStream;
|
||||||
private final OutputStream controlOutputStream;
|
private final OutputStream controlOutputStream;
|
||||||
@ -27,9 +30,10 @@ public final class DesktopConnection implements Closeable {
|
|||||||
private final ControlMessageReader reader = new ControlMessageReader();
|
private final ControlMessageReader reader = new ControlMessageReader();
|
||||||
private final DeviceMessageWriter writer = new DeviceMessageWriter();
|
private final DeviceMessageWriter writer = new DeviceMessageWriter();
|
||||||
|
|
||||||
private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException {
|
private DesktopConnection(LocalSocket videoSocket, LocalSocket audioSocket, LocalSocket controlSocket) throws IOException {
|
||||||
this.videoSocket = videoSocket;
|
this.videoSocket = videoSocket;
|
||||||
this.controlSocket = controlSocket;
|
this.controlSocket = controlSocket;
|
||||||
|
this.audioSocket = audioSocket;
|
||||||
if (controlSocket != null) {
|
if (controlSocket != null) {
|
||||||
controlInputStream = controlSocket.getInputStream();
|
controlInputStream = controlSocket.getInputStream();
|
||||||
controlOutputStream = controlSocket.getOutputStream();
|
controlOutputStream = controlSocket.getOutputStream();
|
||||||
@ -38,6 +42,7 @@ public final class DesktopConnection implements Closeable {
|
|||||||
controlOutputStream = null;
|
controlOutputStream = null;
|
||||||
}
|
}
|
||||||
videoFd = videoSocket.getFileDescriptor();
|
videoFd = videoSocket.getFileDescriptor();
|
||||||
|
audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LocalSocket connect(String abstractName) throws IOException {
|
private static LocalSocket connect(String abstractName) throws IOException {
|
||||||
@ -55,40 +60,50 @@ public final class DesktopConnection implements Closeable {
|
|||||||
return SOCKET_NAME_PREFIX + String.format("_%08x", scid);
|
return SOCKET_NAME_PREFIX + String.format("_%08x", scid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DesktopConnection open(int scid, boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException {
|
public static DesktopConnection open(int scid, boolean tunnelForward, boolean audio, boolean control, boolean sendDummyByte) throws IOException {
|
||||||
String socketName = getSocketName(scid);
|
String socketName = getSocketName(scid);
|
||||||
|
|
||||||
LocalSocket videoSocket;
|
LocalSocket videoSocket = null;
|
||||||
|
LocalSocket audioSocket = null;
|
||||||
LocalSocket controlSocket = null;
|
LocalSocket controlSocket = null;
|
||||||
if (tunnelForward) {
|
try {
|
||||||
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
|
if (tunnelForward) {
|
||||||
videoSocket = localServerSocket.accept();
|
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
|
||||||
if (sendDummyByte) {
|
videoSocket = localServerSocket.accept();
|
||||||
// send one byte so the client may read() to detect a connection error
|
if (sendDummyByte) {
|
||||||
videoSocket.getOutputStream().write(0);
|
// send one byte so the client may read() to detect a connection error
|
||||||
}
|
videoSocket.getOutputStream().write(0);
|
||||||
if (control) {
|
}
|
||||||
try {
|
if (audio) {
|
||||||
|
audioSocket = localServerSocket.accept();
|
||||||
|
}
|
||||||
|
if (control) {
|
||||||
controlSocket = localServerSocket.accept();
|
controlSocket = localServerSocket.accept();
|
||||||
} catch (IOException | RuntimeException e) {
|
|
||||||
videoSocket.close();
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
} else {
|
videoSocket = connect(socketName);
|
||||||
videoSocket = connect(socketName);
|
if (audio) {
|
||||||
if (control) {
|
audioSocket = connect(socketName);
|
||||||
try {
|
}
|
||||||
|
if (control) {
|
||||||
controlSocket = connect(socketName);
|
controlSocket = connect(socketName);
|
||||||
} catch (IOException | RuntimeException e) {
|
|
||||||
videoSocket.close();
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (IOException | RuntimeException e) {
|
||||||
|
if (videoSocket != null) {
|
||||||
|
videoSocket.close();
|
||||||
|
}
|
||||||
|
if (audioSocket != null) {
|
||||||
|
audioSocket.close();
|
||||||
|
}
|
||||||
|
if (controlSocket != null) {
|
||||||
|
controlSocket.close();
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new DesktopConnection(videoSocket, controlSocket);
|
return new DesktopConnection(videoSocket, audioSocket, controlSocket);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
@ -121,6 +136,10 @@ public final class DesktopConnection implements Closeable {
|
|||||||
return videoFd;
|
return videoFd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FileDescriptor getAudioFd() {
|
||||||
|
return audioFd;
|
||||||
|
}
|
||||||
|
|
||||||
public ControlMessage receiveControlMessage() throws IOException {
|
public ControlMessage receiveControlMessage() throws IOException {
|
||||||
ControlMessage msg = reader.next();
|
ControlMessage msg = reader.next();
|
||||||
while (msg == null) {
|
while (msg == null) {
|
||||||
|
@ -61,12 +61,12 @@ public final class Device {
|
|||||||
|
|
||||||
private final boolean supportsInputEvents;
|
private final boolean supportsInputEvents;
|
||||||
|
|
||||||
public Device(Options options) {
|
public Device(Options options) throws ConfigurationException {
|
||||||
displayId = options.getDisplayId();
|
displayId = options.getDisplayId();
|
||||||
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
|
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
|
||||||
if (displayInfo == null) {
|
if (displayInfo == null) {
|
||||||
int[] displayIds = ServiceManager.getDisplayManager().getDisplayIds();
|
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
|
||||||
throw new InvalidDisplayIdException(displayId, displayIds);
|
throw new ConfigurationException("Unknown display id: " + displayId);
|
||||||
}
|
}
|
||||||
|
|
||||||
int displayInfoFlags = displayInfo.getFlags();
|
int displayInfoFlags = displayInfo.getFlags();
|
||||||
|
41
server/src/main/java/com/genymobile/scrcpy/FakeContext.java
Normal file
41
server/src/main/java/com/genymobile/scrcpy/FakeContext.java
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.AttributionSource;
|
||||||
|
import android.content.ContextWrapper;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Process;
|
||||||
|
|
||||||
|
public final class FakeContext extends ContextWrapper {
|
||||||
|
|
||||||
|
public static final String PACKAGE_NAME = "com.android.shell";
|
||||||
|
public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29
|
||||||
|
|
||||||
|
private static final FakeContext INSTANCE = new FakeContext();
|
||||||
|
|
||||||
|
public static FakeContext get() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FakeContext() {
|
||||||
|
super(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPackageName() {
|
||||||
|
return PACKAGE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOpPackageName() {
|
||||||
|
return PACKAGE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.S)
|
||||||
|
@Override
|
||||||
|
public AttributionSource getAttributionSource() {
|
||||||
|
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
|
||||||
|
builder.setPackageName(PACKAGE_NAME);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +0,0 @@
|
|||||||
package com.genymobile.scrcpy;
|
|
||||||
|
|
||||||
public class InvalidDisplayIdException extends RuntimeException {
|
|
||||||
|
|
||||||
private final int displayId;
|
|
||||||
private final int[] availableDisplayIds;
|
|
||||||
|
|
||||||
public InvalidDisplayIdException(int displayId, int[] availableDisplayIds) {
|
|
||||||
super("There is no display having id " + displayId);
|
|
||||||
this.displayId = displayId;
|
|
||||||
this.availableDisplayIds = availableDisplayIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDisplayId() {
|
|
||||||
return displayId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int[] getAvailableDisplayIds() {
|
|
||||||
return availableDisplayIds;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
package com.genymobile.scrcpy;
|
|
||||||
|
|
||||||
import android.media.MediaCodecInfo;
|
|
||||||
|
|
||||||
public class InvalidEncoderException extends RuntimeException {
|
|
||||||
|
|
||||||
private final String name;
|
|
||||||
private final MediaCodecInfo[] availableEncoders;
|
|
||||||
|
|
||||||
public InvalidEncoderException(String name, MediaCodecInfo[] availableEncoders) {
|
|
||||||
super("There is no encoder having name '" + name + "'");
|
|
||||||
this.name = name;
|
|
||||||
this.availableEncoders = availableEncoders;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MediaCodecInfo[] getAvailableEncoders() {
|
|
||||||
return availableEncoders;
|
|
||||||
}
|
|
||||||
}
|
|
63
server/src/main/java/com/genymobile/scrcpy/LogUtils.java
Normal file
63
server/src/main/java/com/genymobile/scrcpy/LogUtils.java
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.wrappers.DisplayManager;
|
||||||
|
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class LogUtils {
|
||||||
|
|
||||||
|
private LogUtils() {
|
||||||
|
// not instantiable
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String buildVideoEncoderListMessage() {
|
||||||
|
StringBuilder builder = new StringBuilder("List of video encoders:");
|
||||||
|
List<CodecUtils.DeviceEncoder> videoEncoders = CodecUtils.listVideoEncoders();
|
||||||
|
if (videoEncoders.isEmpty()) {
|
||||||
|
builder.append("\n (none)");
|
||||||
|
} else {
|
||||||
|
for (CodecUtils.DeviceEncoder encoder : videoEncoders) {
|
||||||
|
builder.append("\n --video-codec=").append(encoder.getCodec().getName());
|
||||||
|
builder.append(" --video-encoder='").append(encoder.getInfo().getName()).append("'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String buildAudioEncoderListMessage() {
|
||||||
|
StringBuilder builder = new StringBuilder("List of audio encoders:");
|
||||||
|
List<CodecUtils.DeviceEncoder> audioEncoders = CodecUtils.listAudioEncoders();
|
||||||
|
if (audioEncoders.isEmpty()) {
|
||||||
|
builder.append("\n (none)");
|
||||||
|
} else {
|
||||||
|
for (CodecUtils.DeviceEncoder encoder : audioEncoders) {
|
||||||
|
builder.append("\n --audio-codec=").append(encoder.getCodec().getName());
|
||||||
|
builder.append(" --audio-encoder='").append(encoder.getInfo().getName()).append("'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String buildDisplayListMessage() {
|
||||||
|
StringBuilder builder = new StringBuilder("List of displays:");
|
||||||
|
DisplayManager displayManager = ServiceManager.getDisplayManager();
|
||||||
|
int[] displayIds = displayManager.getDisplayIds();
|
||||||
|
if (displayIds == null || displayIds.length == 0) {
|
||||||
|
builder.append("\n (none)");
|
||||||
|
} else {
|
||||||
|
for (int id : displayIds) {
|
||||||
|
builder.append("\n --display=").append(id).append(" (");
|
||||||
|
DisplayInfo displayInfo = displayManager.getDisplayInfo(id);
|
||||||
|
if (displayInfo != null) {
|
||||||
|
Size size = displayInfo.getSize();
|
||||||
|
builder.append(size.getWidth()).append("x").append(size.getHeight());
|
||||||
|
} else {
|
||||||
|
builder.append("size unknown");
|
||||||
|
}
|
||||||
|
builder.append(")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -8,9 +8,12 @@ public class Options {
|
|||||||
|
|
||||||
private Ln.Level logLevel = Ln.Level.DEBUG;
|
private Ln.Level logLevel = Ln.Level.DEBUG;
|
||||||
private int scid = -1; // 31-bit non-negative value, or -1
|
private int scid = -1; // 31-bit non-negative value, or -1
|
||||||
|
private boolean audio = true;
|
||||||
private int maxSize;
|
private int maxSize;
|
||||||
private VideoCodec codec = VideoCodec.H264;
|
private VideoCodec videoCodec = VideoCodec.H264;
|
||||||
private int bitRate = 8000000;
|
private AudioCodec audioCodec = AudioCodec.OPUS;
|
||||||
|
private int videoBitRate = 8000000;
|
||||||
|
private int audioBitRate = 196000;
|
||||||
private int maxFps;
|
private int maxFps;
|
||||||
private int lockVideoOrientation = -1;
|
private int lockVideoOrientation = -1;
|
||||||
private boolean tunnelForward;
|
private boolean tunnelForward;
|
||||||
@ -19,14 +22,20 @@ public class Options {
|
|||||||
private int displayId;
|
private int displayId;
|
||||||
private boolean showTouches;
|
private boolean showTouches;
|
||||||
private boolean stayAwake;
|
private boolean stayAwake;
|
||||||
private List<CodecOption> codecOptions;
|
private List<CodecOption> videoCodecOptions;
|
||||||
private String encoderName;
|
private List<CodecOption> audioCodecOptions;
|
||||||
|
|
||||||
|
private String videoEncoder;
|
||||||
|
private String audioEncoder;
|
||||||
private boolean powerOffScreenOnClose;
|
private boolean powerOffScreenOnClose;
|
||||||
private boolean clipboardAutosync = true;
|
private boolean clipboardAutosync = true;
|
||||||
private boolean downsizeOnError = true;
|
private boolean downsizeOnError = true;
|
||||||
private boolean cleanup = true;
|
private boolean cleanup = true;
|
||||||
private boolean powerOn = true;
|
private boolean powerOn = true;
|
||||||
|
|
||||||
|
private boolean listEncoders;
|
||||||
|
private boolean listDisplays;
|
||||||
|
|
||||||
// Options not used by the scrcpy client, but useful to use scrcpy-server directly
|
// Options not used by the scrcpy client, but useful to use scrcpy-server directly
|
||||||
private boolean sendDeviceMeta = true; // send device name and size
|
private boolean sendDeviceMeta = true; // send device name and size
|
||||||
private boolean sendFrameMeta = true; // send PTS so that the client may record properly
|
private boolean sendFrameMeta = true; // send PTS so that the client may record properly
|
||||||
@ -49,6 +58,14 @@ public class Options {
|
|||||||
this.scid = scid;
|
this.scid = scid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean getAudio() {
|
||||||
|
return audio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAudio(boolean audio) {
|
||||||
|
this.audio = audio;
|
||||||
|
}
|
||||||
|
|
||||||
public int getMaxSize() {
|
public int getMaxSize() {
|
||||||
return maxSize;
|
return maxSize;
|
||||||
}
|
}
|
||||||
@ -57,20 +74,36 @@ public class Options {
|
|||||||
this.maxSize = maxSize;
|
this.maxSize = maxSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
public VideoCodec getCodec() {
|
public VideoCodec getVideoCodec() {
|
||||||
return codec;
|
return videoCodec;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCodec(VideoCodec codec) {
|
public void setVideoCodec(VideoCodec videoCodec) {
|
||||||
this.codec = codec;
|
this.videoCodec = videoCodec;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getBitRate() {
|
public AudioCodec getAudioCodec() {
|
||||||
return bitRate;
|
return audioCodec;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBitRate(int bitRate) {
|
public void setAudioCodec(AudioCodec audioCodec) {
|
||||||
this.bitRate = bitRate;
|
this.audioCodec = audioCodec;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getVideoBitRate() {
|
||||||
|
return videoBitRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVideoBitRate(int videoBitRate) {
|
||||||
|
this.videoBitRate = videoBitRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAudioBitRate() {
|
||||||
|
return audioBitRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAudioBitRate(int audioBitRate) {
|
||||||
|
this.audioBitRate = audioBitRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getMaxFps() {
|
public int getMaxFps() {
|
||||||
@ -137,20 +170,36 @@ public class Options {
|
|||||||
this.stayAwake = stayAwake;
|
this.stayAwake = stayAwake;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<CodecOption> getCodecOptions() {
|
public List<CodecOption> getVideoCodecOptions() {
|
||||||
return codecOptions;
|
return videoCodecOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCodecOptions(List<CodecOption> codecOptions) {
|
public void setVideoCodecOptions(List<CodecOption> videoCodecOptions) {
|
||||||
this.codecOptions = codecOptions;
|
this.videoCodecOptions = videoCodecOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getEncoderName() {
|
public List<CodecOption> getAudioCodecOptions() {
|
||||||
return encoderName;
|
return audioCodecOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEncoderName(String encoderName) {
|
public void setAudioCodecOptions(List<CodecOption> audioCodecOptions) {
|
||||||
this.encoderName = encoderName;
|
this.audioCodecOptions = audioCodecOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVideoEncoder() {
|
||||||
|
return videoEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVideoEncoder(String videoEncoder) {
|
||||||
|
this.videoEncoder = videoEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAudioEncoder() {
|
||||||
|
return audioEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAudioEncoder(String audioEncoder) {
|
||||||
|
this.audioEncoder = audioEncoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) {
|
public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) {
|
||||||
@ -193,6 +242,22 @@ public class Options {
|
|||||||
this.powerOn = powerOn;
|
this.powerOn = powerOn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean getListEncoders() {
|
||||||
|
return listEncoders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setListEncoders(boolean listEncoders) {
|
||||||
|
this.listEncoders = listEncoders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getListDisplays() {
|
||||||
|
return listDisplays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setListDisplays(boolean listDisplays) {
|
||||||
|
this.listDisplays = listDisplays;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean getSendDeviceMeta() {
|
public boolean getSendDeviceMeta() {
|
||||||
return sendDeviceMeta;
|
return sendDeviceMeta;
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
|||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
import android.media.MediaCodecList;
|
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
@ -14,17 +13,11 @@ import android.view.Surface;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public class ScreenEncoder implements Device.RotationListener {
|
public class ScreenEncoder implements Device.RotationListener {
|
||||||
|
|
||||||
public interface Callbacks {
|
|
||||||
void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
|
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
|
||||||
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
|
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
|
||||||
private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder";
|
private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder";
|
||||||
@ -35,19 +28,22 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
|
|
||||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||||
|
|
||||||
private final String videoMimeType;
|
private final Device device;
|
||||||
|
private final Streamer streamer;
|
||||||
private final String encoderName;
|
private final String encoderName;
|
||||||
private final List<CodecOption> codecOptions;
|
private final List<CodecOption> codecOptions;
|
||||||
private final int bitRate;
|
private final int videoBitRate;
|
||||||
private final int maxFps;
|
private final int maxFps;
|
||||||
private final boolean downsizeOnError;
|
private final boolean downsizeOnError;
|
||||||
|
|
||||||
private boolean firstFrameSent;
|
private boolean firstFrameSent;
|
||||||
private int consecutiveErrors;
|
private int consecutiveErrors;
|
||||||
|
|
||||||
public ScreenEncoder(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName, boolean downsizeOnError) {
|
public ScreenEncoder(Device device, Streamer streamer, int videoBitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
|
||||||
this.videoMimeType = videoMimeType;
|
boolean downsizeOnError) {
|
||||||
this.bitRate = bitRate;
|
this.device = device;
|
||||||
|
this.streamer = streamer;
|
||||||
|
this.videoBitRate = videoBitRate;
|
||||||
this.maxFps = maxFps;
|
this.maxFps = maxFps;
|
||||||
this.codecOptions = codecOptions;
|
this.codecOptions = codecOptions;
|
||||||
this.encoderName = encoderName;
|
this.encoderName = encoderName;
|
||||||
@ -63,11 +59,15 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
return rotationChanged.getAndSet(false);
|
return rotationChanged.getAndSet(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void streamScreen(Device device, Callbacks callbacks) throws IOException {
|
public void streamScreen() throws IOException, ConfigurationException {
|
||||||
MediaCodec codec = createCodec(videoMimeType, encoderName);
|
Codec codec = streamer.getCodec();
|
||||||
MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions);
|
MediaCodec mediaCodec = createMediaCodec(codec, encoderName);
|
||||||
|
MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions);
|
||||||
IBinder display = createDisplay();
|
IBinder display = createDisplay();
|
||||||
device.setRotationListener(this);
|
device.setRotationListener(this);
|
||||||
|
|
||||||
|
streamer.writeHeader();
|
||||||
|
|
||||||
boolean alive;
|
boolean alive;
|
||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
@ -81,8 +81,8 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
|
|
||||||
Surface surface = null;
|
Surface surface = null;
|
||||||
try {
|
try {
|
||||||
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||||
surface = codec.createInputSurface();
|
surface = mediaCodec.createInputSurface();
|
||||||
|
|
||||||
// does not include the locked video orientation
|
// does not include the locked video orientation
|
||||||
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
|
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
|
||||||
@ -90,11 +90,11 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
int layerStack = device.getLayerStack();
|
int layerStack = device.getLayerStack();
|
||||||
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
|
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
|
||||||
|
|
||||||
codec.start();
|
mediaCodec.start();
|
||||||
|
|
||||||
alive = encode(codec, callbacks);
|
alive = encode(mediaCodec, streamer);
|
||||||
// do not call stop() on exception, it would trigger an IllegalStateException
|
// do not call stop() on exception, it would trigger an IllegalStateException
|
||||||
codec.stop();
|
mediaCodec.stop();
|
||||||
} catch (IllegalStateException | IllegalArgumentException e) {
|
} catch (IllegalStateException | IllegalArgumentException e) {
|
||||||
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
|
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
|
||||||
if (!prepareRetry(device, screenInfo)) {
|
if (!prepareRetry(device, screenInfo)) {
|
||||||
@ -103,14 +103,14 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
Ln.i("Retrying...");
|
Ln.i("Retrying...");
|
||||||
alive = true;
|
alive = true;
|
||||||
} finally {
|
} finally {
|
||||||
codec.reset();
|
mediaCodec.reset();
|
||||||
if (surface != null) {
|
if (surface != null) {
|
||||||
surface.release();
|
surface.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} while (alive);
|
} while (alive);
|
||||||
} finally {
|
} finally {
|
||||||
codec.release();
|
mediaCodec.release();
|
||||||
device.setRotationListener(null);
|
device.setRotationListener(null);
|
||||||
SurfaceControl.destroyDisplay(display);
|
SurfaceControl.destroyDisplay(display);
|
||||||
}
|
}
|
||||||
@ -161,7 +161,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean encode(MediaCodec codec, Callbacks callbacks) throws IOException {
|
private boolean encode(MediaCodec codec, Streamer streamer) throws IOException {
|
||||||
boolean eof = false;
|
boolean eof = false;
|
||||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||||
|
|
||||||
@ -184,7 +184,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
consecutiveErrors = 0;
|
consecutiveErrors = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
callbacks.onPacket(codecBuffer, bufferInfo);
|
streamer.writePacket(codecBuffer, bufferInfo);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (outputBufferId >= 0) {
|
if (outputBufferId >= 0) {
|
||||||
@ -196,47 +196,19 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
return !eof;
|
return !eof;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaCodecInfo[] listEncoders(String videoMimeType) {
|
private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException {
|
||||||
List<MediaCodecInfo> result = new ArrayList<>();
|
|
||||||
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
|
||||||
for (MediaCodecInfo codecInfo : list.getCodecInfos()) {
|
|
||||||
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(videoMimeType)) {
|
|
||||||
result.add(codecInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toArray(new MediaCodecInfo[result.size()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaCodec createCodec(String videoMimeType, String encoderName) throws IOException {
|
|
||||||
if (encoderName != null) {
|
if (encoderName != null) {
|
||||||
Ln.d("Creating encoder by name: '" + encoderName + "'");
|
Ln.d("Creating encoder by name: '" + encoderName + "'");
|
||||||
try {
|
try {
|
||||||
return MediaCodec.createByCodecName(encoderName);
|
return MediaCodec.createByCodecName(encoderName);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
MediaCodecInfo[] encoders = listEncoders(videoMimeType);
|
Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage());
|
||||||
throw new InvalidEncoderException(encoderName, encoders);
|
throw new ConfigurationException("Unknown encoder: " + encoderName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MediaCodec codec = MediaCodec.createEncoderByType(videoMimeType);
|
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
||||||
Ln.d("Using encoder: '" + codec.getName() + "'");
|
Ln.d("Using encoder: '" + mediaCodec.getName() + "'");
|
||||||
return codec;
|
return mediaCodec;
|
||||||
}
|
|
||||||
|
|
||||||
private static void setCodecOption(MediaFormat format, CodecOption codecOption) {
|
|
||||||
String key = codecOption.getKey();
|
|
||||||
Object value = codecOption.getValue();
|
|
||||||
|
|
||||||
if (value instanceof Integer) {
|
|
||||||
format.setInteger(key, (Integer) value);
|
|
||||||
} else if (value instanceof Long) {
|
|
||||||
format.setLong(key, (Long) value);
|
|
||||||
} else if (value instanceof Float) {
|
|
||||||
format.setFloat(key, (Float) value);
|
|
||||||
} else if (value instanceof String) {
|
|
||||||
format.setString(key, (String) value);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||||
@ -258,7 +230,10 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
|
|
||||||
if (codecOptions != null) {
|
if (codecOptions != null) {
|
||||||
for (CodecOption option : codecOptions) {
|
for (CodecOption option : codecOptions) {
|
||||||
setCodecOption(format, option);
|
String key = option.getKey();
|
||||||
|
Object value = option.getValue();
|
||||||
|
CodecUtils.setCodecOption(format, key, value);
|
||||||
|
Ln.d("Video codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.media.MediaCodecInfo;
|
|
||||||
import android.os.BatteryManager;
|
import android.os.BatteryManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
@ -59,41 +58,47 @@ public final class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void scrcpy(Options options) throws IOException {
|
private static void scrcpy(Options options) throws IOException, ConfigurationException {
|
||||||
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
||||||
final Device device = new Device(options);
|
final Device device = new Device(options);
|
||||||
List<CodecOption> codecOptions = options.getCodecOptions();
|
|
||||||
|
|
||||||
Thread initThread = startInitThread(options);
|
Thread initThread = startInitThread(options);
|
||||||
|
|
||||||
int scid = options.getScid();
|
int scid = options.getScid();
|
||||||
boolean tunnelForward = options.isTunnelForward();
|
boolean tunnelForward = options.isTunnelForward();
|
||||||
boolean control = options.getControl();
|
boolean control = options.getControl();
|
||||||
|
boolean audio = options.getAudio();
|
||||||
boolean sendDummyByte = options.getSendDummyByte();
|
boolean sendDummyByte = options.getSendDummyByte();
|
||||||
|
|
||||||
Workarounds.prepareMainLooper();
|
Workarounds.prepareMainLooper();
|
||||||
if (Build.BRAND.equalsIgnoreCase("meizu")) {
|
|
||||||
// Workarounds must be applied for Meizu phones:
|
// Workarounds must be applied for Meizu phones:
|
||||||
// - <https://github.com/Genymobile/scrcpy/issues/240>
|
// - <https://github.com/Genymobile/scrcpy/issues/240>
|
||||||
// - <https://github.com/Genymobile/scrcpy/issues/365>
|
// - <https://github.com/Genymobile/scrcpy/issues/365>
|
||||||
// - <https://github.com/Genymobile/scrcpy/issues/2656>
|
// - <https://github.com/Genymobile/scrcpy/issues/2656>
|
||||||
//
|
//
|
||||||
// But only apply when strictly necessary, since workarounds can cause other issues:
|
// But only apply when strictly necessary, since workarounds can cause other issues:
|
||||||
// - <https://github.com/Genymobile/scrcpy/issues/940>
|
// - <https://github.com/Genymobile/scrcpy/issues/940>
|
||||||
// - <https://github.com/Genymobile/scrcpy/issues/994>
|
// - <https://github.com/Genymobile/scrcpy/issues/994>
|
||||||
|
boolean mustFillAppInfo = Build.BRAND.equalsIgnoreCase("meizu");
|
||||||
|
|
||||||
|
// Before Android 11, audio is not supported.
|
||||||
|
// Since Android 12, we can properly set a context on the AudioRecord.
|
||||||
|
// Only on Android 11 we must fill app info for the AudioRecord to work.
|
||||||
|
mustFillAppInfo |= audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R;
|
||||||
|
|
||||||
|
if (mustFillAppInfo) {
|
||||||
Workarounds.fillAppInfo();
|
Workarounds.fillAppInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
Controller controller = null;
|
Controller controller = null;
|
||||||
|
AudioEncoder audioEncoder = null;
|
||||||
|
|
||||||
try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, control, sendDummyByte)) {
|
try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) {
|
||||||
VideoCodec codec = options.getCodec();
|
|
||||||
if (options.getSendDeviceMeta()) {
|
if (options.getSendDeviceMeta()) {
|
||||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||||
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
||||||
}
|
}
|
||||||
ScreenEncoder screenEncoder = new ScreenEncoder(codec.getMimeType(), options.getBitRate(), options.getMaxFps(), codecOptions,
|
|
||||||
options.getEncoderName(), options.getDownsizeOnError());
|
|
||||||
|
|
||||||
if (control) {
|
if (control) {
|
||||||
controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
|
controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
|
||||||
@ -103,13 +108,20 @@ public final class Server {
|
|||||||
device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text));
|
device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (audio) {
|
||||||
|
Streamer audioStreamer = new Streamer(connection.getAudioFd(), options.getAudioCodec(), options.getSendCodecId(),
|
||||||
|
options.getSendFrameMeta());
|
||||||
|
audioEncoder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(), options.getAudioEncoder());
|
||||||
|
audioEncoder.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecId(),
|
||||||
|
options.getSendFrameMeta());
|
||||||
|
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
|
||||||
|
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
|
||||||
try {
|
try {
|
||||||
// synchronous
|
// synchronous
|
||||||
VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), options.getSendFrameMeta());
|
screenEncoder.streamScreen();
|
||||||
if (options.getSendCodecId()) {
|
|
||||||
videoStreamer.writeHeader(codec.getId());
|
|
||||||
}
|
|
||||||
screenEncoder.streamScreen(device, videoStreamer);
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// Broken pipe is expected on close, because the socket is closed by the client
|
// Broken pipe is expected on close, because the socket is closed by the client
|
||||||
if (!IO.isBrokenPipe(e)) {
|
if (!IO.isBrokenPipe(e)) {
|
||||||
@ -119,12 +131,18 @@ public final class Server {
|
|||||||
} finally {
|
} finally {
|
||||||
Ln.d("Screen streaming stopped");
|
Ln.d("Screen streaming stopped");
|
||||||
initThread.interrupt();
|
initThread.interrupt();
|
||||||
|
if (audioEncoder != null) {
|
||||||
|
audioEncoder.stop();
|
||||||
|
}
|
||||||
if (controller != null) {
|
if (controller != null) {
|
||||||
controller.stop();
|
controller.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
initThread.join();
|
initThread.join();
|
||||||
|
if (audioEncoder != null) {
|
||||||
|
audioEncoder.join();
|
||||||
|
}
|
||||||
if (controller != null) {
|
if (controller != null) {
|
||||||
controller.join();
|
controller.join();
|
||||||
}
|
}
|
||||||
@ -140,6 +158,7 @@ public final class Server {
|
|||||||
return thread;
|
return thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("MethodLength")
|
||||||
private static Options createOptions(String... args) {
|
private static Options createOptions(String... args) {
|
||||||
if (args.length < 1) {
|
if (args.length < 1) {
|
||||||
throw new IllegalArgumentException("Missing client version");
|
throw new IllegalArgumentException("Missing client version");
|
||||||
@ -173,20 +192,35 @@ public final class Server {
|
|||||||
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
|
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
|
||||||
options.setLogLevel(level);
|
options.setLogLevel(level);
|
||||||
break;
|
break;
|
||||||
case "codec":
|
case "audio":
|
||||||
VideoCodec codec = VideoCodec.findByName(value);
|
boolean audio = Boolean.parseBoolean(value);
|
||||||
if (codec == null) {
|
options.setAudio(audio);
|
||||||
|
break;
|
||||||
|
case "video_codec":
|
||||||
|
VideoCodec videoCodec = VideoCodec.findByName(value);
|
||||||
|
if (videoCodec == null) {
|
||||||
throw new IllegalArgumentException("Video codec " + value + " not supported");
|
throw new IllegalArgumentException("Video codec " + value + " not supported");
|
||||||
}
|
}
|
||||||
options.setCodec(codec);
|
options.setVideoCodec(videoCodec);
|
||||||
|
break;
|
||||||
|
case "audio_codec":
|
||||||
|
AudioCodec audioCodec = AudioCodec.findByName(value);
|
||||||
|
if (audioCodec == null) {
|
||||||
|
throw new IllegalArgumentException("Audio codec " + value + " not supported");
|
||||||
|
}
|
||||||
|
options.setAudioCodec(audioCodec);
|
||||||
break;
|
break;
|
||||||
case "max_size":
|
case "max_size":
|
||||||
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
|
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
|
||||||
options.setMaxSize(maxSize);
|
options.setMaxSize(maxSize);
|
||||||
break;
|
break;
|
||||||
case "bit_rate":
|
case "video_bit_rate":
|
||||||
int bitRate = Integer.parseInt(value);
|
int videoBitRate = Integer.parseInt(value);
|
||||||
options.setBitRate(bitRate);
|
options.setVideoBitRate(videoBitRate);
|
||||||
|
break;
|
||||||
|
case "audio_bit_rate":
|
||||||
|
int audioBitRate = Integer.parseInt(value);
|
||||||
|
options.setAudioBitRate(audioBitRate);
|
||||||
break;
|
break;
|
||||||
case "max_fps":
|
case "max_fps":
|
||||||
int maxFps = Integer.parseInt(value);
|
int maxFps = Integer.parseInt(value);
|
||||||
@ -220,15 +254,23 @@ public final class Server {
|
|||||||
boolean stayAwake = Boolean.parseBoolean(value);
|
boolean stayAwake = Boolean.parseBoolean(value);
|
||||||
options.setStayAwake(stayAwake);
|
options.setStayAwake(stayAwake);
|
||||||
break;
|
break;
|
||||||
case "codec_options":
|
case "video_codec_options":
|
||||||
List<CodecOption> codecOptions = CodecOption.parse(value);
|
List<CodecOption> videoCodecOptions = CodecOption.parse(value);
|
||||||
options.setCodecOptions(codecOptions);
|
options.setVideoCodecOptions(videoCodecOptions);
|
||||||
break;
|
break;
|
||||||
case "encoder_name":
|
case "audio_codec_options":
|
||||||
|
List<CodecOption> audioCodecOptions = CodecOption.parse(value);
|
||||||
|
options.setAudioCodecOptions(audioCodecOptions);
|
||||||
|
break;
|
||||||
|
case "video_encoder":
|
||||||
if (!value.isEmpty()) {
|
if (!value.isEmpty()) {
|
||||||
options.setEncoderName(value);
|
options.setVideoEncoder(value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "audio_encoder":
|
||||||
|
if (!value.isEmpty()) {
|
||||||
|
options.setAudioEncoder(value);
|
||||||
|
}
|
||||||
case "power_off_on_close":
|
case "power_off_on_close":
|
||||||
boolean powerOffScreenOnClose = Boolean.parseBoolean(value);
|
boolean powerOffScreenOnClose = Boolean.parseBoolean(value);
|
||||||
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
|
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
|
||||||
@ -249,6 +291,14 @@ public final class Server {
|
|||||||
boolean powerOn = Boolean.parseBoolean(value);
|
boolean powerOn = Boolean.parseBoolean(value);
|
||||||
options.setPowerOn(powerOn);
|
options.setPowerOn(powerOn);
|
||||||
break;
|
break;
|
||||||
|
case "list_encoders":
|
||||||
|
boolean listEncoders = Boolean.parseBoolean(value);
|
||||||
|
options.setListEncoders(listEncoders);
|
||||||
|
break;
|
||||||
|
case "list_displays":
|
||||||
|
boolean listDisplays = Boolean.parseBoolean(value);
|
||||||
|
options.setListDisplays(listDisplays);
|
||||||
|
break;
|
||||||
case "send_device_meta":
|
case "send_device_meta":
|
||||||
boolean sendDeviceMeta = Boolean.parseBoolean(value);
|
boolean sendDeviceMeta = Boolean.parseBoolean(value);
|
||||||
options.setSendDeviceMeta(sendDeviceMeta);
|
options.setSendDeviceMeta(sendDeviceMeta);
|
||||||
@ -299,38 +349,35 @@ public final class Server {
|
|||||||
return new Rect(x, y, x + width, y + height);
|
return new Rect(x, y, x + width, y + height);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void suggestFix(Throwable e) {
|
|
||||||
if (e instanceof InvalidDisplayIdException) {
|
|
||||||
InvalidDisplayIdException idie = (InvalidDisplayIdException) e;
|
|
||||||
int[] displayIds = idie.getAvailableDisplayIds();
|
|
||||||
if (displayIds != null && displayIds.length > 0) {
|
|
||||||
Ln.e("Try to use one of the available display ids:");
|
|
||||||
for (int id : displayIds) {
|
|
||||||
Ln.e(" scrcpy --display=" + id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (e instanceof InvalidEncoderException) {
|
|
||||||
InvalidEncoderException iee = (InvalidEncoderException) e;
|
|
||||||
MediaCodecInfo[] encoders = iee.getAvailableEncoders();
|
|
||||||
if (encoders != null && encoders.length > 0) {
|
|
||||||
Ln.e("Try to use one of the available encoders:");
|
|
||||||
for (MediaCodecInfo encoder : encoders) {
|
|
||||||
Ln.e(" scrcpy --encoder='" + encoder.getName() + "'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String... args) throws Exception {
|
public static void main(String... args) throws Exception {
|
||||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
||||||
Ln.e("Exception on thread " + t, e);
|
Ln.e("Exception on thread " + t, e);
|
||||||
suggestFix(e);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Options options = createOptions(args);
|
Options options = createOptions(args);
|
||||||
|
|
||||||
Ln.initLogLevel(options.getLogLevel());
|
Ln.initLogLevel(options.getLogLevel());
|
||||||
|
|
||||||
scrcpy(options);
|
if (options.getListEncoders() || options.getListDisplays()) {
|
||||||
|
if (options.getCleanup()) {
|
||||||
|
CleanUp.unlinkSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.getListEncoders()) {
|
||||||
|
Ln.i(LogUtils.buildVideoEncoderListMessage());
|
||||||
|
Ln.i(LogUtils.buildAudioEncoderListMessage());
|
||||||
|
}
|
||||||
|
if (options.getListDisplays()) {
|
||||||
|
Ln.i(LogUtils.buildDisplayListMessage());
|
||||||
|
}
|
||||||
|
// Just print the requested data, do not mirror
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
scrcpy(options);
|
||||||
|
} catch (ConfigurationException e) {
|
||||||
|
// Do not print stack trace, a user-friendly error-message has already been logged
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
129
server/src/main/java/com/genymobile/scrcpy/Streamer.java
Normal file
129
server/src/main/java/com/genymobile/scrcpy/Streamer.java
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import android.media.MediaCodec;
|
||||||
|
|
||||||
|
import java.io.FileDescriptor;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public final class Streamer {
|
||||||
|
|
||||||
|
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
||||||
|
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
||||||
|
|
||||||
|
private static final long AOPUSHDR = 0x5244485355504F41L; // "AOPUSHDR" in ASCII (little-endian)
|
||||||
|
|
||||||
|
private final FileDescriptor fd;
|
||||||
|
private final Codec codec;
|
||||||
|
private final boolean sendCodecId;
|
||||||
|
private final boolean sendFrameMeta;
|
||||||
|
|
||||||
|
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||||
|
|
||||||
|
public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecId, boolean sendFrameMeta) {
|
||||||
|
this.fd = fd;
|
||||||
|
this.codec = codec;
|
||||||
|
this.sendCodecId = sendCodecId;
|
||||||
|
this.sendFrameMeta = sendFrameMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Codec getCodec() {
|
||||||
|
return codec;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeHeader() throws IOException {
|
||||||
|
if (sendCodecId) {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||||
|
buffer.putInt(codec.getId());
|
||||||
|
buffer.flip();
|
||||||
|
IO.writeFully(fd, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeDisableStream(boolean error) throws IOException {
|
||||||
|
// Writing a specific code as codec-id means that the device disables the stream
|
||||||
|
// code 0: it explicitly disables the stream (because it could not capture audio), scrcpy should continue mirroring video only
|
||||||
|
// code 1: a configuration error occurred, scrcpy must be stopped
|
||||||
|
byte[] code = new byte[4];
|
||||||
|
if (error) {
|
||||||
|
code[3] = 1;
|
||||||
|
}
|
||||||
|
IO.writeFully(fd, code, 0, code.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writePacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
|
||||||
|
if (codec == AudioCodec.OPUS) {
|
||||||
|
fixOpusConfigPacket(codecBuffer, bufferInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendFrameMeta) {
|
||||||
|
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
||||||
|
}
|
||||||
|
|
||||||
|
IO.writeFully(fd, codecBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
|
||||||
|
headerBuffer.clear();
|
||||||
|
|
||||||
|
long ptsAndFlags;
|
||||||
|
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
||||||
|
ptsAndFlags = PACKET_FLAG_CONFIG; // non-media data packet
|
||||||
|
} else {
|
||||||
|
ptsAndFlags = bufferInfo.presentationTimeUs;
|
||||||
|
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
|
||||||
|
ptsAndFlags |= PACKET_FLAG_KEY_FRAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headerBuffer.putLong(ptsAndFlags);
|
||||||
|
headerBuffer.putInt(packetSize);
|
||||||
|
headerBuffer.flip();
|
||||||
|
IO.writeFully(fd, headerBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void fixOpusConfigPacket(ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
|
||||||
|
// Here is an example of the config packet received for an OPUS stream:
|
||||||
|
//
|
||||||
|
// 00000000 41 4f 50 55 53 48 44 52 13 00 00 00 00 00 00 00 |AOPUSHDR........|
|
||||||
|
// -------------- BELOW IS THE PART WE MUST PUT AS EXTRADATA -------------------
|
||||||
|
// 00000010 4f 70 75 73 48 65 61 64 01 01 38 01 80 bb 00 00 |OpusHead..8.....|
|
||||||
|
// 00000020 00 00 00 |... |
|
||||||
|
// ------------------------------------------------------------------------------
|
||||||
|
// 00000020 41 4f 50 55 53 44 4c 59 08 00 00 00 00 | AOPUSDLY.....|
|
||||||
|
// 00000030 00 00 00 a0 2e 63 00 00 00 00 00 41 4f 50 55 53 |.....c.....AOPUS|
|
||||||
|
// 00000040 50 52 4c 08 00 00 00 00 00 00 00 00 b4 c4 04 00 |PRL.............|
|
||||||
|
// 00000050 00 00 00 |...|
|
||||||
|
//
|
||||||
|
// Each "section" is prefixed by a 64-bit ID and a 64-bit length.
|
||||||
|
//
|
||||||
|
// <https://developer.android.com/reference/android/media/MediaCodec#CSD>
|
||||||
|
|
||||||
|
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
|
||||||
|
if (!isConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer.remaining() < 16) {
|
||||||
|
throw new IOException("Not enough data in OPUS config packet");
|
||||||
|
}
|
||||||
|
|
||||||
|
long id = buffer.getLong();
|
||||||
|
if (id != AOPUSHDR) {
|
||||||
|
throw new IOException("OPUS header not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
long sizeLong = buffer.getLong();
|
||||||
|
if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) {
|
||||||
|
throw new IOException("Invalid block size in OPUS header: " + sizeLong);
|
||||||
|
}
|
||||||
|
|
||||||
|
int size = (int) sizeLong;
|
||||||
|
if (buffer.remaining() < size) {
|
||||||
|
throw new IOException("Not enough data in OPUS header (invalid size: " + size + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the buffer to point to the OPUS header slice
|
||||||
|
buffer.limit(buffer.position() + size);
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,12 @@
|
|||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
|
|
||||||
public enum VideoCodec {
|
public enum VideoCodec implements Codec {
|
||||||
H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC),
|
H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC),
|
||||||
H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC),
|
H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC),
|
||||||
|
@SuppressLint("InlinedApi") // introduced in API 21
|
||||||
AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1);
|
AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1);
|
||||||
|
|
||||||
private final int id; // 4-byte ASCII representation of the name
|
private final int id; // 4-byte ASCII representation of the name
|
||||||
@ -17,10 +19,22 @@ public enum VideoCodec {
|
|||||||
this.mimeType = mimeType;
|
this.mimeType = mimeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type getType() {
|
||||||
|
return Type.VIDEO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int getId() {
|
public int getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getMimeType() {
|
public String getMimeType() {
|
||||||
return mimeType;
|
return mimeType;
|
||||||
}
|
}
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
package com.genymobile.scrcpy;
|
|
||||||
|
|
||||||
import android.media.MediaCodec;
|
|
||||||
|
|
||||||
import java.io.FileDescriptor;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
public final class VideoStreamer implements ScreenEncoder.Callbacks {
|
|
||||||
|
|
||||||
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
|
||||||
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
|
||||||
|
|
||||||
private final FileDescriptor fd;
|
|
||||||
private final boolean sendFrameMeta;
|
|
||||||
|
|
||||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
|
||||||
|
|
||||||
public VideoStreamer(FileDescriptor fd, boolean sendFrameMeta) {
|
|
||||||
this.fd = fd;
|
|
||||||
this.sendFrameMeta = sendFrameMeta;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void writeHeader(int codecId) throws IOException {
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
|
||||||
buffer.putInt(codecId);
|
|
||||||
buffer.flip();
|
|
||||||
IO.writeFully(fd, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
|
|
||||||
if (sendFrameMeta) {
|
|
||||||
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
|
||||||
}
|
|
||||||
|
|
||||||
IO.writeFully(fd, codecBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
|
|
||||||
headerBuffer.clear();
|
|
||||||
|
|
||||||
long ptsAndFlags;
|
|
||||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
|
||||||
ptsAndFlags = PACKET_FLAG_CONFIG; // non-media data packet
|
|
||||||
} else {
|
|
||||||
ptsAndFlags = bufferInfo.presentationTimeUs;
|
|
||||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
|
|
||||||
ptsAndFlags |= PACKET_FLAG_KEY_FRAME;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
headerBuffer.putLong(ptsAndFlags);
|
|
||||||
headerBuffer.putInt(packetSize);
|
|
||||||
headerBuffer.flip();
|
|
||||||
IO.writeFully(fd, headerBuffer);
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,14 +2,12 @@ package com.genymobile.scrcpy;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.app.Instrumentation;
|
import android.content.ContextWrapper;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
public final class Workarounds {
|
public final class Workarounds {
|
||||||
private Workarounds() {
|
private Workarounds() {
|
||||||
@ -50,7 +48,7 @@ public final class Workarounds {
|
|||||||
Object appBindData = appBindDataConstructor.newInstance();
|
Object appBindData = appBindDataConstructor.newInstance();
|
||||||
|
|
||||||
ApplicationInfo applicationInfo = new ApplicationInfo();
|
ApplicationInfo applicationInfo = new ApplicationInfo();
|
||||||
applicationInfo.packageName = "com.genymobile.scrcpy";
|
applicationInfo.packageName = FakeContext.PACKAGE_NAME;
|
||||||
|
|
||||||
// appBindData.appInfo = applicationInfo;
|
// appBindData.appInfo = applicationInfo;
|
||||||
Field appInfoField = appBindDataClass.getDeclaredField("appInfo");
|
Field appInfoField = appBindDataClass.getDeclaredField("appInfo");
|
||||||
@ -62,11 +60,10 @@ public final class Workarounds {
|
|||||||
mBoundApplicationField.setAccessible(true);
|
mBoundApplicationField.setAccessible(true);
|
||||||
mBoundApplicationField.set(activityThread, appBindData);
|
mBoundApplicationField.set(activityThread, appBindData);
|
||||||
|
|
||||||
// Context ctx = activityThread.getSystemContext();
|
Application app = Application.class.newInstance();
|
||||||
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
|
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
|
||||||
Context ctx = (Context) getSystemContextMethod.invoke(activityThread);
|
baseField.setAccessible(true);
|
||||||
|
baseField.set(app, FakeContext.get());
|
||||||
Application app = Instrumentation.newApplication(Application.class, ctx);
|
|
||||||
|
|
||||||
// activityThread.mInitialApplication = app;
|
// activityThread.mInitialApplication = app;
|
||||||
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
|
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
package com.genymobile.scrcpy.wrappers;
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.FakeContext;
|
||||||
import com.genymobile.scrcpy.Ln;
|
import com.genymobile.scrcpy.Ln;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Intent;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.IInterface;
|
import android.os.IInterface;
|
||||||
|
|
||||||
@ -10,12 +16,15 @@ import java.lang.reflect.Field;
|
|||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||||
public class ActivityManager {
|
public class ActivityManager {
|
||||||
|
|
||||||
private final IInterface manager;
|
private final IInterface manager;
|
||||||
private Method getContentProviderExternalMethod;
|
private Method getContentProviderExternalMethod;
|
||||||
private boolean getContentProviderExternalMethodNewVersion = true;
|
private boolean getContentProviderExternalMethodNewVersion = true;
|
||||||
private Method removeContentProviderExternalMethod;
|
private Method removeContentProviderExternalMethod;
|
||||||
|
private Method startActivityAsUserWithFeatureMethod;
|
||||||
|
private Method forceStopPackageMethod;
|
||||||
|
|
||||||
public ActivityManager(IInterface manager) {
|
public ActivityManager(IInterface manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
@ -42,16 +51,17 @@ public class ActivityManager {
|
|||||||
return removeContentProviderExternalMethod;
|
return removeContentProviderExternalMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.Q)
|
||||||
private ContentProvider getContentProviderExternal(String name, IBinder token) {
|
private ContentProvider getContentProviderExternal(String name, IBinder token) {
|
||||||
try {
|
try {
|
||||||
Method method = getGetContentProviderExternalMethod();
|
Method method = getGetContentProviderExternalMethod();
|
||||||
Object[] args;
|
Object[] args;
|
||||||
if (getContentProviderExternalMethodNewVersion) {
|
if (getContentProviderExternalMethodNewVersion) {
|
||||||
// new version
|
// new version
|
||||||
args = new Object[]{name, ServiceManager.USER_ID, token, null};
|
args = new Object[]{name, FakeContext.ROOT_UID, token, null};
|
||||||
} else {
|
} else {
|
||||||
// old version
|
// old version
|
||||||
args = new Object[]{name, ServiceManager.USER_ID, token};
|
args = new Object[]{name, FakeContext.ROOT_UID, token};
|
||||||
}
|
}
|
||||||
// ContentProviderHolder providerHolder = getContentProviderExternal(...);
|
// ContentProviderHolder providerHolder = getContentProviderExternal(...);
|
||||||
Object providerHolder = method.invoke(manager, args);
|
Object providerHolder = method.invoke(manager, args);
|
||||||
@ -84,4 +94,55 @@ public class ActivityManager {
|
|||||||
public ContentProvider createSettingsProvider() {
|
public ContentProvider createSettingsProvider() {
|
||||||
return getContentProviderExternal("settings", new Binder());
|
return getContentProviderExternal("settings", new Binder());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Method getStartActivityAsUserWithFeatureMethod() throws NoSuchMethodException, ClassNotFoundException {
|
||||||
|
if (startActivityAsUserWithFeatureMethod == null) {
|
||||||
|
Class<?> iApplicationThreadClass = Class.forName("android.app.IApplicationThread");
|
||||||
|
Class<?> profilerInfo = Class.forName("android.app.ProfilerInfo");
|
||||||
|
startActivityAsUserWithFeatureMethod = manager.getClass()
|
||||||
|
.getMethod("startActivityAsUserWithFeature", iApplicationThreadClass, String.class, String.class, Intent.class, String.class,
|
||||||
|
IBinder.class, String.class, int.class, int.class, profilerInfo, Bundle.class, int.class);
|
||||||
|
}
|
||||||
|
return startActivityAsUserWithFeatureMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("ConstantConditions")
|
||||||
|
public int startActivityAsUserWithFeature(Intent intent) {
|
||||||
|
try {
|
||||||
|
Method method = getStartActivityAsUserWithFeatureMethod();
|
||||||
|
return (int) method.invoke(
|
||||||
|
/* this */ manager,
|
||||||
|
/* caller */ null,
|
||||||
|
/* callingPackage */ FakeContext.PACKAGE_NAME,
|
||||||
|
/* callingFeatureId */ null,
|
||||||
|
/* intent */ intent,
|
||||||
|
/* resolvedType */ null,
|
||||||
|
/* resultTo */ null,
|
||||||
|
/* resultWho */ null,
|
||||||
|
/* requestCode */ 0,
|
||||||
|
/* startFlags */ 0,
|
||||||
|
/* profilerInfo */ null,
|
||||||
|
/* bOptions */ null,
|
||||||
|
/* userId */ /* UserHandle.USER_CURRENT */ -2);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Ln.e("Could not invoke method", e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Method getForceStopPackageMethod() throws NoSuchMethodException {
|
||||||
|
if (forceStopPackageMethod == null) {
|
||||||
|
forceStopPackageMethod = manager.getClass().getMethod("forceStopPackage", String.class, int.class);
|
||||||
|
}
|
||||||
|
return forceStopPackageMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forceStopPackage(String packageName) {
|
||||||
|
try {
|
||||||
|
Method method = getForceStopPackageMethod();
|
||||||
|
method.invoke(manager, packageName, /* userId */ /* UserHandle.USER_CURRENT */ -2);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Ln.e("Could not invoke method", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.genymobile.scrcpy.wrappers;
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.FakeContext;
|
||||||
import com.genymobile.scrcpy.Ln;
|
import com.genymobile.scrcpy.Ln;
|
||||||
|
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
@ -58,22 +59,22 @@ public class ClipboardManager {
|
|||||||
private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager)
|
private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager)
|
||||||
throws InvocationTargetException, IllegalAccessException {
|
throws InvocationTargetException, IllegalAccessException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME);
|
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
|
||||||
}
|
}
|
||||||
if (alternativeMethod) {
|
if (alternativeMethod) {
|
||||||
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
|
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||||
}
|
}
|
||||||
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData)
|
private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData)
|
||||||
throws InvocationTargetException, IllegalAccessException {
|
throws InvocationTargetException, IllegalAccessException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME);
|
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
|
||||||
} else if (alternativeMethod) {
|
} else if (alternativeMethod) {
|
||||||
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
|
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||||
} else {
|
} else {
|
||||||
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,11 +107,11 @@ public class ClipboardManager {
|
|||||||
private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager,
|
private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager,
|
||||||
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
|
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME);
|
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
|
||||||
} else if (alternativeMethod) {
|
} else if (alternativeMethod) {
|
||||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
|
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||||
} else {
|
} else {
|
||||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package com.genymobile.scrcpy.wrappers;
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.FakeContext;
|
||||||
import com.genymobile.scrcpy.Ln;
|
import com.genymobile.scrcpy.Ln;
|
||||||
import com.genymobile.scrcpy.SettingsException;
|
import com.genymobile.scrcpy.SettingsException;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.AttributionSource;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
|
||||||
@ -51,11 +54,10 @@ public class ContentProvider implements Closeable {
|
|||||||
@SuppressLint("PrivateApi")
|
@SuppressLint("PrivateApi")
|
||||||
private Method getCallMethod() throws NoSuchMethodException {
|
private Method getCallMethod() throws NoSuchMethodException {
|
||||||
if (callMethod == null) {
|
if (callMethod == null) {
|
||||||
try {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
Class<?> attributionSourceClass = Class.forName("android.content.AttributionSource");
|
callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class);
|
||||||
callMethod = provider.getClass().getMethod("call", attributionSourceClass, String.class, String.class, String.class, Bundle.class);
|
|
||||||
callMethodVersion = 0;
|
callMethodVersion = 0;
|
||||||
} catch (NoSuchMethodException | ClassNotFoundException e0) {
|
} else {
|
||||||
// old versions
|
// old versions
|
||||||
try {
|
try {
|
||||||
callMethod = provider.getClass()
|
callMethod = provider.getClass()
|
||||||
@ -75,40 +77,29 @@ public class ContentProvider implements Closeable {
|
|||||||
return callMethod;
|
return callMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("PrivateApi")
|
|
||||||
private Object getAttributionSource()
|
|
||||||
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
|
|
||||||
if (attributionSource == null) {
|
|
||||||
Class<?> cl = Class.forName("android.content.AttributionSource$Builder");
|
|
||||||
Object builder = cl.getConstructor(int.class).newInstance(ServiceManager.USER_ID);
|
|
||||||
cl.getDeclaredMethod("setPackageName", String.class).invoke(builder, ServiceManager.PACKAGE_NAME);
|
|
||||||
attributionSource = cl.getDeclaredMethod("build").invoke(builder);
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributionSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Bundle call(String callMethod, String arg, Bundle extras)
|
private Bundle call(String callMethod, String arg, Bundle extras)
|
||||||
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
|
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
|
||||||
try {
|
try {
|
||||||
Method method = getCallMethod();
|
Method method = getCallMethod();
|
||||||
Object[] args;
|
Object[] args;
|
||||||
switch (callMethodVersion) {
|
|
||||||
case 0:
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callMethodVersion == 0) {
|
||||||
args = new Object[]{getAttributionSource(), "settings", callMethod, arg, extras};
|
args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras};
|
||||||
break;
|
} else {
|
||||||
case 1:
|
switch (callMethodVersion) {
|
||||||
args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
|
case 1:
|
||||||
break;
|
args = new Object[]{FakeContext.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
|
||||||
case 2:
|
break;
|
||||||
args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
|
case 2:
|
||||||
break;
|
args = new Object[]{FakeContext.PACKAGE_NAME, "settings", callMethod, arg, extras};
|
||||||
default:
|
break;
|
||||||
args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras};
|
default:
|
||||||
break;
|
args = new Object[]{FakeContext.PACKAGE_NAME, callMethod, arg, extras};
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (Bundle) method.invoke(provider, args);
|
return (Bundle) method.invoke(provider, args);
|
||||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) {
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
Ln.e("Could not invoke method", e);
|
Ln.e("Could not invoke method", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -147,7 +138,7 @@ public class ContentProvider implements Closeable {
|
|||||||
public String getValue(String table, String key) throws SettingsException {
|
public String getValue(String table, String key) throws SettingsException {
|
||||||
String method = getGetMethod(table);
|
String method = getGetMethod(table);
|
||||||
Bundle arg = new Bundle();
|
Bundle arg = new Bundle();
|
||||||
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
|
arg.putInt(CALL_METHOD_USER_KEY, FakeContext.ROOT_UID);
|
||||||
try {
|
try {
|
||||||
Bundle bundle = call(method, key, arg);
|
Bundle bundle = call(method, key, arg);
|
||||||
if (bundle == null) {
|
if (bundle == null) {
|
||||||
@ -163,7 +154,7 @@ public class ContentProvider implements Closeable {
|
|||||||
public void putValue(String table, String key, String value) throws SettingsException {
|
public void putValue(String table, String key, String value) throws SettingsException {
|
||||||
String method = getPutMethod(table);
|
String method = getPutMethod(table);
|
||||||
Bundle arg = new Bundle();
|
Bundle arg = new Bundle();
|
||||||
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
|
arg.putInt(CALL_METHOD_USER_KEY, FakeContext.ROOT_UID);
|
||||||
arg.putString(NAME_VALUE_TABLE_VALUE, value);
|
arg.putString(NAME_VALUE_TABLE_VALUE, value);
|
||||||
try {
|
try {
|
||||||
call(method, key, arg);
|
call(method, key, arg);
|
||||||
|
@ -10,9 +10,6 @@ import java.lang.reflect.Method;
|
|||||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||||
public final class ServiceManager {
|
public final class ServiceManager {
|
||||||
|
|
||||||
public static final String PACKAGE_NAME = "com.android.shell";
|
|
||||||
public static final int USER_ID = 0;
|
|
||||||
|
|
||||||
private static final Method GET_SERVICE_METHOD;
|
private static final Method GET_SERVICE_METHOD;
|
||||||
static {
|
static {
|
||||||
try {
|
try {
|
||||||
|
Reference in New Issue
Block a user