Compare commits
16 Commits
v0.1.0
...
test-highl
Author | SHA1 | Date | |
---|---|---|---|
633fd4b41c
|
|||
77d4e80eec
|
|||
44d7e15fb1
|
|||
c985b696ec
|
|||
00cae63272
|
|||
99884b670d
|
|||
3fe7417d4c
|
|||
eb7277e4fd
|
|||
81479d2f64
|
|||
b2a22a9a57
|
|||
ac863c571e
|
|||
13397a59f7
|
|||
be5c37b433
|
|||
3ca3d7784c
|
|||
fa937567bd
|
|||
c129e5104d
|
.gitea/workflows
.gitignoreCargo.tomlREADME.mdflake.nixsetup_test_assets.shsrc
test_assets
tests
integration_tests
mock_socket_tests
@@ -7,37 +7,26 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest-personal
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install latest nightly toolchain
|
- name: Install rust toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
|
||||||
toolchain: nightly
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --all-features --verbose --release
|
run: cargo build --all-features --verbose --release
|
||||||
|
|
||||||
check:
|
check:
|
||||||
runs-on: ubuntu-latest-personal
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install latest nightly toolchain
|
- name: Install rust toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
|
||||||
override: true
|
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
|
|
||||||
- name: Check code format
|
- name: Check code format
|
||||||
run: cargo fmt -- --check
|
run: cargo fmt -- --check
|
||||||
|
|
||||||
@@ -45,30 +34,27 @@ jobs:
|
|||||||
run: cargo clippy --all-features -- --deny warnings
|
run: cargo clippy --all-features -- --deny warnings
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest-personal
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: cargo-bins/cargo-binstall@main
|
|
||||||
|
- name: Install cargo binstall
|
||||||
|
uses: cargo-bins/cargo-binstall@main
|
||||||
|
|
||||||
- name: Install mpv
|
- name: Install mpv
|
||||||
run: apt-get update && apt-get install -y mpv
|
run: apt-get update && apt-get install -y mpv
|
||||||
|
|
||||||
- name: Install latest nightly toolchain
|
- name: Install rust toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: dtolnay/rust-toolchain@nightly
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
|
||||||
override: true
|
|
||||||
components: llvm-tools-preview
|
components: llvm-tools-preview
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
|
|
||||||
- name: Install nextest
|
- name: Install nextest
|
||||||
run: cargo binstall -y cargo-nextest --secure
|
run: cargo binstall -y cargo-nextest --secure
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
cargo nextest run --all-features --release --no-fail-fast
|
cargo nextest run --all-features --release --no-fail-fast --test-threads=1
|
||||||
env:
|
env:
|
||||||
RUST_LOG: "trace"
|
RUST_LOG: "trace"
|
||||||
RUSTFLAGS: "-Cinstrument-coverage"
|
RUSTFLAGS: "-Cinstrument-coverage"
|
||||||
@@ -96,38 +82,32 @@ jobs:
|
|||||||
target/coverage/
|
target/coverage/
|
||||||
|
|
||||||
- name: Upload test report
|
- name: Upload test report
|
||||||
uses: https://git.pvv.ntnu.no/oysteikt/rsync-action@main
|
uses: https://git.pvv.ntnu.no/Projects/rsync-action@v1
|
||||||
with:
|
with:
|
||||||
source: target/coverage/html/
|
source: target/coverage/html/
|
||||||
target: mpvipc-async/${{ gitea.ref_name }}/coverage/
|
target: ${{ gitea.ref_name }}/coverage/
|
||||||
username: oysteikt
|
username: gitea-web
|
||||||
ssh-key: ${{ secrets.OYSTEIKT_GITEA_WEBDOCS_SSH_KEY }}
|
ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }}
|
||||||
host: microbel.pvv.ntnu.no
|
host: bekkalokk.pvv.ntnu.no
|
||||||
known-hosts: "microbel.pvv.ntnu.no ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEq0yasKP0mH6PI6ypmuzPzMnbHELo9k+YB5yW534aKudKZS65YsHJKQ9vapOtmegrn5MQbCCgrshf+/XwZcjbM="
|
known-hosts: "bekkalokk.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEI6VSaDrMG8+flg4/AeHlAFIen8RUzWh6URQKqFegSx"
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
runs-on: ubuntu-latest-personal
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install latest nightly toolchain
|
- name: Install rust toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
|
||||||
toolchain: nightly
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
|
|
||||||
- name: Build docs
|
- name: Build docs
|
||||||
run: cargo doc --all-features --document-private-items --release
|
run: cargo doc --all-features --document-private-items --release
|
||||||
|
|
||||||
- name: Transfer files
|
- name: Transfer files
|
||||||
uses: https://git.pvv.ntnu.no/oysteikt/rsync-action@main
|
uses: https://git.pvv.ntnu.no/Projects/rsync-action@v1
|
||||||
with:
|
with:
|
||||||
source: target/doc/
|
source: target/doc/
|
||||||
target: mpvipc-async/${{ gitea.ref_name }}/docs/
|
target: ${{ gitea.ref_name }}/docs/
|
||||||
username: oysteikt
|
username: gitea-web
|
||||||
ssh-key: ${{ secrets.OYSTEIKT_GITEA_WEBDOCS_SSH_KEY }}
|
ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }}
|
||||||
host: microbel.pvv.ntnu.no
|
host: bekkalokk.pvv.ntnu.no
|
||||||
known-hosts: "microbel.pvv.ntnu.no ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEq0yasKP0mH6PI6ypmuzPzMnbHELo9k+YB5yW534aKudKZS65YsHJKQ9vapOtmegrn5MQbCCgrshf+/XwZcjbM="
|
known-hosts: "bekkalokk.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEI6VSaDrMG8+flg4/AeHlAFIen8RUzWh6URQKqFegSx"
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,5 @@
|
|||||||
target
|
target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
|
||||||
|
test_assets/*
|
||||||
|
!test_assets/.gitkeep
|
@@ -7,9 +7,8 @@ authors = [
|
|||||||
]
|
]
|
||||||
description = "A small library which provides bindings to control existing mpv instances through sockets."
|
description = "A small library which provides bindings to control existing mpv instances through sockets."
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
homepage = "https://git.pvv.ntnu.no/oysteikt/mpvipc-async"
|
repository = "https://git.pvv.ntnu.no/Projects/mpvipc-async"
|
||||||
repository = "https://git.pvv.ntnu.no/oysteikt/mpvipc-async"
|
documentation = "https://pages.pvv.ntnu.no/Projects/mpvipc-async/main/docs/mpvipc_async/"
|
||||||
documentation = "https://pvv.ntnu.no/~oysteikt/gitea/mpvipc-async/master/docs/mpvipc-async/"
|
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.75"
|
rust-version = "1.75"
|
||||||
|
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
[](https://pvv.ntnu.no/~oysteikt/gitea/mpvipc-async/main/coverage/src/)
|
[](https://pages.pvv.ntnu.no/Projects/mpvipc-async/main/coverage/src/)
|
||||||
[](https://pvv.ntnu.no/~oysteikt/gitea/mpvipc-async/main/docs/mpvipc_async/)
|
[](https://pages.pvv.ntnu.no/Projects/mpvipc-async/main/docs/mpvipc_async/)
|
||||||
|
|
||||||
# mpvipc-async
|
# mpvipc-async
|
||||||
|
|
||||||
> **NOTE:** This is a fork of [gitlab.com/mpv-ipc/mpvipc](https://gitlab.com/mpv-ipc/mpvipc), which introduces a lot of changes to be able to use the library asynchronously with [tokio](https://github.com/tokio-rs/tokio).
|
> **NOTE:** This is a fork of [gitlab.com/mpv-ipc/mpvipc](https://gitlab.com/mpv-ipc/mpvipc), which introduces a lot of changes to be able to use the library asynchronously with [tokio](https://github.com/tokio-rs/tokio).
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
A small library which provides bindings to control existing mpv instances through sockets.
|
A small library which provides bindings to control existing mpv instances through sockets.
|
||||||
|
|
||||||
@@ -34,4 +33,4 @@ async fn main() -> Result<(), MpvError> {
|
|||||||
let paused: bool = mpv.get_property("pause").await?;
|
let paused: bool = mpv.get_property("pause").await?;
|
||||||
mpv.set_property("pause", !paused).await.expect("Error pausing");
|
mpv.set_property("pause", !paused).await.expect("Error pausing");
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@@ -31,6 +31,7 @@
|
|||||||
pkgs.mpv
|
pkgs.mpv
|
||||||
pkgs.grcov
|
pkgs.grcov
|
||||||
pkgs.cargo-nextest
|
pkgs.cargo-nextest
|
||||||
|
pkgs.ffmpeg
|
||||||
];
|
];
|
||||||
RUST_SRC_PATH = "${toolchain.rust-src}/lib/rustlib/src/rust/library";
|
RUST_SRC_PATH = "${toolchain.rust-src}/lib/rustlib/src/rust/library";
|
||||||
});
|
});
|
||||||
|
21
setup_test_assets.sh
Executable file
21
setup_test_assets.sh
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REQUIRED_COMMANDS=(
|
||||||
|
"git"
|
||||||
|
"ffmpeg"
|
||||||
|
)
|
||||||
|
|
||||||
|
for cmd in "${REQUIRED_COMMANDS[@]}"; do
|
||||||
|
if ! command -v "$cmd" &> /dev/null; then
|
||||||
|
echo "Command '$cmd' not found. Please install it and try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
ROOT_DIR=$(git rev-parse --show-toplevel)
|
||||||
|
|
||||||
|
# Generate 30 seconds of 480p video with black background
|
||||||
|
|
||||||
|
ffmpeg -f lavfi -i color=c=black:s=640x480:d=30 -c:v libx264 -t 30 -pix_fmt yuv420p "$ROOT_DIR/test_assets/black-background-30s-480p.mp4"
|
@@ -27,39 +27,65 @@ use crate::{
|
|||||||
/// the upstream list of commands.
|
/// the upstream list of commands.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum MpvCommand {
|
pub enum MpvCommand {
|
||||||
|
/// Load the given file or URL and play it.
|
||||||
LoadFile {
|
LoadFile {
|
||||||
file: String,
|
file: String,
|
||||||
option: PlaylistAddOptions,
|
option: PlaylistAddOptions,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Load the given playlist file or URL.
|
||||||
LoadList {
|
LoadList {
|
||||||
file: String,
|
file: String,
|
||||||
option: PlaylistAddOptions,
|
option: PlaylistAddOptions,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Clear the playlist, except for the currently playing file.
|
||||||
PlaylistClear,
|
PlaylistClear,
|
||||||
PlaylistMove {
|
|
||||||
from: usize,
|
///Move the playlist entry at `from`, so that it takes the place of the entry `to`.
|
||||||
to: usize,
|
/// (Paradoxically, the moved playlist entry will not have the index value `to` after moving
|
||||||
},
|
/// if `from` was lower than `to`, because `to` refers to the target entry, not the index
|
||||||
Observe {
|
/// the entry will have after moving.)
|
||||||
id: usize,
|
PlaylistMove { from: usize, to: usize },
|
||||||
property: String,
|
|
||||||
},
|
/// Observe a property. This will start triggering [`Event::PropertyChange`] events
|
||||||
|
/// in the event stream whenever the specific property changes.
|
||||||
|
/// You can use [`Mpv::get_event_stream`] to get the stream.
|
||||||
|
Observe { id: u64, property: String },
|
||||||
|
|
||||||
|
/// Skip to the next entry in the playlist.
|
||||||
PlaylistNext,
|
PlaylistNext,
|
||||||
|
|
||||||
|
/// Skip to the previous entry in the playlist.
|
||||||
PlaylistPrev,
|
PlaylistPrev,
|
||||||
|
|
||||||
|
/// Remove an entry from the playlist by its position in the playlist.
|
||||||
PlaylistRemove(usize),
|
PlaylistRemove(usize),
|
||||||
|
|
||||||
|
/// Shuffle the playlist
|
||||||
PlaylistShuffle,
|
PlaylistShuffle,
|
||||||
|
|
||||||
|
/// Exit the player
|
||||||
Quit,
|
Quit,
|
||||||
|
|
||||||
|
/// Send a message to all clients, and pass it the following list of arguments.
|
||||||
|
/// What this message means, how many arguments it takes, and what the arguments
|
||||||
|
/// mean is fully up to the receiver and the sender.
|
||||||
ScriptMessage(Vec<String>),
|
ScriptMessage(Vec<String>),
|
||||||
ScriptMessageTo {
|
|
||||||
target: String,
|
/// Same as [`MpvCommand::ScriptMessage`], but send the message to a specific target.
|
||||||
args: Vec<String>,
|
ScriptMessageTo { target: String, args: Vec<String> },
|
||||||
},
|
|
||||||
Seek {
|
/// Change the playback position.
|
||||||
seconds: f64,
|
Seek { seconds: f64, option: SeekOptions },
|
||||||
option: SeekOptions,
|
|
||||||
},
|
/// Stop the current playback, and clear the playlist.
|
||||||
|
/// This esentially resets the entire player state without exiting mpv.
|
||||||
Stop,
|
Stop,
|
||||||
Unobserve(usize),
|
|
||||||
|
/// Unobserve all properties registered with the given id.
|
||||||
|
/// See [`MpvCommand::Observe`] for more context.
|
||||||
|
Unobserve(u64),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper trait to keep track of the string literals that mpv expects.
|
/// Helper trait to keep track of the string literals that mpv expects.
|
||||||
@@ -82,7 +108,7 @@ pub enum MpvDataType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A mpv playlist.
|
/// A mpv playlist.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
pub struct Playlist(pub Vec<PlaylistEntry>);
|
pub struct Playlist(pub Vec<PlaylistEntry>);
|
||||||
|
|
||||||
/// A single entry in the mpv playlist.
|
/// A single entry in the mpv playlist.
|
||||||
|
18
src/error.rs
18
src/error.rs
@@ -8,8 +8,11 @@ use crate::{MpvDataType, Property};
|
|||||||
/// Any error that can occur when interacting with mpv.
|
/// Any error that can occur when interacting with mpv.
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum MpvError {
|
pub enum MpvError {
|
||||||
#[error("MpvError: {0}")]
|
#[error("Mpv returned error in response to command: {message}\nCommand: {command:#?}")]
|
||||||
MpvError(String),
|
MpvError {
|
||||||
|
command: Vec<Value>,
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
|
||||||
#[error("Error communicating over mpv socket: {0}")]
|
#[error("Error communicating over mpv socket: {0}")]
|
||||||
MpvSocketConnectionError(String),
|
MpvSocketConnectionError(String),
|
||||||
@@ -53,7 +56,16 @@ pub enum MpvError {
|
|||||||
impl PartialEq for MpvError {
|
impl PartialEq for MpvError {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
match (self, other) {
|
match (self, other) {
|
||||||
(Self::MpvError(l0), Self::MpvError(r0)) => l0 == r0,
|
(
|
||||||
|
Self::MpvError {
|
||||||
|
command: l_command,
|
||||||
|
message: l_message,
|
||||||
|
},
|
||||||
|
Self::MpvError {
|
||||||
|
command: r_command,
|
||||||
|
message: r_message,
|
||||||
|
},
|
||||||
|
) => l_command == r_command && l_message == r_message,
|
||||||
(Self::MpvSocketConnectionError(l0), Self::MpvSocketConnectionError(r0)) => l0 == r0,
|
(Self::MpvSocketConnectionError(l0), Self::MpvSocketConnectionError(r0)) => l0 == r0,
|
||||||
(Self::InternalConnectionError(l0), Self::InternalConnectionError(r0)) => l0 == r0,
|
(Self::InternalConnectionError(l0), Self::InternalConnectionError(r0)) => l0 == r0,
|
||||||
(Self::JsonParseError(l0), Self::JsonParseError(r0)) => {
|
(Self::JsonParseError(l0), Self::JsonParseError(r0)) => {
|
||||||
|
@@ -109,7 +109,7 @@ pub enum Event {
|
|||||||
VideoReconfig,
|
VideoReconfig,
|
||||||
AudioReconfig,
|
AudioReconfig,
|
||||||
PropertyChange {
|
PropertyChange {
|
||||||
id: usize,
|
id: u64,
|
||||||
name: String,
|
name: String,
|
||||||
data: Option<MpvDataType>,
|
data: Option<MpvDataType>,
|
||||||
},
|
},
|
||||||
@@ -296,7 +296,7 @@ fn parse_client_message(event: &Map<String, Value>) -> Result<Event, MpvError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_property_change(event: &Map<String, Value>) -> Result<Event, MpvError> {
|
fn parse_property_change(event: &Map<String, Value>) -> Result<Event, MpvError> {
|
||||||
let id = get_key_as!(as_u64, "id", event) as usize;
|
let id = get_key_as!(as_u64, "id", event);
|
||||||
let property_name = get_key_as!(as_str, "name", event);
|
let property_name = get_key_as!(as_str, "name", event);
|
||||||
let data = event.get("data").map(json_to_value).transpose()?;
|
let data = event.get("data").map(json_to_value).transpose()?;
|
||||||
|
|
||||||
|
@@ -88,11 +88,11 @@ pub trait MpvExt {
|
|||||||
|
|
||||||
/// Notify mpv to send events whenever a property changes.
|
/// Notify mpv to send events whenever a property changes.
|
||||||
/// See [`Mpv::get_event_stream`] and [`Property`](crate::Property) for more information.
|
/// See [`Mpv::get_event_stream`] and [`Property`](crate::Property) for more information.
|
||||||
async fn observe_property(&self, id: usize, property: &str) -> Result<(), MpvError>;
|
async fn observe_property(&self, id: u64, property: &str) -> Result<(), MpvError>;
|
||||||
|
|
||||||
/// Stop observing a property.
|
/// Stop observing a property.
|
||||||
/// See [`Mpv::get_event_stream`] and [`Property`](crate::Property) for more information.
|
/// See [`Mpv::get_event_stream`] and [`Property`](crate::Property) for more information.
|
||||||
async fn unobserve_property(&self, id: usize) -> Result<(), MpvError>;
|
async fn unobserve_property(&self, id: u64) -> Result<(), MpvError>;
|
||||||
|
|
||||||
/// Skip to the next entry in the playlist.
|
/// Skip to the next entry in the playlist.
|
||||||
async fn next(&self) -> Result<(), MpvError>;
|
async fn next(&self) -> Result<(), MpvError>;
|
||||||
@@ -259,7 +259,7 @@ impl MpvExt for Mpv {
|
|||||||
self.run_command(MpvCommand::PlaylistPrev).await
|
self.run_command(MpvCommand::PlaylistPrev).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn observe_property(&self, id: usize, property: &str) -> Result<(), MpvError> {
|
async fn observe_property(&self, id: u64, property: &str) -> Result<(), MpvError> {
|
||||||
self.run_command(MpvCommand::Observe {
|
self.run_command(MpvCommand::Observe {
|
||||||
id,
|
id,
|
||||||
property: property.to_string(),
|
property: property.to_string(),
|
||||||
@@ -267,7 +267,7 @@ impl MpvExt for Mpv {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn unobserve_property(&self, id: usize) -> Result<(), MpvError> {
|
async fn unobserve_property(&self, id: u64) -> Result<(), MpvError> {
|
||||||
self.run_command(MpvCommand::Unobserve(id)).await
|
self.run_command(MpvCommand::Unobserve(id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
20
src/ipc.rs
20
src/ipc.rs
@@ -24,8 +24,8 @@ pub(crate) enum MpvIpcCommand {
|
|||||||
Command(Vec<String>),
|
Command(Vec<String>),
|
||||||
GetProperty(String),
|
GetProperty(String),
|
||||||
SetProperty(String, Value),
|
SetProperty(String, Value),
|
||||||
ObserveProperty(usize, String),
|
ObserveProperty(u64, String),
|
||||||
UnobserveProperty(usize),
|
UnobserveProperty(u64),
|
||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ impl MpvIpc {
|
|||||||
|
|
||||||
log::trace!("Received response: {:?}", response);
|
log::trace!("Received response: {:?}", response);
|
||||||
|
|
||||||
parse_mpv_response_data(response?)
|
parse_mpv_response_data(response?, command)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn get_mpv_property(
|
pub(crate) async fn get_mpv_property(
|
||||||
@@ -114,17 +114,14 @@ impl MpvIpc {
|
|||||||
|
|
||||||
pub(crate) async fn observe_property(
|
pub(crate) async fn observe_property(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: usize,
|
id: u64,
|
||||||
property: &str,
|
property: &str,
|
||||||
) -> Result<Option<Value>, MpvError> {
|
) -> Result<Option<Value>, MpvError> {
|
||||||
self.send_command(&[json!("observe_property"), json!(id), json!(property)])
|
self.send_command(&[json!("observe_property"), json!(id), json!(property)])
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn unobserve_property(
|
pub(crate) async fn unobserve_property(&mut self, id: u64) -> Result<Option<Value>, MpvError> {
|
||||||
&mut self,
|
|
||||||
id: usize,
|
|
||||||
) -> Result<Option<Value>, MpvError> {
|
|
||||||
self.send_command(&[json!("unobserve_property"), json!(id)])
|
self.send_command(&[json!("unobserve_property"), json!(id)])
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -197,7 +194,7 @@ impl MpvIpc {
|
|||||||
/// This function does the most basic JSON parsing and error handling
|
/// This function does the most basic JSON parsing and error handling
|
||||||
/// for status codes and errors that all responses from mpv are
|
/// for status codes and errors that all responses from mpv are
|
||||||
/// expected to contain.
|
/// expected to contain.
|
||||||
fn parse_mpv_response_data(value: Value) -> Result<Option<Value>, MpvError> {
|
fn parse_mpv_response_data(value: Value, command: &[Value]) -> Result<Option<Value>, MpvError> {
|
||||||
log::trace!("Parsing mpv response data: {:?}", value);
|
log::trace!("Parsing mpv response data: {:?}", value);
|
||||||
let result = value
|
let result = value
|
||||||
.as_object()
|
.as_object()
|
||||||
@@ -225,7 +222,10 @@ fn parse_mpv_response_data(value: Value) -> Result<Option<Value>, MpvError> {
|
|||||||
.and_then(|(error, data)| match error {
|
.and_then(|(error, data)| match error {
|
||||||
"success" => Ok(data),
|
"success" => Ok(data),
|
||||||
"property unavailable" => Ok(None),
|
"property unavailable" => Ok(None),
|
||||||
err => Err(MpvError::MpvError(err.to_string())),
|
err => Err(MpvError::MpvError {
|
||||||
|
command: command.to_owned(),
|
||||||
|
message: err.to_string(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
match &result {
|
match &result {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
//! JSON parsing logic for properties returned by
|
//! JSON parsing logic for properties returned by
|
||||||
//! [[`Event::PropertyChange`], and used internally in `MpvExt`
|
//! [`Event::PropertyChange`], and used internally in `MpvExt`
|
||||||
//! to parse the response from `Mpv::get_property()`.
|
//! to parse the response from `Mpv::get_property()`.
|
||||||
//!
|
//!
|
||||||
//! This module is used to parse the json data from the `data` field of
|
//! This module is used to parse the json data from the `data` field of
|
||||||
|
0
test_assets/.gitkeep
Normal file
0
test_assets/.gitkeep
Normal file
@@ -8,7 +8,7 @@ use test_log::test;
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
const MPV_CHANNEL_ID: usize = 1337;
|
const MPV_CHANNEL_ID: u64 = 1337;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
enum PropertyCheckingThreadError {
|
enum PropertyCheckingThreadError {
|
||||||
@@ -115,7 +115,7 @@ async fn graceful_shutdown(
|
|||||||
#[test(tokio::test)]
|
#[test(tokio::test)]
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
async fn test_highlevel_event_pause() -> Result<(), MpvError> {
|
async fn test_highlevel_event_pause() -> Result<(), MpvError> {
|
||||||
let (proc, mpv) = spawn_headless_mpv().await?;
|
let (proc, mpv) = spawn_mpv(true).await?;
|
||||||
|
|
||||||
mpv.observe_property(MPV_CHANNEL_ID, "pause").await?;
|
mpv.observe_property(MPV_CHANNEL_ID, "pause").await?;
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ async fn test_highlevel_event_pause() -> Result<(), MpvError> {
|
|||||||
#[test(tokio::test)]
|
#[test(tokio::test)]
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
async fn test_highlevel_event_volume() -> Result<(), MpvError> {
|
async fn test_highlevel_event_volume() -> Result<(), MpvError> {
|
||||||
let (proc, mpv) = spawn_headless_mpv().await?;
|
let (proc, mpv) = spawn_mpv(true).await?;
|
||||||
|
|
||||||
mpv.observe_property(1337, "volume").await?;
|
mpv.observe_property(1337, "volume").await?;
|
||||||
let events = mpv.get_event_stream().await;
|
let events = mpv.get_event_stream().await;
|
||||||
@@ -174,7 +174,7 @@ async fn test_highlevel_event_volume() -> Result<(), MpvError> {
|
|||||||
#[test(tokio::test)]
|
#[test(tokio::test)]
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
async fn test_highlevel_event_mute() -> Result<(), MpvError> {
|
async fn test_highlevel_event_mute() -> Result<(), MpvError> {
|
||||||
let (proc, mpv) = spawn_headless_mpv().await?;
|
let (proc, mpv) = spawn_mpv(true).await?;
|
||||||
|
|
||||||
mpv.observe_property(1337, "mute").await?;
|
mpv.observe_property(1337, "mute").await?;
|
||||||
let events = mpv.get_event_stream().await;
|
let events = mpv.get_event_stream().await;
|
||||||
@@ -202,7 +202,7 @@ async fn test_highlevel_event_mute() -> Result<(), MpvError> {
|
|||||||
#[test(tokio::test)]
|
#[test(tokio::test)]
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
async fn test_highlevel_event_duration() -> Result<(), MpvError> {
|
async fn test_highlevel_event_duration() -> Result<(), MpvError> {
|
||||||
let (proc, mpv) = spawn_headless_mpv().await?;
|
let (proc, mpv) = spawn_mpv(true).await?;
|
||||||
|
|
||||||
mpv.observe_property(1337, "duration").await?;
|
mpv.observe_property(1337, "duration").await?;
|
||||||
|
|
||||||
|
35
tests/integration_tests/highlevel_api.rs
Normal file
35
tests/integration_tests/highlevel_api.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use super::util::{get_test_asset, spawn_mpv};
|
||||||
|
|
||||||
|
use mpvipc_async::{
|
||||||
|
MpvError, MpvExt, PlaylistAddOptions, PlaylistAddTypeOptions, SeekOptions, Switch,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
async fn test_seek() -> Result<(), MpvError> {
|
||||||
|
let (mut proc, mpv) = spawn_mpv(false).await.unwrap();
|
||||||
|
mpv.playlist_add(
|
||||||
|
&get_test_asset("black-background-30s-480p.mp4"),
|
||||||
|
PlaylistAddTypeOptions::File,
|
||||||
|
PlaylistAddOptions::Append,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
mpv.set_playback(Switch::On).await?;
|
||||||
|
mpv.set_playback(Switch::Off).await?;
|
||||||
|
|
||||||
|
// TODO: wait for property "seekable" to be true
|
||||||
|
|
||||||
|
mpv.seek(10.0, SeekOptions::Relative).await?;
|
||||||
|
let time_pos: f64 = mpv.get_property("time-pos").await?.unwrap();
|
||||||
|
assert_eq!(time_pos, 10.0);
|
||||||
|
|
||||||
|
mpv.seek(5.0, SeekOptions::Relative).await?;
|
||||||
|
let time_pos: f64 = mpv.get_property("time-pos").await?.unwrap();
|
||||||
|
assert_eq!(time_pos, 15.0);
|
||||||
|
|
||||||
|
mpv.kill().await.unwrap();
|
||||||
|
proc.kill().await.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@@ -5,7 +5,7 @@ use super::*;
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
async fn test_get_mpv_version() -> Result<(), MpvError> {
|
async fn test_get_mpv_version() -> Result<(), MpvError> {
|
||||||
let (mut proc, mpv) = spawn_headless_mpv().await.unwrap();
|
let (mut proc, mpv) = spawn_mpv(true).await.unwrap();
|
||||||
let version: String = mpv.get_property("mpv-version").await?.unwrap();
|
let version: String = mpv.get_property("mpv-version").await?.unwrap();
|
||||||
assert!(version.starts_with("mpv"));
|
assert!(version.starts_with("mpv"));
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ async fn test_get_mpv_version() -> Result<(), MpvError> {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
async fn test_set_property() -> Result<(), MpvError> {
|
async fn test_set_property() -> Result<(), MpvError> {
|
||||||
let (mut proc, mpv) = spawn_headless_mpv().await.unwrap();
|
let (mut proc, mpv) = spawn_mpv(true).await.unwrap();
|
||||||
mpv.set_property("pause", true).await.unwrap();
|
mpv.set_property("pause", true).await.unwrap();
|
||||||
let paused: bool = mpv.get_property("pause").await?.unwrap();
|
let paused: bool = mpv.get_property("pause").await?.unwrap();
|
||||||
assert!(paused);
|
assert!(paused);
|
||||||
@@ -32,7 +32,7 @@ async fn test_set_property() -> Result<(), MpvError> {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
async fn test_get_unavailable_property() -> Result<(), MpvError> {
|
async fn test_get_unavailable_property() -> Result<(), MpvError> {
|
||||||
let (mut proc, mpv) = spawn_headless_mpv().await.unwrap();
|
let (mut proc, mpv) = spawn_mpv(true).await.unwrap();
|
||||||
let time_pos = mpv.get_property::<f64>("time-pos").await;
|
let time_pos = mpv.get_property::<f64>("time-pos").await;
|
||||||
assert_eq!(time_pos, Ok(None));
|
assert_eq!(time_pos, Ok(None));
|
||||||
|
|
||||||
@@ -45,12 +45,15 @@ async fn test_get_unavailable_property() -> Result<(), MpvError> {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
async fn test_get_nonexistent_property() -> Result<(), MpvError> {
|
async fn test_get_nonexistent_property() -> Result<(), MpvError> {
|
||||||
let (mut proc, mpv) = spawn_headless_mpv().await.unwrap();
|
let (mut proc, mpv) = spawn_mpv(true).await.unwrap();
|
||||||
let nonexistent = mpv.get_property::<f64>("nonexistent").await;
|
let nonexistent = mpv.get_property::<f64>("nonexistent").await;
|
||||||
assert_eq!(
|
|
||||||
nonexistent,
|
match nonexistent {
|
||||||
Err(MpvError::MpvError("property not found".to_string()))
|
Err(MpvError::MpvError { message, .. }) => {
|
||||||
);
|
assert_eq!(message, "property not found");
|
||||||
|
}
|
||||||
|
_ => panic!("Unexpected result: {:?}", nonexistent),
|
||||||
|
}
|
||||||
|
|
||||||
mpv.kill().await.unwrap();
|
mpv.kill().await.unwrap();
|
||||||
proc.kill().await.unwrap();
|
proc.kill().await.unwrap();
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
mod event_property_parser;
|
mod event_property_parser;
|
||||||
|
mod highlevel_api;
|
||||||
mod misc;
|
mod misc;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
@@ -6,8 +6,35 @@ use tokio::{
|
|||||||
time::{sleep, timeout},
|
time::{sleep, timeout},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub fn assert_test_assets_exist() {
|
||||||
|
let test_data_dir = Path::new("test_assets");
|
||||||
|
if !test_data_dir.exists()
|
||||||
|
|| !test_data_dir.is_dir()
|
||||||
|
// `.gitkeep` should always be present, so there should be at least 2 entries
|
||||||
|
|| test_data_dir.read_dir().unwrap().count() <= 1
|
||||||
|
{
|
||||||
|
panic!(
|
||||||
|
"Test assets directory not found at {:?}, please run `./setup_test_assets.sh`",
|
||||||
|
test_data_dir
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn get_test_assets_dir() -> &'static Path {
|
||||||
|
Path::new("test_assets")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_test_asset(file_name: &str) -> String {
|
||||||
|
assert_test_assets_exist();
|
||||||
|
|
||||||
|
let test_assets_dir = get_test_assets_dir();
|
||||||
|
let file_path = test_assets_dir.join(file_name);
|
||||||
|
file_path.to_str().unwrap().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
pub async fn spawn_headless_mpv() -> Result<(Child, Mpv), MpvError> {
|
pub async fn spawn_mpv(headless: bool) -> Result<(Child, Mpv), MpvError> {
|
||||||
let socket_path_str = format!("/tmp/mpv-ipc-{}", uuid::Uuid::new_v4());
|
let socket_path_str = format!("/tmp/mpv-ipc-{}", uuid::Uuid::new_v4());
|
||||||
let socket_path = Path::new(&socket_path_str);
|
let socket_path = Path::new(&socket_path_str);
|
||||||
|
|
||||||
@@ -15,8 +42,11 @@ pub async fn spawn_headless_mpv() -> Result<(Child, Mpv), MpvError> {
|
|||||||
let process_handle = Command::new("mpv")
|
let process_handle = Command::new("mpv")
|
||||||
.arg("--no-config")
|
.arg("--no-config")
|
||||||
.arg("--idle")
|
.arg("--idle")
|
||||||
.arg("--no-video")
|
.args(if headless {
|
||||||
.arg("--no-audio")
|
vec!["--no-video", "--no-audio"]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
})
|
||||||
.arg(format!(
|
.arg(format!(
|
||||||
"--input-ipc-server={}",
|
"--input-ipc-server={}",
|
||||||
&socket_path.to_str().unwrap()
|
&socket_path.to_str().unwrap()
|
||||||
@@ -25,7 +55,7 @@ pub async fn spawn_headless_mpv() -> Result<(Child, Mpv), MpvError> {
|
|||||||
.spawn()
|
.spawn()
|
||||||
.expect("Failed to start mpv");
|
.expect("Failed to start mpv");
|
||||||
|
|
||||||
timeout(Duration::from_millis(500), async {
|
timeout(Duration::from_millis(1000), async {
|
||||||
while !&socket_path.exists() {
|
while !&socket_path.exists() {
|
||||||
sleep(Duration::from_millis(10)).await;
|
sleep(Duration::from_millis(10)).await;
|
||||||
}
|
}
|
||||||
|
@@ -151,8 +151,8 @@ async fn test_get_property_simultaneous_requests() {
|
|||||||
tokio::time::sleep(Duration::from_millis(2)).await;
|
tokio::time::sleep(Duration::from_millis(2)).await;
|
||||||
let maybe_volume = mpv_clone_3.get_property::<f64>("nonexistent").await;
|
let maybe_volume = mpv_clone_3.get_property::<f64>("nonexistent").await;
|
||||||
match maybe_volume {
|
match maybe_volume {
|
||||||
Err(MpvError::MpvError(err)) => {
|
Err(MpvError::MpvError { message, .. }) => {
|
||||||
assert_eq!(err, "property not found");
|
assert_eq!(message, "property not found");
|
||||||
}
|
}
|
||||||
_ => panic!("Unexpected result: {:?}", maybe_volume),
|
_ => panic!("Unexpected result: {:?}", maybe_volume),
|
||||||
}
|
}
|
||||||
|
@@ -63,12 +63,12 @@ async fn test_set_property_wrong_type() -> Result<(), MpvError> {
|
|||||||
let mpv = Mpv::connect_socket(server).await?;
|
let mpv = Mpv::connect_socket(server).await?;
|
||||||
let maybe_volume = mpv.set_property::<bool>("volume", true).await;
|
let maybe_volume = mpv.set_property::<bool>("volume", true).await;
|
||||||
|
|
||||||
assert_eq!(
|
match maybe_volume {
|
||||||
maybe_volume,
|
Err(MpvError::MpvError { message, .. }) => {
|
||||||
Err(MpvError::MpvError(
|
assert_eq!(message, "unsupported format for accessing property");
|
||||||
"unsupported format for accessing property".to_string()
|
}
|
||||||
))
|
_ => panic!("Unexpected result: {:?}", maybe_volume),
|
||||||
);
|
}
|
||||||
|
|
||||||
join_handle.await.unwrap().unwrap();
|
join_handle.await.unwrap().unwrap();
|
||||||
|
|
||||||
@@ -84,10 +84,12 @@ async fn test_get_property_error() -> Result<(), MpvError> {
|
|||||||
let mpv = Mpv::connect_socket(server).await?;
|
let mpv = Mpv::connect_socket(server).await?;
|
||||||
let maybe_volume = mpv.set_property("nonexistent", true).await;
|
let maybe_volume = mpv.set_property("nonexistent", true).await;
|
||||||
|
|
||||||
assert_eq!(
|
match maybe_volume {
|
||||||
maybe_volume,
|
Err(MpvError::MpvError { message, .. }) => {
|
||||||
Err(MpvError::MpvError("property not found".to_string()))
|
assert_eq!(message, "property not found");
|
||||||
);
|
}
|
||||||
|
_ => panic!("Unexpected result: {:?}", maybe_volume),
|
||||||
|
}
|
||||||
|
|
||||||
join_handle.await.unwrap().unwrap();
|
join_handle.await.unwrap().unwrap();
|
||||||
|
|
||||||
@@ -161,8 +163,8 @@ async fn test_set_property_simultaneous_requests() {
|
|||||||
tokio::time::sleep(Duration::from_millis(2)).await;
|
tokio::time::sleep(Duration::from_millis(2)).await;
|
||||||
let maybe_volume = mpv_clone_3.set_property("nonexistent", "a").await;
|
let maybe_volume = mpv_clone_3.set_property("nonexistent", "a").await;
|
||||||
match maybe_volume {
|
match maybe_volume {
|
||||||
Err(MpvError::MpvError(err)) => {
|
Err(MpvError::MpvError { message, .. }) => {
|
||||||
assert_eq!(err, "property not found");
|
assert_eq!(message, "property not found");
|
||||||
}
|
}
|
||||||
_ => panic!("Unexpected result: {:?}", maybe_volume),
|
_ => panic!("Unexpected result: {:?}", maybe_volume),
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user