stream video file
All checks were successful
Rust / build (pull_request) Successful in 9m0s

This commit is contained in:
Vinzenz Schroeter 2025-03-08 12:06:13 +01:00
parent 05289581a1
commit cfe9699997
8 changed files with 179 additions and 7 deletions

View file

@ -26,10 +26,10 @@ jobs:
- name: Install rust toolchain - name: Install rust toolchain
run: sudo apt-get install -qy cargo-1.80 rust-1.80-clippy run: sudo apt-get install -qy cargo-1.80 rust-1.80-clippy
- name: Install system dependencies - name: Install system dependencies
run: sudo apt-get install -qy liblzma-dev libpipewire-0.3-dev libclang-dev libdbus-1-dev run: sudo apt-get install -qy liblzma-dev libpipewire-0.3-dev libclang-dev libdbus-1-dev ffmpeg libavutil-dev libavformat-dev libavfilter-dev libavdevice-dev
- name: Run Clippy - name: Run Clippy
run: cargo clippy --all-targets --all-features run: cargo clippy --all-targets --all-features
- name: Build - name: Build
run: cargo build --release --verbose run: cargo build --release

64
Cargo.lock generated
View file

@ -162,6 +162,24 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "bindgen"
version = "0.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
dependencies = [
"bitflags 2.9.0",
"cexpr",
"clang-sys",
"itertools",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]] [[package]]
name = "bit_field" name = "bit_field"
version = "0.10.2" version = "0.10.2"
@ -633,6 +651,31 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "ffmpeg-next"
version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da02698288e0275e442a47fc12ca26d50daf0d48b15398ba5906f20ac2e2a9f9"
dependencies = [
"bitflags 2.9.0",
"ffmpeg-sys-next",
"libc",
]
[[package]]
name = "ffmpeg-sys-next"
version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bc3234d0a4b2f7d083699d0860c6c9dd83713908771b60f94a96f8704adfe45"
dependencies = [
"bindgen 0.70.1",
"cc",
"libc",
"num_cpus",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.1" version = "1.1.1"
@ -842,6 +885,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]] [[package]]
name = "http" name = "http"
version = "1.3.1" version = "1.3.1"
@ -1056,7 +1105,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f" checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f"
dependencies = [ dependencies = [
"bindgen", "bindgen 0.69.5",
"cc", "cc",
"system-deps", "system-deps",
] ]
@ -1237,6 +1286,16 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]] [[package]]
name = "objc" name = "objc"
version = "0.2.7" version = "0.2.7"
@ -1346,7 +1405,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112" checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112"
dependencies = [ dependencies = [
"bindgen", "bindgen 0.69.5",
"libspa-sys", "libspa-sys",
"system-deps", "system-deps",
] ]
@ -1751,6 +1810,7 @@ dependencies = [
"clap", "clap",
"env_logger", "env_logger",
"fast_image_resize", "fast_image_resize",
"ffmpeg-next",
"image", "image",
"log", "log",
"scap", "scap",

View file

@ -20,6 +20,7 @@ scap = "0.0.8"
image = "0.25.5" image = "0.25.5"
fast_image_resize = { version = "5.1", features = ["image"] } fast_image_resize = { version = "5.1", features = ["image"] }
tungstenite = "0.26" tungstenite = "0.26"
ffmpeg-next = "7.1.0"
[dependencies.servicepoint] [dependencies.servicepoint]
package = "servicepoint" package = "servicepoint"

View file

@ -69,6 +69,7 @@ Commands:
flip Invert the state of all pixels [aliases: f] flip Invert the state of all pixels [aliases: f]
on Set all pixels to the on state on Set all pixels to the on state
image Send an image file (e.g. jpeg or png) to the display. [aliases: i] image Send an image file (e.g. jpeg or png) to the display. [aliases: i]
video Stream a video file (e.g. mp4) to the display. [aliases: v]
screen Stream the default screen capture source to the display. On Linux Wayland, this pops up a screen or window chooser, but it also may directly start streaming your main screen. [aliases: s] screen Stream the default screen capture source to the display. On Linux Wayland, this pops up a screen or window chooser, but it also may directly start streaming your main screen. [aliases: s]
``` ```
@ -91,6 +92,25 @@ Options:
--no-aspect Do not keep aspect ratio when resizing. --no-aspect Do not keep aspect ratio when resizing.
``` ```
#### Video file
```text
Stream a video file (e.g. mp4) to the display.
Usage: servicepoint-cli pixels video [OPTIONS] <FILE_NAME>
Arguments:
<FILE_NAME>
Options:
--no-hist Disable histogram correction
--no-blur Disable blur
--no-sharp Disable sharpening
--no-dither Disable dithering. Brightness will be adjusted so that around half of the pixels are on.
--no-spacers Do not remove the spacers from the image.
--no-aspect Do not keep aspect ratio when resizing.
```
#### Screen #### Screen
```text ```text

View file

@ -90,8 +90,7 @@
{ {
default = pkgs.mkShell rec { default = pkgs.mkShell rec {
inputsFrom = [ self.packages.${system}.default ]; inputsFrom = [ self.packages.${system}.default ];
packages = [ packages = with pkgs; [
pkgs.gdb
(pkgs.symlinkJoin { (pkgs.symlinkJoin {
name = "rust-toolchain"; name = "rust-toolchain";
paths = with pkgs; [ paths = with pkgs; [
@ -103,7 +102,11 @@
cargo-expand cargo-expand
]; ];
}) })
pkgs.cargo-flamegraph
cargo-flamegraph
gdb
ffmpeg-headless
]; ];
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}"; LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}";
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";

View file

@ -74,6 +74,16 @@ pub enum PixelCommand {
#[command(flatten)] #[command(flatten)]
image_processing_options: ImageProcessingOptions, image_processing_options: ImageProcessingOptions,
}, },
#[command(
visible_alias = "v",
about = "Stream a video file (e.g. mp4) to the display."
)]
Video {
#[command(flatten)]
send_image_options: SendImageOptions,
#[command(flatten)]
image_processing_options: ImageProcessingOptions,
},
#[command( #[command(
visible_alias = "s", visible_alias = "s",
about = "Stream the default screen capture source to the display. \ about = "Stream the default screen capture source to the display. \

View file

@ -35,6 +35,7 @@ impl ImageProcessingPipeline {
} }
} }
#[must_use]
pub fn process(&mut self, frame: DynamicImage) -> Bitmap { pub fn process(&mut self, frame: DynamicImage) -> Bitmap {
let start_time = Instant::now(); let start_time = Instant::now();

View file

@ -4,6 +4,8 @@ use crate::{
stream_window::stream_window, stream_window::stream_window,
transport::Transport, transport::Transport,
}; };
use ffmpeg_next as ffmpeg;
use image::{DynamicImage, RgbImage};
use log::info; use log::info;
use servicepoint::{ use servicepoint::{
BinaryOperation, BitVecCommand, BitmapCommand, ClearCommand, CompressionCode, DisplayBitVec, BinaryOperation, BitVecCommand, BitmapCommand, ClearCommand, CompressionCode, DisplayBitVec,
@ -23,6 +25,10 @@ pub(crate) fn pixels(connection: &Transport, pixel_command: PixelCommand) {
stream_options, stream_options,
image_processing, image_processing,
} => stream_window(connection, stream_options, image_processing), } => stream_window(connection, stream_options, image_processing),
PixelCommand::Video {
image_processing_options: processing_options,
send_image_options: image_options,
} => pixels_video(connection, image_options, processing_options),
} }
} }
@ -78,3 +84,74 @@ fn pixels_image(
.expect("failed to send image command"); .expect("failed to send image command");
info!("sent image to display"); info!("sent image to display");
} }
fn pixels_video(
connection: &Transport,
options: SendImageOptions,
processing_options: ImageProcessingOptions,
) {
ffmpeg::init().unwrap();
let mut ictx = ffmpeg::format::input(&options.file_name).expect("failed to open video input file");
let input = ictx
.streams()
.best(ffmpeg::media::Type::Video)
.ok_or(ffmpeg::Error::StreamNotFound)
.expect("could not get video stream from input file");
let video_stream_index = input.index();
let context_decoder = ffmpeg::codec::context::Context::from_parameters(input.parameters())
.expect("could not extract video context from parameters");
let mut decoder = context_decoder.decoder().video()
.expect("failed to create decoder for video stream");
let src_width = decoder.width();
let src_height = decoder.height();
let mut scaler = ffmpeg::software::scaling::Context::get(
decoder.format(),
src_width,
src_height,
ffmpeg::format::Pixel::RGB24,
src_width,
src_height,
ffmpeg::software::scaling::Flags::BILINEAR,
).expect("failed to create scaling context");
let mut frame_index = 0;
let mut processing_pipeline = ImageProcessingPipeline::new(processing_options);
let mut receive_and_process_decoded_frames =
|decoder: &mut ffmpeg::decoder::Video| -> Result<(), ffmpeg::Error> {
let mut decoded = ffmpeg::util::frame::video::Video::empty();
let mut rgb_frame = ffmpeg::util::frame::video::Video::empty();
while decoder.receive_frame(&mut decoded).is_ok() {
scaler.run(&decoded, &mut rgb_frame)
.expect("failed to scale frame");
let image = RgbImage::from_raw(src_width, src_height, rgb_frame.data(0).to_owned())
.expect("could not read rgb data to image");
let image = DynamicImage::from(image);
let bitmap= processing_pipeline.process(image);
connection.send_command(BitmapCommand::from(bitmap))
.expect("failed to send image command");
frame_index += 1;
}
Ok(())
};
for (stream, packet) in ictx.packets() {
if stream.index() == video_stream_index {
decoder.send_packet(&packet)
.expect("failed to send video packet");
receive_and_process_decoded_frames(&mut decoder)
.expect("failed to process video packet");
}
}
decoder.send_eof().expect("failed to send eof");
receive_and_process_decoded_frames(&mut decoder)
.expect("failed to eof packet");
}