Compare commits
51 Commits
a1c36e4fb2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d509fb7813 | |||
| dcbb5a127b | |||
| e66c457b57 | |||
| 76fc14c73b | |||
| 5b4fbd5df6 | |||
| e7fd01c0af | |||
| a040478069 | |||
| e5ef173473 | |||
| 429371002b | |||
| 335e8fdbef | |||
| 9dac0b6c78 | |||
|
|
97a7a632d4 | ||
|
|
29390140cd | ||
|
|
97c2b3f14c | ||
|
|
2b2e8060e7 | ||
|
|
584495453f | ||
|
|
99853167df | ||
|
|
fc9555873b | ||
|
|
a7ffa69326 | ||
|
|
4ed15c97f0 | ||
|
|
a2491695b3 | ||
|
|
5a0bdae84b | ||
|
|
5d0b795ba5 | ||
|
|
ebe2312272 | ||
|
|
3382aebb1f | ||
|
|
8d46bd2b85 | ||
|
|
043d1e99f0 | ||
|
|
d42ef3b550 | ||
|
|
21cbaff610 | ||
|
|
a0bda88246 | ||
|
|
ccae03d105 | ||
|
|
232c0f4d53 | ||
|
|
5cec7821d0 | ||
|
|
c2fdedf05a | ||
|
|
7003002b69 | ||
|
|
c675c29be3 | ||
|
|
7f9152e8fd | ||
|
|
6cc83ba655 | ||
|
|
253d27c176 | ||
|
|
c7afcd3f0d | ||
|
|
d75a2fb7e4 | ||
|
|
73fcf9bad1 | ||
|
|
05ae9ff570 | ||
|
|
ca1fd2e977 | ||
|
|
77fe7b6bb4 | ||
|
|
0fb5cb1d99 | ||
|
|
61a2ea1733 | ||
|
|
b1cfc19b96 | ||
|
|
f41625e0ed | ||
|
|
29674df85e | ||
|
|
442a7e49b2 |
2044
Cargo.lock
generated
2044
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
28
Cargo.toml
28
Cargo.toml
@@ -1,5 +1,28 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "api", "typegen", "ui-gpui", "ui-iced"]
|
members = [
|
||||||
|
".",
|
||||||
|
"api",
|
||||||
|
"typegen",
|
||||||
|
"ui-gpui",
|
||||||
|
"ui-iced",
|
||||||
|
"store",
|
||||||
|
"jello-types",
|
||||||
|
"gst",
|
||||||
|
"examples/hdr-gstreamer-wgpu",
|
||||||
|
"crates/iced-video",
|
||||||
|
]
|
||||||
|
[workspace.dependencies]
|
||||||
|
iced = { version = "0.14.0" }
|
||||||
|
gst = { version = "0.1.0", path = "gst" }
|
||||||
|
iced_wgpu = { version = "0.14.0" }
|
||||||
|
iced-video = { version = "0.1.0", path = "crates/iced-video" }
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
iced_wgpu = { git = "https://github.com/uttarayan21/iced", branch = "0.14" }
|
||||||
|
iced_core = { git = "https://github.com/uttarayan21/iced", branch = "0.14" }
|
||||||
|
iced_renderer = { git = "https://github.com/uttarayan21/iced", branch = "0.14" }
|
||||||
|
iced_futures = { git = "https://github.com/uttarayan21/iced", branch = "0.14" }
|
||||||
|
iced = { git = "https://github.com/uttarayan21/iced", branch = "0.14" }
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "jello"
|
name = "jello"
|
||||||
@@ -9,8 +32,11 @@ license = "MIT"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
api = { version = "0.1.0", path = "api" }
|
api = { version = "0.1.0", path = "api" }
|
||||||
|
bytemuck = { version = "1.24.0", features = ["derive"] }
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
clap-verbosity-flag = { version = "3.0.4", features = ["tracing"] }
|
||||||
clap_complete = "4.5"
|
clap_complete = "4.5"
|
||||||
|
color-backtrace = "0.7.2"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
error-stack = "0.6"
|
error-stack = "0.6"
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
|
|||||||
112
README.md
Normal file
112
README.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# Jello
|
||||||
|
|
||||||
|
A WIP video client for jellyfin.
|
||||||
|
|
||||||
|
(Planned) Features
|
||||||
|
|
||||||
|
1. Integrate with jellyfin
|
||||||
|
2. HDR video playback
|
||||||
|
3. Audio Track selection
|
||||||
|
4. Chapter selection
|
||||||
|
|
||||||
|
Libraries and frameworks used for this
|
||||||
|
1. iced -> primary gui toolkit
|
||||||
|
2. gstreamer -> primary video + audio decoding library
|
||||||
|
3. wgpu -> rendering the video from gstreamer in iced
|
||||||
|
|
||||||
|
|
||||||
|
### HDR
|
||||||
|
I'll try to document all my findings about HDR here.
|
||||||
|
I'm making this project to mainly learn about videos, color-spaces and gpu programming. And so very obviously I'm bound to make mistakes in either the code or the fundamental understanding of a concept. Please don't take anything in this text as absolute.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let window = ... // use winnit to get a window handle, check the example in this repo
|
||||||
|
let instance = wgpu::Instance::default();
|
||||||
|
let surface = instance.create_surface(window).unwrap();
|
||||||
|
let adapter = instance
|
||||||
|
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
|
power_preference: wgpu::PowerPreference::default(),
|
||||||
|
compatible_surface: Some(&surface),
|
||||||
|
force_fallback_adapter: false,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("Failed to request wgpu adapter")?;
|
||||||
|
let caps = surface.get_capabilities();
|
||||||
|
println!("{:#?}", caps.formats);
|
||||||
|
```
|
||||||
|
|
||||||
|
This should print out all the texture formats that can be used by your current hardware
|
||||||
|
Among these the formats that support hdr (afaik) are
|
||||||
|
```
|
||||||
|
wgpu::TextureFormat::Rgba16Float
|
||||||
|
wgpu::TextureFormat::Rgba32Float
|
||||||
|
wgpu::TextureFormat::Rgb10a2Unorm
|
||||||
|
wgpu::TextureFormat::Rgb10a2Uint // (unsure)
|
||||||
|
```
|
||||||
|
|
||||||
|
My display supports Rgb10a2Unorm so I'll be going forward with that texture format.
|
||||||
|
|
||||||
|
`Rgb10a2Unorm` is still the same size as a `Rgba8Unorm` but data is in a different representation in each of them
|
||||||
|
|
||||||
|
`Rgb10a2Unorm`:
|
||||||
|
R, G, B => 10 bits each (2^10 = 1024 [0..=1023])
|
||||||
|
A => 2 bits (2^2 = 4 [0..=3])
|
||||||
|
|
||||||
|
Whereas in a normal pixel
|
||||||
|
`Rgba8Unorm`
|
||||||
|
R, G, B, A => 8 bits each (2^8 = 256 [0..=255])
|
||||||
|
|
||||||
|
|
||||||
|
For displaying videos the alpha components is not really used (I don't know of any) so we can use re-allocate 6 bits from the alpha channel and put them in the r,g and b components.
|
||||||
|
In the shader the components get uniformly normalized from [0..=1023] integer to [0..=1] in float so we can compute them properly
|
||||||
|
|
||||||
|
Videos however are generally not stored in this format or any rgb format in general because it is not as efficient for (lossy) compression as YUV formats.
|
||||||
|
|
||||||
|
Right now I don't want to deal with yuv formats so I'll use gstreamer caps to convert the video into `Rgba10a2` format
|
||||||
|
|
||||||
|
|
||||||
|
## Pixel formats and Planes
|
||||||
|
Dated: Sun Jan 4 09:09:16 AM IST 2026
|
||||||
|
| value | count | quantile | percentage | frequency |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| yuv420p | 1815 | 0.5067001675041876 | 50.67% | ************************************************** |
|
||||||
|
| yuv420p10le | 1572 | 0.4388609715242881 | 43.89% | ******************************************* |
|
||||||
|
| yuvj420p | 171 | 0.04773869346733668 | 4.77% | **** |
|
||||||
|
| rgba | 14 | 0.003908431044109436 | 0.39% | |
|
||||||
|
| yuvj444p | 10 | 0.0027917364600781687 | 0.28% | |
|
||||||
|
|
||||||
|
For all of my media collection these are the pixel formats for all the videos
|
||||||
|
|
||||||
|
### RGBA
|
||||||
|
Pretty self evident
|
||||||
|
8 channels for each of R, G, B and A
|
||||||
|
Hopefully shouldn't be too hard to make a function or possibly a lut that takes data from rgba and maps it to Rgb10a2Unorm
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
packet
|
||||||
|
title RGBA
|
||||||
|
+8: "R"
|
||||||
|
+8: "G"
|
||||||
|
+8: "B"
|
||||||
|
+8: "A"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### YUV
|
||||||
|
[All YUV formats](https://learn.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering#surface-definitions)
|
||||||
|
[10 and 16 bit yuv formats](https://learn.microsoft.com/en-us/windows/win32/medfound/10-bit-and-16-bit-yuv-video-formats)
|
||||||
|
|
||||||
|
Y -> Luminance
|
||||||
|
U,V -> Chrominance
|
||||||
|
|
||||||
|
p -> Planar
|
||||||
|
sp -> semi planar
|
||||||
|
|
||||||
|
j -> full range
|
||||||
|
|
||||||
|
planar formats have each of the channels in a contiguous array one after another
|
||||||
|
in semi-planar formats the y channel is seperate and uv channels are interleaved
|
||||||
|
|
||||||
|
|
||||||
|
## Chroma Subsampling
|
||||||
|
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
name = "api"
|
name = "api"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bytes = "1.11.0"
|
||||||
iref = { version = "3.2.2", features = ["serde"] }
|
iref = { version = "3.2.2", features = ["serde"] }
|
||||||
jiff = { version = "0.2.16", features = ["serde"] }
|
jiff = { version = "0.2.16", features = ["serde"] }
|
||||||
reqwest = { version = "0.12.24", features = ["json"] }
|
reqwest = { version = "0.12.24", features = ["json"] }
|
||||||
@@ -14,6 +16,7 @@ thiserror = "2.0.17"
|
|||||||
tokio = { version = "1.48.0", features = ["fs"] }
|
tokio = { version = "1.48.0", features = ["fs"] }
|
||||||
toml = "0.9.8"
|
toml = "0.9.8"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
|
url = "2.5.7"
|
||||||
uuid = { version = "1.18.1", features = ["serde"] }
|
uuid = { version = "1.18.1", features = ["serde"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ pub async fn main() {
|
|||||||
let config = std::fs::read_to_string("config.toml").expect("Config.toml");
|
let config = std::fs::read_to_string("config.toml").expect("Config.toml");
|
||||||
let config: JellyfinConfig = toml::from_str(&config).expect("Failed to parse config.toml");
|
let config: JellyfinConfig = toml::from_str(&config).expect("Failed to parse config.toml");
|
||||||
|
|
||||||
let mut jellyfin = JellyfinClient::new(config);
|
let mut jellyfin = JellyfinClient::new_with_config(config);
|
||||||
jellyfin
|
jellyfin
|
||||||
.authenticate_with_cached_token(".session")
|
.authenticate_with_cached_token(".session")
|
||||||
.await
|
.await
|
||||||
@@ -17,8 +17,16 @@ pub async fn main() {
|
|||||||
for item in items {
|
for item in items {
|
||||||
println!("{}: {:?}", item.id, item.name);
|
println!("{}: {:?}", item.id, item.name);
|
||||||
let items = jellyfin.items(item.id).await.expect("Items");
|
let items = jellyfin.items(item.id).await.expect("Items");
|
||||||
for item in items {
|
std::fs::write(
|
||||||
println!(" {}: {:?}", item.id, item.name);
|
format!("items_{:?}.json", item.name),
|
||||||
}
|
serde_json::to_string_pretty(&items).expect("Serialize items"),
|
||||||
|
);
|
||||||
|
// for item in items {
|
||||||
|
// println!(" {}: {:?}", item.id, item.name);
|
||||||
|
// std::fs::write(
|
||||||
|
// format!("item_{}.json", item.id),
|
||||||
|
// serde_json::to_string_pretty(&item).expect("Serialize item"),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6013,7 +6013,6 @@ pub struct XbmcMetadataOptions {
|
|||||||
pub enable_extra_thumbs_duplication: bool,
|
pub enable_extra_thumbs_duplication: bool,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ApiName {
|
pub enum ApiName {
|
||||||
#[serde(rename = "Mal")]
|
#[serde(rename = "Mal")]
|
||||||
Mal,
|
Mal,
|
||||||
@@ -6030,7 +6029,6 @@ pub enum ApiName {
|
|||||||
}
|
}
|
||||||
/// An enum representing formats of spatial audio.
|
/// An enum representing formats of spatial audio.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum AudioSpatialFormat {
|
pub enum AudioSpatialFormat {
|
||||||
#[serde(rename = "None")]
|
#[serde(rename = "None")]
|
||||||
None,
|
None,
|
||||||
@@ -6041,7 +6039,6 @@ pub enum AudioSpatialFormat {
|
|||||||
}
|
}
|
||||||
/// The base item kind.
|
/// The base item kind.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum BaseItemKind {
|
pub enum BaseItemKind {
|
||||||
#[serde(rename = "AggregateFolder")]
|
#[serde(rename = "AggregateFolder")]
|
||||||
AggregateFolder,
|
AggregateFolder,
|
||||||
@@ -6119,7 +6116,6 @@ pub enum BaseItemKind {
|
|||||||
Year,
|
Year,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ChannelItemSortField {
|
pub enum ChannelItemSortField {
|
||||||
#[serde(rename = "Name")]
|
#[serde(rename = "Name")]
|
||||||
Name,
|
Name,
|
||||||
@@ -6137,7 +6133,6 @@ pub enum ChannelItemSortField {
|
|||||||
CommunityPlayCount,
|
CommunityPlayCount,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ChannelMediaContentType {
|
pub enum ChannelMediaContentType {
|
||||||
#[serde(rename = "Clip")]
|
#[serde(rename = "Clip")]
|
||||||
Clip,
|
Clip,
|
||||||
@@ -6157,7 +6152,6 @@ pub enum ChannelMediaContentType {
|
|||||||
TvExtra,
|
TvExtra,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ChannelMediaType {
|
pub enum ChannelMediaType {
|
||||||
#[serde(rename = "Audio")]
|
#[serde(rename = "Audio")]
|
||||||
Audio,
|
Audio,
|
||||||
@@ -6168,7 +6162,6 @@ pub enum ChannelMediaType {
|
|||||||
}
|
}
|
||||||
/// Enum ChannelType.
|
/// Enum ChannelType.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ChannelType {
|
pub enum ChannelType {
|
||||||
#[serde(rename = "TV")]
|
#[serde(rename = "TV")]
|
||||||
Tv,
|
Tv,
|
||||||
@@ -6176,7 +6169,6 @@ pub enum ChannelType {
|
|||||||
Radio,
|
Radio,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum CodecType {
|
pub enum CodecType {
|
||||||
#[serde(rename = "Video")]
|
#[serde(rename = "Video")]
|
||||||
Video,
|
Video,
|
||||||
@@ -6187,7 +6179,6 @@ pub enum CodecType {
|
|||||||
}
|
}
|
||||||
/// Collection type.
|
/// Collection type.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum CollectionType {
|
pub enum CollectionType {
|
||||||
#[serde(rename = "unknown")]
|
#[serde(rename = "unknown")]
|
||||||
Unknown,
|
Unknown,
|
||||||
@@ -6218,7 +6209,6 @@ pub enum CollectionType {
|
|||||||
}
|
}
|
||||||
/// The collection type options.
|
/// The collection type options.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum CollectionTypeOptions {
|
pub enum CollectionTypeOptions {
|
||||||
#[serde(rename = "movies")]
|
#[serde(rename = "movies")]
|
||||||
Movies,
|
Movies,
|
||||||
@@ -6238,7 +6228,6 @@ pub enum CollectionTypeOptions {
|
|||||||
Mixed,
|
Mixed,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum DayOfWeek {
|
pub enum DayOfWeek {
|
||||||
#[serde(rename = "Sunday")]
|
#[serde(rename = "Sunday")]
|
||||||
Sunday,
|
Sunday,
|
||||||
@@ -6256,7 +6245,6 @@ pub enum DayOfWeek {
|
|||||||
Saturday,
|
Saturday,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum DayPattern {
|
pub enum DayPattern {
|
||||||
#[serde(rename = "Daily")]
|
#[serde(rename = "Daily")]
|
||||||
Daily,
|
Daily,
|
||||||
@@ -6267,7 +6255,6 @@ pub enum DayPattern {
|
|||||||
}
|
}
|
||||||
/// Enum containing deinterlace methods.
|
/// Enum containing deinterlace methods.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum DeinterlaceMethod {
|
pub enum DeinterlaceMethod {
|
||||||
#[serde(rename = "yadif")]
|
#[serde(rename = "yadif")]
|
||||||
Yadif,
|
Yadif,
|
||||||
@@ -6275,7 +6262,6 @@ pub enum DeinterlaceMethod {
|
|||||||
Bwdif,
|
Bwdif,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum DlnaProfileType {
|
pub enum DlnaProfileType {
|
||||||
#[serde(rename = "Audio")]
|
#[serde(rename = "Audio")]
|
||||||
Audio,
|
Audio,
|
||||||
@@ -6290,7 +6276,6 @@ pub enum DlnaProfileType {
|
|||||||
}
|
}
|
||||||
/// An enum representing an algorithm to downmix surround sound to stereo.
|
/// An enum representing an algorithm to downmix surround sound to stereo.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum DownMixStereoAlgorithms {
|
pub enum DownMixStereoAlgorithms {
|
||||||
#[serde(rename = "None")]
|
#[serde(rename = "None")]
|
||||||
None,
|
None,
|
||||||
@@ -6305,7 +6290,6 @@ pub enum DownMixStereoAlgorithms {
|
|||||||
}
|
}
|
||||||
/// An enum that represents a day of the week, weekdays, weekends, or all days.
|
/// An enum that represents a day of the week, weekdays, weekends, or all days.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum DynamicDayOfWeek {
|
pub enum DynamicDayOfWeek {
|
||||||
#[serde(rename = "Sunday")]
|
#[serde(rename = "Sunday")]
|
||||||
Sunday,
|
Sunday,
|
||||||
@@ -6330,7 +6314,6 @@ pub enum DynamicDayOfWeek {
|
|||||||
}
|
}
|
||||||
/// An enum representing the options to disable embedded subs.
|
/// An enum representing the options to disable embedded subs.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum EmbeddedSubtitleOptions {
|
pub enum EmbeddedSubtitleOptions {
|
||||||
#[serde(rename = "AllowAll")]
|
#[serde(rename = "AllowAll")]
|
||||||
AllowAll,
|
AllowAll,
|
||||||
@@ -6343,7 +6326,6 @@ pub enum EmbeddedSubtitleOptions {
|
|||||||
}
|
}
|
||||||
/// Enum containing encoder presets.
|
/// Enum containing encoder presets.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum EncoderPreset {
|
pub enum EncoderPreset {
|
||||||
#[serde(rename = "auto")]
|
#[serde(rename = "auto")]
|
||||||
Auto,
|
Auto,
|
||||||
@@ -6369,7 +6351,6 @@ pub enum EncoderPreset {
|
|||||||
Ultrafast,
|
Ultrafast,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum EncodingContext {
|
pub enum EncodingContext {
|
||||||
#[serde(rename = "Streaming")]
|
#[serde(rename = "Streaming")]
|
||||||
Streaming,
|
Streaming,
|
||||||
@@ -6378,7 +6359,6 @@ pub enum EncodingContext {
|
|||||||
}
|
}
|
||||||
/// The specific media type of an MediaBrowser.Model.Providers.ExternalIdInfo.
|
/// The specific media type of an MediaBrowser.Model.Providers.ExternalIdInfo.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ExternalIdMediaType {
|
pub enum ExternalIdMediaType {
|
||||||
#[serde(rename = "Album")]
|
#[serde(rename = "Album")]
|
||||||
Album,
|
Album,
|
||||||
@@ -6408,7 +6388,6 @@ pub enum ExternalIdMediaType {
|
|||||||
Book,
|
Book,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ExtraType {
|
pub enum ExtraType {
|
||||||
#[serde(rename = "Unknown")]
|
#[serde(rename = "Unknown")]
|
||||||
Unknown,
|
Unknown,
|
||||||
@@ -6437,7 +6416,6 @@ pub enum ExtraType {
|
|||||||
}
|
}
|
||||||
/// Enum FileSystemEntryType.
|
/// Enum FileSystemEntryType.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum FileSystemEntryType {
|
pub enum FileSystemEntryType {
|
||||||
#[serde(rename = "File")]
|
#[serde(rename = "File")]
|
||||||
File,
|
File,
|
||||||
@@ -6449,7 +6427,6 @@ pub enum FileSystemEntryType {
|
|||||||
NetworkShare,
|
NetworkShare,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ForgotPasswordAction {
|
pub enum ForgotPasswordAction {
|
||||||
#[serde(rename = "ContactAdmin")]
|
#[serde(rename = "ContactAdmin")]
|
||||||
ContactAdmin,
|
ContactAdmin,
|
||||||
@@ -6460,7 +6437,6 @@ pub enum ForgotPasswordAction {
|
|||||||
}
|
}
|
||||||
/// This exists simply to identify a set of known commands.
|
/// This exists simply to identify a set of known commands.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum GeneralCommandType {
|
pub enum GeneralCommandType {
|
||||||
#[serde(rename = "MoveUp")]
|
#[serde(rename = "MoveUp")]
|
||||||
MoveUp,
|
MoveUp,
|
||||||
@@ -6551,7 +6527,6 @@ pub enum GeneralCommandType {
|
|||||||
}
|
}
|
||||||
/// Enum GroupQueueMode.
|
/// Enum GroupQueueMode.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum GroupQueueMode {
|
pub enum GroupQueueMode {
|
||||||
#[serde(rename = "Queue")]
|
#[serde(rename = "Queue")]
|
||||||
Queue,
|
Queue,
|
||||||
@@ -6560,7 +6535,6 @@ pub enum GroupQueueMode {
|
|||||||
}
|
}
|
||||||
/// Enum GroupRepeatMode.
|
/// Enum GroupRepeatMode.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum GroupRepeatMode {
|
pub enum GroupRepeatMode {
|
||||||
#[serde(rename = "RepeatOne")]
|
#[serde(rename = "RepeatOne")]
|
||||||
RepeatOne,
|
RepeatOne,
|
||||||
@@ -6571,7 +6545,6 @@ pub enum GroupRepeatMode {
|
|||||||
}
|
}
|
||||||
/// Enum GroupShuffleMode.
|
/// Enum GroupShuffleMode.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum GroupShuffleMode {
|
pub enum GroupShuffleMode {
|
||||||
#[serde(rename = "Sorted")]
|
#[serde(rename = "Sorted")]
|
||||||
Sorted,
|
Sorted,
|
||||||
@@ -6580,7 +6553,6 @@ pub enum GroupShuffleMode {
|
|||||||
}
|
}
|
||||||
/// Enum GroupState.
|
/// Enum GroupState.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum GroupStateType {
|
pub enum GroupStateType {
|
||||||
#[serde(rename = "Idle")]
|
#[serde(rename = "Idle")]
|
||||||
Idle,
|
Idle,
|
||||||
@@ -6593,7 +6565,6 @@ pub enum GroupStateType {
|
|||||||
}
|
}
|
||||||
/// Enum GroupUpdateType.
|
/// Enum GroupUpdateType.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum GroupUpdateType {
|
pub enum GroupUpdateType {
|
||||||
#[serde(rename = "UserJoined")]
|
#[serde(rename = "UserJoined")]
|
||||||
UserJoined,
|
UserJoined,
|
||||||
@@ -6620,7 +6591,6 @@ pub enum GroupUpdateType {
|
|||||||
}
|
}
|
||||||
/// Enum containing hardware acceleration types.
|
/// Enum containing hardware acceleration types.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum HardwareAccelerationType {
|
pub enum HardwareAccelerationType {
|
||||||
#[serde(rename = "none")]
|
#[serde(rename = "none")]
|
||||||
None,
|
None,
|
||||||
@@ -6641,7 +6611,6 @@ pub enum HardwareAccelerationType {
|
|||||||
}
|
}
|
||||||
/// Enum ImageOutputFormat.
|
/// Enum ImageOutputFormat.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ImageFormat {
|
pub enum ImageFormat {
|
||||||
#[serde(rename = "Bmp")]
|
#[serde(rename = "Bmp")]
|
||||||
Bmp,
|
Bmp,
|
||||||
@@ -6657,7 +6626,6 @@ pub enum ImageFormat {
|
|||||||
Svg,
|
Svg,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ImageOrientation {
|
pub enum ImageOrientation {
|
||||||
#[serde(rename = "TopLeft")]
|
#[serde(rename = "TopLeft")]
|
||||||
TopLeft,
|
TopLeft,
|
||||||
@@ -6678,7 +6646,6 @@ pub enum ImageOrientation {
|
|||||||
}
|
}
|
||||||
/// Enum ImageResolution.
|
/// Enum ImageResolution.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ImageResolution {
|
pub enum ImageResolution {
|
||||||
#[serde(rename = "MatchSource")]
|
#[serde(rename = "MatchSource")]
|
||||||
MatchSource,
|
MatchSource,
|
||||||
@@ -6700,7 +6667,6 @@ pub enum ImageResolution {
|
|||||||
P2160,
|
P2160,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ImageSavingConvention {
|
pub enum ImageSavingConvention {
|
||||||
#[serde(rename = "Legacy")]
|
#[serde(rename = "Legacy")]
|
||||||
Legacy,
|
Legacy,
|
||||||
@@ -6709,7 +6675,6 @@ pub enum ImageSavingConvention {
|
|||||||
}
|
}
|
||||||
/// Enum ImageType.
|
/// Enum ImageType.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ImageType {
|
pub enum ImageType {
|
||||||
#[serde(rename = "Primary")]
|
#[serde(rename = "Primary")]
|
||||||
Primary,
|
Primary,
|
||||||
@@ -6740,7 +6705,6 @@ pub enum ImageType {
|
|||||||
}
|
}
|
||||||
/// Enum IsoType.
|
/// Enum IsoType.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum IsoType {
|
pub enum IsoType {
|
||||||
#[serde(rename = "Dvd")]
|
#[serde(rename = "Dvd")]
|
||||||
Dvd,
|
Dvd,
|
||||||
@@ -6749,7 +6713,6 @@ pub enum IsoType {
|
|||||||
}
|
}
|
||||||
/// Used to control the data that gets attached to DtoBaseItems.
|
/// Used to control the data that gets attached to DtoBaseItems.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ItemFields {
|
pub enum ItemFields {
|
||||||
#[serde(rename = "AirTime")]
|
#[serde(rename = "AirTime")]
|
||||||
AirTime,
|
AirTime,
|
||||||
@@ -6874,7 +6837,6 @@ pub enum ItemFields {
|
|||||||
}
|
}
|
||||||
/// Enum ItemFilter.
|
/// Enum ItemFilter.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ItemFilter {
|
pub enum ItemFilter {
|
||||||
#[serde(rename = "IsFolder")]
|
#[serde(rename = "IsFolder")]
|
||||||
IsFolder,
|
IsFolder,
|
||||||
@@ -6897,7 +6859,6 @@ pub enum ItemFilter {
|
|||||||
}
|
}
|
||||||
/// These represent sort orders.
|
/// These represent sort orders.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ItemSortBy {
|
pub enum ItemSortBy {
|
||||||
#[serde(rename = "Default")]
|
#[serde(rename = "Default")]
|
||||||
Default,
|
Default,
|
||||||
@@ -6965,7 +6926,6 @@ pub enum ItemSortBy {
|
|||||||
SearchScore,
|
SearchScore,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum KeepUntil {
|
pub enum KeepUntil {
|
||||||
#[serde(rename = "UntilDeleted")]
|
#[serde(rename = "UntilDeleted")]
|
||||||
UntilDeleted,
|
UntilDeleted,
|
||||||
@@ -6977,7 +6937,6 @@ pub enum KeepUntil {
|
|||||||
UntilDate,
|
UntilDate,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum LiveTvServiceStatus {
|
pub enum LiveTvServiceStatus {
|
||||||
#[serde(rename = "Ok")]
|
#[serde(rename = "Ok")]
|
||||||
Ok,
|
Ok,
|
||||||
@@ -6986,7 +6945,6 @@ pub enum LiveTvServiceStatus {
|
|||||||
}
|
}
|
||||||
/// Enum LocationType.
|
/// Enum LocationType.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum LocationType {
|
pub enum LocationType {
|
||||||
#[serde(rename = "FileSystem")]
|
#[serde(rename = "FileSystem")]
|
||||||
FileSystem,
|
FileSystem,
|
||||||
@@ -6998,7 +6956,6 @@ pub enum LocationType {
|
|||||||
Offline,
|
Offline,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum LogLevel {
|
pub enum LogLevel {
|
||||||
#[serde(rename = "Trace")]
|
#[serde(rename = "Trace")]
|
||||||
Trace,
|
Trace,
|
||||||
@@ -7016,7 +6973,6 @@ pub enum LogLevel {
|
|||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum MediaProtocol {
|
pub enum MediaProtocol {
|
||||||
#[serde(rename = "File")]
|
#[serde(rename = "File")]
|
||||||
File,
|
File,
|
||||||
@@ -7035,7 +6991,6 @@ pub enum MediaProtocol {
|
|||||||
}
|
}
|
||||||
/// Defines the types of content an individual Jellyfin.Data.Entities.MediaSegment represents.
|
/// Defines the types of content an individual Jellyfin.Data.Entities.MediaSegment represents.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum MediaSegmentType {
|
pub enum MediaSegmentType {
|
||||||
#[serde(rename = "Unknown")]
|
#[serde(rename = "Unknown")]
|
||||||
Unknown,
|
Unknown,
|
||||||
@@ -7051,7 +7006,6 @@ pub enum MediaSegmentType {
|
|||||||
Intro,
|
Intro,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum MediaSourceType {
|
pub enum MediaSourceType {
|
||||||
#[serde(rename = "Default")]
|
#[serde(rename = "Default")]
|
||||||
Default,
|
Default,
|
||||||
@@ -7063,7 +7017,6 @@ pub enum MediaSourceType {
|
|||||||
/** Media streaming protocol.
|
/** Media streaming protocol.
|
||||||
Lowercase for backwards compatibility.*/
|
Lowercase for backwards compatibility.*/
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum MediaStreamProtocol {
|
pub enum MediaStreamProtocol {
|
||||||
#[serde(rename = "http")]
|
#[serde(rename = "http")]
|
||||||
Http,
|
Http,
|
||||||
@@ -7072,7 +7025,6 @@ pub enum MediaStreamProtocol {
|
|||||||
}
|
}
|
||||||
/// Enum MediaStreamType.
|
/// Enum MediaStreamType.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum MediaStreamType {
|
pub enum MediaStreamType {
|
||||||
#[serde(rename = "Audio")]
|
#[serde(rename = "Audio")]
|
||||||
Audio,
|
Audio,
|
||||||
@@ -7089,7 +7041,6 @@ pub enum MediaStreamType {
|
|||||||
}
|
}
|
||||||
/// Media types.
|
/// Media types.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum MediaType {
|
pub enum MediaType {
|
||||||
#[serde(rename = "Unknown")]
|
#[serde(rename = "Unknown")]
|
||||||
Unknown,
|
Unknown,
|
||||||
@@ -7104,7 +7055,6 @@ pub enum MediaType {
|
|||||||
}
|
}
|
||||||
/// Enum MetadataFields.
|
/// Enum MetadataFields.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum MetadataField {
|
pub enum MetadataField {
|
||||||
#[serde(rename = "Cast")]
|
#[serde(rename = "Cast")]
|
||||||
Cast,
|
Cast,
|
||||||
@@ -7126,7 +7076,6 @@ pub enum MetadataField {
|
|||||||
OfficialRating,
|
OfficialRating,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum MetadataRefreshMode {
|
pub enum MetadataRefreshMode {
|
||||||
#[serde(rename = "None")]
|
#[serde(rename = "None")]
|
||||||
None,
|
None,
|
||||||
@@ -7138,7 +7087,6 @@ pub enum MetadataRefreshMode {
|
|||||||
FullRefresh,
|
FullRefresh,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ParameterInclude {
|
pub enum ParameterInclude {
|
||||||
#[serde(rename = "ProviderList")]
|
#[serde(rename = "ProviderList")]
|
||||||
ProviderList,
|
ProviderList,
|
||||||
@@ -7151,7 +7099,6 @@ pub enum ParameterInclude {
|
|||||||
}
|
}
|
||||||
/// The person kind.
|
/// The person kind.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum PersonKind {
|
pub enum PersonKind {
|
||||||
#[serde(rename = "Unknown")]
|
#[serde(rename = "Unknown")]
|
||||||
Unknown,
|
Unknown,
|
||||||
@@ -7205,7 +7152,6 @@ pub enum PersonKind {
|
|||||||
Translator,
|
Translator,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum PlayAccess {
|
pub enum PlayAccess {
|
||||||
#[serde(rename = "Full")]
|
#[serde(rename = "Full")]
|
||||||
Full,
|
Full,
|
||||||
@@ -7213,7 +7159,6 @@ pub enum PlayAccess {
|
|||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum PlaybackErrorCode {
|
pub enum PlaybackErrorCode {
|
||||||
#[serde(rename = "NotAllowed")]
|
#[serde(rename = "NotAllowed")]
|
||||||
NotAllowed,
|
NotAllowed,
|
||||||
@@ -7224,7 +7169,6 @@ pub enum PlaybackErrorCode {
|
|||||||
}
|
}
|
||||||
/// Enum PlaybackOrder.
|
/// Enum PlaybackOrder.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum PlaybackOrder {
|
pub enum PlaybackOrder {
|
||||||
#[serde(rename = "Default")]
|
#[serde(rename = "Default")]
|
||||||
Default,
|
Default,
|
||||||
@@ -7233,7 +7177,6 @@ pub enum PlaybackOrder {
|
|||||||
}
|
}
|
||||||
/// Enum PlaybackRequestType.
|
/// Enum PlaybackRequestType.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum PlaybackRequestType {
|
pub enum PlaybackRequestType {
|
||||||
#[serde(rename = "Play")]
|
#[serde(rename = "Play")]
|
||||||
Play,
|
Play,
|
||||||
@@ -7272,7 +7215,6 @@ pub enum PlaybackRequestType {
|
|||||||
}
|
}
|
||||||
/// Enum PlayCommand.
|
/// Enum PlayCommand.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum PlayCommand {
|
pub enum PlayCommand {
|
||||||
#[serde(rename = "PlayNow")]
|
#[serde(rename = "PlayNow")]
|
||||||
PlayNow,
|
PlayNow,
|
||||||
@@ -7286,7 +7228,6 @@ pub enum PlayCommand {
|
|||||||
PlayShuffle,
|
PlayShuffle,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum PlayMethod {
|
pub enum PlayMethod {
|
||||||
#[serde(rename = "Transcode")]
|
#[serde(rename = "Transcode")]
|
||||||
Transcode,
|
Transcode,
|
||||||
@@ -7297,7 +7238,6 @@ pub enum PlayMethod {
|
|||||||
}
|
}
|
||||||
/// Enum PlayQueueUpdateReason.
|
/// Enum PlayQueueUpdateReason.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum PlayQueueUpdateReason {
|
pub enum PlayQueueUpdateReason {
|
||||||
#[serde(rename = "NewPlaylist")]
|
#[serde(rename = "NewPlaylist")]
|
||||||
NewPlaylist,
|
NewPlaylist,
|
||||||
@@ -7322,7 +7262,6 @@ pub enum PlayQueueUpdateReason {
|
|||||||
}
|
}
|
||||||
/// Enum PlaystateCommand.
|
/// Enum PlaystateCommand.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum PlaystateCommand {
|
pub enum PlaystateCommand {
|
||||||
#[serde(rename = "Stop")]
|
#[serde(rename = "Stop")]
|
||||||
Stop,
|
Stop,
|
||||||
@@ -7345,7 +7284,6 @@ pub enum PlaystateCommand {
|
|||||||
}
|
}
|
||||||
/// Plugin load status.
|
/// Plugin load status.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum PluginStatus {
|
pub enum PluginStatus {
|
||||||
#[serde(rename = "Active")]
|
#[serde(rename = "Active")]
|
||||||
Active,
|
Active,
|
||||||
@@ -7363,7 +7301,6 @@ pub enum PluginStatus {
|
|||||||
Disabled,
|
Disabled,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ProcessPriorityClass {
|
pub enum ProcessPriorityClass {
|
||||||
#[serde(rename = "Normal")]
|
#[serde(rename = "Normal")]
|
||||||
Normal,
|
Normal,
|
||||||
@@ -7379,7 +7316,6 @@ pub enum ProcessPriorityClass {
|
|||||||
AboveNormal,
|
AboveNormal,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ProfileConditionType {
|
pub enum ProfileConditionType {
|
||||||
#[serde(rename = "Equals")]
|
#[serde(rename = "Equals")]
|
||||||
Equals,
|
Equals,
|
||||||
@@ -7393,7 +7329,6 @@ pub enum ProfileConditionType {
|
|||||||
EqualsAny,
|
EqualsAny,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ProfileConditionValue {
|
pub enum ProfileConditionValue {
|
||||||
#[serde(rename = "AudioChannels")]
|
#[serde(rename = "AudioChannels")]
|
||||||
AudioChannels,
|
AudioChannels,
|
||||||
@@ -7445,7 +7380,6 @@ pub enum ProfileConditionValue {
|
|||||||
VideoRangeType,
|
VideoRangeType,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ProgramAudio {
|
pub enum ProgramAudio {
|
||||||
#[serde(rename = "Mono")]
|
#[serde(rename = "Mono")]
|
||||||
Mono,
|
Mono,
|
||||||
@@ -7461,7 +7395,6 @@ pub enum ProgramAudio {
|
|||||||
Atmos,
|
Atmos,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum RatingType {
|
pub enum RatingType {
|
||||||
#[serde(rename = "Score")]
|
#[serde(rename = "Score")]
|
||||||
Score,
|
Score,
|
||||||
@@ -7469,7 +7402,6 @@ pub enum RatingType {
|
|||||||
Likes,
|
Likes,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum RecommendationType {
|
pub enum RecommendationType {
|
||||||
#[serde(rename = "SimilarToRecentlyPlayed")]
|
#[serde(rename = "SimilarToRecentlyPlayed")]
|
||||||
SimilarToRecentlyPlayed,
|
SimilarToRecentlyPlayed,
|
||||||
@@ -7485,7 +7417,6 @@ pub enum RecommendationType {
|
|||||||
HasLikedActor,
|
HasLikedActor,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum RecordingStatus {
|
pub enum RecordingStatus {
|
||||||
#[serde(rename = "New")]
|
#[serde(rename = "New")]
|
||||||
New,
|
New,
|
||||||
@@ -7503,7 +7434,6 @@ pub enum RecordingStatus {
|
|||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum RepeatMode {
|
pub enum RepeatMode {
|
||||||
#[serde(rename = "RepeatNone")]
|
#[serde(rename = "RepeatNone")]
|
||||||
RepeatNone,
|
RepeatNone,
|
||||||
@@ -7514,7 +7444,6 @@ pub enum RepeatMode {
|
|||||||
}
|
}
|
||||||
/// An enum representing the axis that should be scrolled.
|
/// An enum representing the axis that should be scrolled.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum ScrollDirection {
|
pub enum ScrollDirection {
|
||||||
#[serde(rename = "Horizontal")]
|
#[serde(rename = "Horizontal")]
|
||||||
Horizontal,
|
Horizontal,
|
||||||
@@ -7523,7 +7452,6 @@ pub enum ScrollDirection {
|
|||||||
}
|
}
|
||||||
/// Enum SendCommandType.
|
/// Enum SendCommandType.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum SendCommandType {
|
pub enum SendCommandType {
|
||||||
#[serde(rename = "Unpause")]
|
#[serde(rename = "Unpause")]
|
||||||
Unpause,
|
Unpause,
|
||||||
@@ -7536,7 +7464,6 @@ pub enum SendCommandType {
|
|||||||
}
|
}
|
||||||
/// The status of a series.
|
/// The status of a series.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum SeriesStatus {
|
pub enum SeriesStatus {
|
||||||
#[serde(rename = "Continuing")]
|
#[serde(rename = "Continuing")]
|
||||||
Continuing,
|
Continuing,
|
||||||
@@ -7547,7 +7474,6 @@ pub enum SeriesStatus {
|
|||||||
}
|
}
|
||||||
/// The different kinds of messages that are used in the WebSocket api.
|
/// The different kinds of messages that are used in the WebSocket api.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum SessionMessageType {
|
pub enum SessionMessageType {
|
||||||
#[serde(rename = "ForceKeepAlive")]
|
#[serde(rename = "ForceKeepAlive")]
|
||||||
ForceKeepAlive,
|
ForceKeepAlive,
|
||||||
@@ -7620,7 +7546,6 @@ pub enum SessionMessageType {
|
|||||||
}
|
}
|
||||||
/// An enum representing the sorting order.
|
/// An enum representing the sorting order.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum SortOrder {
|
pub enum SortOrder {
|
||||||
#[serde(rename = "Ascending")]
|
#[serde(rename = "Ascending")]
|
||||||
Ascending,
|
Ascending,
|
||||||
@@ -7628,7 +7553,6 @@ pub enum SortOrder {
|
|||||||
Descending,
|
Descending,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
#[serde(rename = "Completed")]
|
#[serde(rename = "Completed")]
|
||||||
Completed,
|
Completed,
|
||||||
@@ -7639,7 +7563,6 @@ pub enum Status {
|
|||||||
}
|
}
|
||||||
/// Delivery method to use during playback of a specific subtitle format.
|
/// Delivery method to use during playback of a specific subtitle format.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum SubtitleDeliveryMethod {
|
pub enum SubtitleDeliveryMethod {
|
||||||
#[serde(rename = "Encode")]
|
#[serde(rename = "Encode")]
|
||||||
Encode,
|
Encode,
|
||||||
@@ -7654,7 +7577,6 @@ pub enum SubtitleDeliveryMethod {
|
|||||||
}
|
}
|
||||||
/// An enum representing a subtitle playback mode.
|
/// An enum representing a subtitle playback mode.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum SubtitlePlaybackMode {
|
pub enum SubtitlePlaybackMode {
|
||||||
#[serde(rename = "Default")]
|
#[serde(rename = "Default")]
|
||||||
Default,
|
Default,
|
||||||
@@ -7668,7 +7590,6 @@ pub enum SubtitlePlaybackMode {
|
|||||||
Smart,
|
Smart,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum SyncAction {
|
pub enum SyncAction {
|
||||||
#[serde(rename = "UpdateProvider")]
|
#[serde(rename = "UpdateProvider")]
|
||||||
UpdateProvider,
|
UpdateProvider,
|
||||||
@@ -7677,7 +7598,6 @@ pub enum SyncAction {
|
|||||||
}
|
}
|
||||||
/// Enum SyncPlayUserAccessType.
|
/// Enum SyncPlayUserAccessType.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum SyncPlayUserAccessType {
|
pub enum SyncPlayUserAccessType {
|
||||||
#[serde(rename = "CreateAndJoinGroups")]
|
#[serde(rename = "CreateAndJoinGroups")]
|
||||||
CreateAndJoinGroups,
|
CreateAndJoinGroups,
|
||||||
@@ -7688,7 +7608,6 @@ pub enum SyncPlayUserAccessType {
|
|||||||
}
|
}
|
||||||
/// Enum TaskCompletionStatus.
|
/// Enum TaskCompletionStatus.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum TaskCompletionStatus {
|
pub enum TaskCompletionStatus {
|
||||||
#[serde(rename = "Completed")]
|
#[serde(rename = "Completed")]
|
||||||
Completed,
|
Completed,
|
||||||
@@ -7701,7 +7620,6 @@ pub enum TaskCompletionStatus {
|
|||||||
}
|
}
|
||||||
/// Enum TaskState.
|
/// Enum TaskState.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum TaskState {
|
pub enum TaskState {
|
||||||
#[serde(rename = "Idle")]
|
#[serde(rename = "Idle")]
|
||||||
Idle,
|
Idle,
|
||||||
@@ -7712,7 +7630,6 @@ pub enum TaskState {
|
|||||||
}
|
}
|
||||||
/// Enum containing tonemapping algorithms.
|
/// Enum containing tonemapping algorithms.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum TonemappingAlgorithm {
|
pub enum TonemappingAlgorithm {
|
||||||
#[serde(rename = "none")]
|
#[serde(rename = "none")]
|
||||||
None,
|
None,
|
||||||
@@ -7733,7 +7650,6 @@ pub enum TonemappingAlgorithm {
|
|||||||
}
|
}
|
||||||
/// Enum containing tonemapping modes.
|
/// Enum containing tonemapping modes.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum TonemappingMode {
|
pub enum TonemappingMode {
|
||||||
#[serde(rename = "auto")]
|
#[serde(rename = "auto")]
|
||||||
Auto,
|
Auto,
|
||||||
@@ -7748,7 +7664,6 @@ pub enum TonemappingMode {
|
|||||||
}
|
}
|
||||||
/// Enum containing tonemapping ranges.
|
/// Enum containing tonemapping ranges.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum TonemappingRange {
|
pub enum TonemappingRange {
|
||||||
#[serde(rename = "auto")]
|
#[serde(rename = "auto")]
|
||||||
Auto,
|
Auto,
|
||||||
@@ -7758,7 +7673,6 @@ pub enum TonemappingRange {
|
|||||||
Pc,
|
Pc,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum TranscodeReason {
|
pub enum TranscodeReason {
|
||||||
#[serde(rename = "ContainerNotSupported")]
|
#[serde(rename = "ContainerNotSupported")]
|
||||||
ContainerNotSupported,
|
ContainerNotSupported,
|
||||||
@@ -7814,7 +7728,6 @@ pub enum TranscodeReason {
|
|||||||
VideoCodecTagNotSupported,
|
VideoCodecTagNotSupported,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum TranscodeSeekInfo {
|
pub enum TranscodeSeekInfo {
|
||||||
#[serde(rename = "Auto")]
|
#[serde(rename = "Auto")]
|
||||||
Auto,
|
Auto,
|
||||||
@@ -7822,7 +7735,6 @@ pub enum TranscodeSeekInfo {
|
|||||||
Bytes,
|
Bytes,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum TransportStreamTimestamp {
|
pub enum TransportStreamTimestamp {
|
||||||
#[serde(rename = "None")]
|
#[serde(rename = "None")]
|
||||||
None,
|
None,
|
||||||
@@ -7833,7 +7745,6 @@ pub enum TransportStreamTimestamp {
|
|||||||
}
|
}
|
||||||
/// Enum TrickplayScanBehavior.
|
/// Enum TrickplayScanBehavior.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum TrickplayScanBehavior {
|
pub enum TrickplayScanBehavior {
|
||||||
#[serde(rename = "Blocking")]
|
#[serde(rename = "Blocking")]
|
||||||
Blocking,
|
Blocking,
|
||||||
@@ -7842,7 +7753,6 @@ pub enum TrickplayScanBehavior {
|
|||||||
}
|
}
|
||||||
/// An enum representing an unrated item.
|
/// An enum representing an unrated item.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum UnratedItem {
|
pub enum UnratedItem {
|
||||||
#[serde(rename = "Movie")]
|
#[serde(rename = "Movie")]
|
||||||
Movie,
|
Movie,
|
||||||
@@ -7864,7 +7774,6 @@ pub enum UnratedItem {
|
|||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum Video3DFormat {
|
pub enum Video3DFormat {
|
||||||
#[serde(rename = "HalfSideBySide")]
|
#[serde(rename = "HalfSideBySide")]
|
||||||
HalfSideBySide,
|
HalfSideBySide,
|
||||||
@@ -7879,7 +7788,6 @@ pub enum Video3DFormat {
|
|||||||
}
|
}
|
||||||
/// An enum representing video ranges.
|
/// An enum representing video ranges.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum VideoRange {
|
pub enum VideoRange {
|
||||||
#[serde(rename = "Unknown")]
|
#[serde(rename = "Unknown")]
|
||||||
Unknown,
|
Unknown,
|
||||||
@@ -7890,7 +7798,6 @@ pub enum VideoRange {
|
|||||||
}
|
}
|
||||||
/// An enum representing types of video ranges.
|
/// An enum representing types of video ranges.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum VideoRangeType {
|
pub enum VideoRangeType {
|
||||||
#[serde(rename = "Unknown")]
|
#[serde(rename = "Unknown")]
|
||||||
Unknown,
|
Unknown,
|
||||||
@@ -7913,7 +7820,6 @@ pub enum VideoRangeType {
|
|||||||
}
|
}
|
||||||
/// Enum VideoType.
|
/// Enum VideoType.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum VideoType {
|
pub enum VideoType {
|
||||||
#[serde(rename = "VideoFile")]
|
#[serde(rename = "VideoFile")]
|
||||||
VideoFile,
|
VideoFile,
|
||||||
|
|||||||
197
api/src/lib.rs
197
api/src/lib.rs
@@ -3,7 +3,7 @@ pub mod jellyfin;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ::tap::*;
|
use ::tap::*;
|
||||||
use reqwest::Method;
|
use reqwest::{Method, header::InvalidHeaderValue};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
@@ -15,6 +15,8 @@ pub enum JellyfinApiError {
|
|||||||
#[error("IO error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
IoError(#[from] std::io::Error),
|
IoError(#[from] std::io::Error),
|
||||||
#[error("Unknown Jellyfin API error")]
|
#[error("Unknown Jellyfin API error")]
|
||||||
|
InvalidHeader(#[from] InvalidHeaderValue),
|
||||||
|
#[error("Unknown Jellyfin API error")]
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +30,49 @@ pub struct JellyfinClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl JellyfinClient {
|
impl JellyfinClient {
|
||||||
pub fn new(config: JellyfinConfig) -> Self {
|
pub async fn authenticate(
|
||||||
|
username: impl AsRef<str>,
|
||||||
|
password: impl AsRef<str>,
|
||||||
|
config: JellyfinConfig,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let url = format!("{}/Users/AuthenticateByName", config.server_url);
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let token = client
|
||||||
|
.post(url)
|
||||||
|
.json(&jellyfin::AuthenticateUserByName {
|
||||||
|
username: Some(username.as_ref().to_string()),
|
||||||
|
pw: Some(password.as_ref().to_string()),
|
||||||
|
})
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json::<jellyfin::AuthenticationResult>()
|
||||||
|
.await?
|
||||||
|
.access_token
|
||||||
|
.ok_or_else(|| std::io::Error::other("No field access_token in auth response"))?;
|
||||||
|
Self::pre_authenticated(token, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pre_authenticated(token: impl AsRef<str>, config: JellyfinConfig) -> Result<Self> {
|
||||||
|
let auth_header = core::iter::once((
|
||||||
|
reqwest::header::HeaderName::from_static("x-emby-authorization"),
|
||||||
|
reqwest::header::HeaderValue::from_str(&format!(
|
||||||
|
"MediaBrowser Client=\"{}\", Device=\"{}\", DeviceId=\"{}\", Version=\"{}\"",
|
||||||
|
config.client_name, config.device_name, config.device_id, config.version
|
||||||
|
))?,
|
||||||
|
))
|
||||||
|
.collect();
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.default_headers(auth_header)
|
||||||
|
.build()?;
|
||||||
|
Ok(Self {
|
||||||
|
client,
|
||||||
|
access_token: Some(token.as_ref().to_string().into()),
|
||||||
|
config: Arc::new(config),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_config(config: JellyfinConfig) -> Self {
|
||||||
JellyfinClient {
|
JellyfinClient {
|
||||||
client: reqwest::Client::new(),
|
client: reqwest::Client::new(),
|
||||||
access_token: None,
|
access_token: None,
|
||||||
@@ -119,45 +163,6 @@ impl JellyfinClient {
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn authenticate(&mut self) -> Result<jellyfin::AuthenticationResult> {
|
|
||||||
let auth_result: jellyfin::AuthenticationResult = self
|
|
||||||
.post(
|
|
||||||
"Users/AuthenticateByName",
|
|
||||||
&jellyfin::AuthenticateUserByName {
|
|
||||||
username: Some(self.config.username.clone()),
|
|
||||||
pw: Some(self.config.password.clone()),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
self.access_token = auth_result.access_token.clone().map(Into::into);
|
|
||||||
Ok(auth_result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn authenticate_with_cached_token(
|
|
||||||
&mut self,
|
|
||||||
path: impl AsRef<std::path::Path>,
|
|
||||||
) -> Result<String> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
if let Ok(token) = self
|
|
||||||
.load_token(path)
|
|
||||||
.await
|
|
||||||
.inspect_err(|err| tracing::warn!("Failed to load cached token: {}", err))
|
|
||||||
{
|
|
||||||
tracing::info!("Authenticating with cached token from {:?}", path);
|
|
||||||
self.access_token = Some(token.clone().into());
|
|
||||||
Ok(token)
|
|
||||||
} else {
|
|
||||||
tracing::info!("No cached token found at {:?}, authenticating...", path);
|
|
||||||
let token = self
|
|
||||||
.authenticate()
|
|
||||||
.await?
|
|
||||||
.access_token
|
|
||||||
.ok_or_else(|| JellyfinApiError::Unknown)?;
|
|
||||||
self.save_token(path).await?;
|
|
||||||
Ok(token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn raw_items(&self) -> Result<jellyfin::BaseItemDtoQueryResult> {
|
pub async fn raw_items(&self) -> Result<jellyfin::BaseItemDtoQueryResult> {
|
||||||
let text = &self
|
let text = &self
|
||||||
.request_builder(Method::GET, "Items")
|
.request_builder(Method::GET, "Items")
|
||||||
@@ -185,49 +190,81 @@ impl JellyfinClient {
|
|||||||
let out: jellyfin::BaseItemDtoQueryResult = serde_json::from_str(&text)?;
|
let out: jellyfin::BaseItemDtoQueryResult = serde_json::from_str(&text)?;
|
||||||
Ok(out.items)
|
Ok(out.items)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// pub trait Item {
|
pub async fn search(&self, query: impl AsRef<str>) -> Result<Vec<jellyfin::BaseItemDto>> {
|
||||||
// fn id(&self) -> &str;
|
let text = &self
|
||||||
// fn name(&self) -> &str;
|
.request_builder(Method::GET, "Items/Search")
|
||||||
// fn type_(&self) -> jellyfin::BaseItemKind;
|
.query(&[("searchTerm", query.as_ref()), ("recursive", "true")])
|
||||||
// fn media_type(&self) -> &str;
|
.send()
|
||||||
// }
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.text()
|
||||||
|
.await?;
|
||||||
|
let out: jellyfin::BaseItemDtoQueryResult = serde_json::from_str(&text)?;
|
||||||
|
Ok(out.items)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
pub async fn thumbnail(
|
||||||
pub struct JellyfinConfig {
|
&self,
|
||||||
pub username: String,
|
item: uuid::Uuid,
|
||||||
pub password: String,
|
image_type: jellyfin::ImageType,
|
||||||
pub server_url: iref::IriBuf,
|
) -> Result<bytes::Bytes> {
|
||||||
pub device_id: String,
|
let uri = format!(
|
||||||
}
|
"Items/{}/Images/{}",
|
||||||
|
item,
|
||||||
|
serde_json::to_string(&image_type).expect("Failed to serialize image type")
|
||||||
|
);
|
||||||
|
let bytes = self
|
||||||
|
.request_builder(Method::GET, uri)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.bytes()
|
||||||
|
.await?;
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
impl JellyfinConfig {
|
pub async fn playback_info(&self, item: uuid::Uuid) -> Result<jellyfin::PlaybackInfoDto> {
|
||||||
pub fn new(
|
let uri = format!("Items/{}/PlaybackInfo", item);
|
||||||
username: String,
|
let text = &self
|
||||||
password: String,
|
.request_builder(Method::GET, uri)
|
||||||
server_url: impl AsRef<str>,
|
.send()
|
||||||
device_id: String,
|
.await?
|
||||||
) -> Self {
|
.error_for_status()?
|
||||||
JellyfinConfig {
|
.text()
|
||||||
username,
|
.await?;
|
||||||
password,
|
let out: jellyfin::PlaybackInfoDto = serde_json::from_str(&text)?;
|
||||||
server_url: iref::IriBuf::new(server_url.as_ref().into())
|
Ok(out)
|
||||||
.expect("Failed to parse server URL"),
|
}
|
||||||
device_id,
|
|
||||||
}
|
pub async fn user_data(&self, item: uuid::Uuid) -> Result<jellyfin::UserItemDataDto> {
|
||||||
|
let uri = format!("UserItems/{}/UserData", item);
|
||||||
|
let text = &self
|
||||||
|
.request_builder(Method::GET, uri)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.text()
|
||||||
|
.await?;
|
||||||
|
let out: jellyfin::UserItemDataDto = serde_json::from_str(&text)?;
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stream_url(&self, item: uuid::Uuid) -> Result<url::Url> {
|
||||||
|
let stream_url = format!(
|
||||||
|
"{}/Videos/{}/stream?static=true",
|
||||||
|
self.config.server_url.as_str(),
|
||||||
|
item,
|
||||||
|
);
|
||||||
|
Ok(url::Url::parse(&stream_url).expect("Failed to parse stream URL"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
fn test_client_authenticate() {
|
pub struct JellyfinConfig {
|
||||||
let config = JellyfinConfig {
|
pub server_url: iref::IriBuf,
|
||||||
username: "servius".to_string(),
|
pub device_id: String,
|
||||||
password: "nfz6yqr_NZD1nxk!faj".to_string(),
|
pub device_name: String,
|
||||||
server_url: iref::IriBuf::new("https://jellyfin.tsuba.darksailor.dev".into()).unwrap(),
|
pub client_name: String,
|
||||||
device_id: "testdeviceid".to_string(),
|
pub version: String,
|
||||||
};
|
|
||||||
let mut client = JellyfinClient::new(config);
|
|
||||||
let auth_result = tokio_test::block_on(client.authenticate());
|
|
||||||
assert!(auth_result.is_ok());
|
|
||||||
}
|
}
|
||||||
|
|||||||
29
crates/iced-video/Cargo.toml
Normal file
29
crates/iced-video/Cargo.toml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
[package]
|
||||||
|
name = "iced-video"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bytemuck = "1.24.0"
|
||||||
|
error-stack = "0.6.0"
|
||||||
|
futures-lite = "2.6.1"
|
||||||
|
gst.workspace = true
|
||||||
|
iced_core = "0.14.0"
|
||||||
|
iced_futures = "0.14.0"
|
||||||
|
iced_renderer = { version = "0.14.0", features = ["iced_wgpu"] }
|
||||||
|
iced_wgpu = { version = "0.14.0" }
|
||||||
|
thiserror = "2.0.17"
|
||||||
|
tracing = "0.1.43"
|
||||||
|
wgpu = { version = "27.0.1", features = ["vulkan"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
iced.workspace = true
|
||||||
|
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
debug = true
|
||||||
|
[profile.release]
|
||||||
|
debug = true
|
||||||
|
|
||||||
|
# [patch.crates-io]
|
||||||
|
# iced_wgpu = { git = "https://github.com/uttarayan21/iced", branch = "0.14" }
|
||||||
178
crates/iced-video/examples/minimal.rs
Normal file
178
crates/iced-video/examples/minimal.rs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
use iced_video::{Video, VideoHandle};
|
||||||
|
|
||||||
|
pub fn main() -> iced::Result {
|
||||||
|
use tracing_subscriber::prelude::*;
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::fmt::layer()
|
||||||
|
.with_thread_ids(true)
|
||||||
|
.with_file(true),
|
||||||
|
)
|
||||||
|
.with(tracing_subscriber::EnvFilter::from_default_env())
|
||||||
|
.init();
|
||||||
|
iced::application(State::new, update, view)
|
||||||
|
.subscription(|state| {
|
||||||
|
// Foo
|
||||||
|
match &state.video {
|
||||||
|
Some(video) => video.subscription_with(state, keyboard_event),
|
||||||
|
None => keyboard_event(state),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keyboard_event(_state: &State) -> iced::Subscription<Message> {
|
||||||
|
use iced::keyboard::{Key, key::Named};
|
||||||
|
iced::keyboard::listen().map(move |event| match event {
|
||||||
|
iced::keyboard::Event::KeyPressed { key, .. } => {
|
||||||
|
let key = key.as_ref();
|
||||||
|
match key {
|
||||||
|
Key::Named(Named::Escape) | Key::Character("q") => Message::Quit,
|
||||||
|
Key::Character("f") => Message::Fullscreen,
|
||||||
|
Key::Named(Named::Space) => Message::Toggle,
|
||||||
|
_ => Message::Noop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Message::Noop,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct State {
|
||||||
|
video: Option<VideoHandle<Message>>,
|
||||||
|
fullscreen: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub fn new() -> (Self, iced::Task<Message>) {
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
video: None,
|
||||||
|
fullscreen: false,
|
||||||
|
},
|
||||||
|
iced::Task::done(Message::Load),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Message {
|
||||||
|
Play,
|
||||||
|
Pause,
|
||||||
|
Toggle,
|
||||||
|
Noop,
|
||||||
|
Load,
|
||||||
|
Fullscreen,
|
||||||
|
OnLoad(VideoHandle<Message>),
|
||||||
|
OnError(String),
|
||||||
|
NewFrame,
|
||||||
|
Eos,
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(state: &mut State, message: Message) -> iced::Task<Message> {
|
||||||
|
match message {
|
||||||
|
Message::NewFrame => {
|
||||||
|
iced::Task::none()
|
||||||
|
}
|
||||||
|
Message::Eos => {
|
||||||
|
iced::Task::done(Message::Pause)
|
||||||
|
}
|
||||||
|
Message::Load => {
|
||||||
|
iced::Task::perform(
|
||||||
|
VideoHandle::load(
|
||||||
|
"https://jellyfin.tsuba.darksailor.dev/Items/6010382cf25273e624d305907010d773/Download?api_key=036c140222464878862231ef66a2bc9c",
|
||||||
|
),
|
||||||
|
|result| match result {
|
||||||
|
Ok(video) => Message::OnLoad(video),
|
||||||
|
Err(err) => Message::OnError(format!("Error loading video: {:?}", err)),
|
||||||
|
},
|
||||||
|
).chain(iced::Task::done(Message::Play))
|
||||||
|
}
|
||||||
|
Message::OnError(err) => {
|
||||||
|
eprintln!("Error: {}", err);
|
||||||
|
iced::Task::none()
|
||||||
|
}
|
||||||
|
Message::OnLoad(video) => {
|
||||||
|
state.video = Some(video.on_new_frame(Message::NewFrame).on_end_of_stream(Message::Eos));
|
||||||
|
iced::Task::none()
|
||||||
|
}
|
||||||
|
Message::Fullscreen => {
|
||||||
|
state.fullscreen = !state.fullscreen;
|
||||||
|
let fullscreen = state.fullscreen;
|
||||||
|
let mode = if fullscreen {
|
||||||
|
iced::window::Mode::Fullscreen
|
||||||
|
} else {
|
||||||
|
iced::window::Mode::Windowed
|
||||||
|
};
|
||||||
|
iced::window::oldest().and_then(move |id| iced::window::set_mode::<Message>(id, mode))
|
||||||
|
}
|
||||||
|
Message::Play => {
|
||||||
|
state
|
||||||
|
.video
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.source()
|
||||||
|
.play()
|
||||||
|
.expect("Failed to play video");
|
||||||
|
iced::Task::none()
|
||||||
|
}
|
||||||
|
Message::Pause => {
|
||||||
|
state
|
||||||
|
.video
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.source()
|
||||||
|
.pause()
|
||||||
|
.expect("Failed to pause video");
|
||||||
|
iced::Task::none()
|
||||||
|
}
|
||||||
|
Message::Toggle => {
|
||||||
|
state
|
||||||
|
.video
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.source()
|
||||||
|
.toggle()
|
||||||
|
.expect("Failed to stop video");
|
||||||
|
iced::Task::none()
|
||||||
|
}
|
||||||
|
Message::Quit => {
|
||||||
|
state
|
||||||
|
.video
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.source()
|
||||||
|
.stop()
|
||||||
|
.expect("Failed to stop video");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
Message::Noop => iced::Task::none(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view<'a>(state: &'a State) -> iced::Element<'a, Message> {
|
||||||
|
if let None = &state.video {
|
||||||
|
return iced::widget::Column::new()
|
||||||
|
.push(iced::widget::Text::new("Press any key to load video"))
|
||||||
|
.align_x(iced::Alignment::Center)
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
let video_widget = Video::new(&state.video.as_ref().unwrap())
|
||||||
|
.width(iced::Length::Fill)
|
||||||
|
.height(iced::Length::Fill)
|
||||||
|
.content_fit(iced::ContentFit::Contain);
|
||||||
|
|
||||||
|
iced::widget::Column::new()
|
||||||
|
.push(video_widget)
|
||||||
|
.push(
|
||||||
|
iced::widget::Row::new()
|
||||||
|
.push(iced::widget::Button::new("Play").on_press(Message::Play))
|
||||||
|
.push(iced::widget::Button::new("Pause").on_press(Message::Pause))
|
||||||
|
.spacing(5)
|
||||||
|
.padding(10)
|
||||||
|
.align_y(iced::Alignment::Center),
|
||||||
|
)
|
||||||
|
.align_x(iced::Alignment::Center)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
8
crates/iced-video/justfile
Normal file
8
crates/iced-video/justfile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
info:
|
||||||
|
RUST_LOG=info,wgpu_core=warn,wgpu_hal=warn cargo run --release --example minimal
|
||||||
|
# GST_DEBUG=5 RUST_LOG="" cargo run --release --example minimal
|
||||||
|
flame:
|
||||||
|
cargo flamegraph run --release --example minimal
|
||||||
|
heaptrack:
|
||||||
|
cargo build --release --example minimal
|
||||||
|
RUST_LOG="info,wgpu_hal=info" heaptrack $CARGO_TARGET_DIR/release/examples/minimal
|
||||||
55
crates/iced-video/src/id.rs
Normal file
55
crates/iced-video/src/id.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
use std::borrow;
|
||||||
|
use std::sync::atomic::{self, AtomicUsize};
|
||||||
|
|
||||||
|
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
|
/// The identifier of a generic widget.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct Id(Internal);
|
||||||
|
|
||||||
|
impl Id {
|
||||||
|
/// Creates a new [`Id`] from a static `str`.
|
||||||
|
pub const fn new(id: &'static str) -> Self {
|
||||||
|
Self(Internal::Custom(borrow::Cow::Borrowed(id)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a unique [`Id`].
|
||||||
|
///
|
||||||
|
/// This function produces a different [`Id`] every time it is called.
|
||||||
|
pub fn unique() -> Self {
|
||||||
|
let id = NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
|
Self(Internal::Unique(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&'static str> for Id {
|
||||||
|
fn from(value: &'static str) -> Self {
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Id {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self(Internal::Custom(borrow::Cow::Owned(value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
enum Internal {
|
||||||
|
Unique(usize),
|
||||||
|
Custom(borrow::Cow<'static, str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::Id;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unique_generates_different_ids() {
|
||||||
|
let a = Id::unique();
|
||||||
|
let b = Id::unique();
|
||||||
|
|
||||||
|
assert_ne!(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
164
crates/iced-video/src/lib.rs
Normal file
164
crates/iced-video/src/lib.rs
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
pub mod id;
|
||||||
|
pub mod primitive;
|
||||||
|
pub mod source;
|
||||||
|
pub mod widget;
|
||||||
|
pub use widget::Video;
|
||||||
|
|
||||||
|
use error_stack::{Report, ResultExt};
|
||||||
|
|
||||||
|
use gst::plugins::app::AppSink;
|
||||||
|
use gst::plugins::playback::Playbin3;
|
||||||
|
use gst::plugins::videoconvertscale::VideoConvert;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[error("Iced Video Error")]
|
||||||
|
pub struct Error;
|
||||||
|
pub type Result<T, E = Report<Error>> = core::result::Result<T, E>;
|
||||||
|
|
||||||
|
use std::sync::{Arc, Mutex, atomic::AtomicBool};
|
||||||
|
|
||||||
|
mod seal {
|
||||||
|
pub trait Sealed {}
|
||||||
|
impl Sealed for super::Unknown {}
|
||||||
|
impl Sealed for super::Ready {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait State: seal::Sealed {
|
||||||
|
fn is_ready() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Unknown;
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Ready;
|
||||||
|
impl State for Unknown {}
|
||||||
|
impl State for Ready {
|
||||||
|
fn is_ready() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is the video handle that is used to control the video playback.
|
||||||
|
/// This should be keps in the application state.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct VideoHandle<Message, S: State = Unknown> {
|
||||||
|
id: id::Id,
|
||||||
|
pub source: source::VideoSource,
|
||||||
|
frame_ready: Arc<AtomicBool>,
|
||||||
|
on_new_frame: Option<Box<Message>>,
|
||||||
|
on_end_of_stream: Option<Box<Message>>,
|
||||||
|
on_about_to_finish: Option<Box<Message>>,
|
||||||
|
__marker: core::marker::PhantomData<S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message: Send + Sync + Clone> VideoHandle<Message, Unknown> {
|
||||||
|
pub fn new(url: impl AsRef<str>) -> Result<Self> {
|
||||||
|
let source = source::VideoSource::new(url)?;
|
||||||
|
let frame_ready = Arc::clone(&source.ready);
|
||||||
|
Ok(Self {
|
||||||
|
id: id::Id::unique(),
|
||||||
|
source: source,
|
||||||
|
on_new_frame: None,
|
||||||
|
on_end_of_stream: None,
|
||||||
|
on_about_to_finish: None,
|
||||||
|
frame_ready,
|
||||||
|
__marker: core::marker::PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new video handle and waits for the metadata to be loaded.
|
||||||
|
pub async fn load(url: impl AsRef<str>) -> Result<VideoHandle<Message, Ready>> {
|
||||||
|
let handle = VideoHandle::new(url)?;
|
||||||
|
handle.wait().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<Message: Send + Sync + Clone, S: State> VideoHandle<Message, S> {
|
||||||
|
pub fn id(&self) -> &id::Id {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn source(&self) -> &source::VideoSource {
|
||||||
|
&self.source
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wait(self) -> Result<VideoHandle<Message, Ready>> {
|
||||||
|
self.source.wait().await?;
|
||||||
|
Ok(self.state::<Ready>())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state<S2: State>(self) -> VideoHandle<Message, S2> {
|
||||||
|
VideoHandle {
|
||||||
|
id: self.id,
|
||||||
|
source: self.source,
|
||||||
|
on_new_frame: self.on_new_frame,
|
||||||
|
on_end_of_stream: self.on_end_of_stream,
|
||||||
|
on_about_to_finish: self.on_about_to_finish,
|
||||||
|
frame_ready: self.frame_ready,
|
||||||
|
__marker: core::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn subscription(&self) -> iced_futures::subscription::Subscription<Message> {
|
||||||
|
// let sub = widget::VideoSubscription {
|
||||||
|
// id: self.id.clone(),
|
||||||
|
// on_end_of_stream: self.on_end_of_stream.clone(),
|
||||||
|
// on_new_frame: self.on_new_frame.clone(),
|
||||||
|
// on_about_to_finish: self.on_about_to_finish.clone(),
|
||||||
|
// bus: self.source.bus.clone(),
|
||||||
|
// };
|
||||||
|
// iced_futures::subscription::from_recipe(sub)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// pub fn subscription_with<State>(
|
||||||
|
// &self,
|
||||||
|
// state: &State,
|
||||||
|
// f: impl FnOnce(&State) -> iced_futures::subscription::Subscription<Message> + 'static,
|
||||||
|
// ) -> iced_futures::subscription::Subscription<Message>
|
||||||
|
// where
|
||||||
|
// State: Send + Sync + 'static,
|
||||||
|
// {
|
||||||
|
// let sub = self.subscription();
|
||||||
|
// iced_futures::subscription::Subscription::batch([sub, f(state)])
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub fn on_new_frame(self, message: Message) -> Self {
|
||||||
|
Self {
|
||||||
|
on_new_frame: Some(Box::new(message)),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_end_of_stream(self, message: Message) -> Self {
|
||||||
|
Self {
|
||||||
|
on_end_of_stream: Some(Box::new(message)),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_about_to_finish(self, message: Message) -> Self {
|
||||||
|
Self {
|
||||||
|
on_about_to_finish: Some(Box::new(message)),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play(&self) {
|
||||||
|
self.source.play();
|
||||||
|
}
|
||||||
|
pub fn pause(&self) {
|
||||||
|
self.source.pause();
|
||||||
|
}
|
||||||
|
pub fn stop(&self) {
|
||||||
|
self.source.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message: Send + Sync + Clone> VideoHandle<Message, Ready> {
|
||||||
|
pub fn format(&self) -> Result<gst::VideoFormat> {
|
||||||
|
self.source
|
||||||
|
.format()
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to get video format")
|
||||||
|
}
|
||||||
|
}
|
||||||
574
crates/iced-video/src/primitive.rs
Normal file
574
crates/iced-video/src/primitive.rs
Normal file
@@ -0,0 +1,574 @@
|
|||||||
|
use crate::id;
|
||||||
|
use gst::videoconvertscale::VideoFormat;
|
||||||
|
use iced_wgpu::primitive::Pipeline;
|
||||||
|
use iced_wgpu::wgpu;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::sync::{Arc, Mutex, atomic::AtomicBool};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, bytemuck::Zeroable, bytemuck::Pod)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct ConversionMatrix {
|
||||||
|
matrix: [Vec3f; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, bytemuck::Zeroable, bytemuck::Pod)]
|
||||||
|
#[repr(C, align(16))]
|
||||||
|
pub struct Vec3f {
|
||||||
|
data: [f32; 3],
|
||||||
|
__padding: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[f32; 3]> for Vec3f {
|
||||||
|
fn from(value: [f32; 3]) -> Self {
|
||||||
|
Vec3f {
|
||||||
|
data: [value[0], value[1], value[2]],
|
||||||
|
__padding: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vec3f {
|
||||||
|
pub fn new(x: f32, y: f32, z: f32) -> Self {
|
||||||
|
Vec3f {
|
||||||
|
data: [x, y, z],
|
||||||
|
__padding: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub const fn from(data: [f32; 3]) -> Self {
|
||||||
|
Vec3f {
|
||||||
|
data: [data[0], data[1], data[2]],
|
||||||
|
__padding: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl ConversionMatrix {
|
||||||
|
// pub fn desc() -> wgpu::VertexBufferLayout<'static> {
|
||||||
|
// wgpu::VertexBufferLayout {
|
||||||
|
// array_stride: core::mem::size_of::<ConversionMatrix>() as wgpu::BufferAddress,
|
||||||
|
// step_mode: wgpu::VertexStepMode::Vertex,
|
||||||
|
// attributes: &[
|
||||||
|
// wgpu::VertexAttribute {
|
||||||
|
// offset: 0,
|
||||||
|
// shader_location: 0,
|
||||||
|
// format: wgpu::VertexFormat::Float32x4,
|
||||||
|
// },
|
||||||
|
// wgpu::VertexAttribute {
|
||||||
|
// offset: 16,
|
||||||
|
// shader_location: 1,
|
||||||
|
// format: wgpu::VertexFormat::Float32x4,
|
||||||
|
// },
|
||||||
|
// wgpu::VertexAttribute {
|
||||||
|
// offset: 32,
|
||||||
|
// shader_location: 2,
|
||||||
|
// format: wgpu::VertexFormat::Float32x4,
|
||||||
|
// },
|
||||||
|
// wgpu::VertexAttribute {
|
||||||
|
// offset: 48,
|
||||||
|
// shader_location: 3,
|
||||||
|
// format: wgpu::VertexFormat::Float32x4,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub const BT2020_TO_RGB: ConversionMatrix = ConversionMatrix {
|
||||||
|
matrix: [
|
||||||
|
Vec3f::from([1.0, 0.0, 1.4746]),
|
||||||
|
Vec3f::from([1.0, -0.16455, -0.5714]),
|
||||||
|
Vec3f::from([1.0, 1.8814, 0.0]),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const BT709_TO_RGB: ConversionMatrix = ConversionMatrix {
|
||||||
|
matrix: [
|
||||||
|
Vec3f::from([1.0, 0.0, 1.5748]),
|
||||||
|
Vec3f::from([1.0, -0.1873, -0.4681]),
|
||||||
|
Vec3f::from([1.0, 1.8556, 0.0]),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct VideoFrame {
|
||||||
|
pub id: id::Id,
|
||||||
|
pub size: wgpu::Extent3d,
|
||||||
|
pub ready: Arc<AtomicBool>,
|
||||||
|
pub frame: Arc<Mutex<gst::Sample>>,
|
||||||
|
pub format: VideoFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl iced_wgpu::Primitive for VideoFrame {
|
||||||
|
type Pipeline = VideoPipeline;
|
||||||
|
|
||||||
|
fn prepare(
|
||||||
|
&self,
|
||||||
|
pipeline: &mut Self::Pipeline,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
bounds: &iced_wgpu::core::Rectangle,
|
||||||
|
viewport: &iced_wgpu::graphics::Viewport,
|
||||||
|
) {
|
||||||
|
let video = pipeline.videos.entry(self.id.clone()).or_insert_with(|| {
|
||||||
|
let texture = VideoTexture::new(
|
||||||
|
"iced-video-texture",
|
||||||
|
self.size,
|
||||||
|
device,
|
||||||
|
pipeline.format,
|
||||||
|
self.format,
|
||||||
|
);
|
||||||
|
|
||||||
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("iced-video-texture-bind-group"),
|
||||||
|
layout: &pipeline.bind_group_layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::TextureView(&texture.y_texture()),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: wgpu::BindingResource::TextureView(&texture.uv_texture()),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: wgpu::BindingResource::Sampler(&pipeline.sampler),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 3,
|
||||||
|
resource: wgpu::BindingResource::Buffer(
|
||||||
|
texture
|
||||||
|
.conversion_matrix_buffer()
|
||||||
|
.as_entire_buffer_binding(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let matrix = if matches!(self.format, VideoFormat::P01010le | VideoFormat::P016Le) {
|
||||||
|
BT2020_TO_RGB
|
||||||
|
} else {
|
||||||
|
BT709_TO_RGB
|
||||||
|
};
|
||||||
|
|
||||||
|
texture.write_conversion_matrix(&matrix, queue);
|
||||||
|
|
||||||
|
VideoFrameData {
|
||||||
|
id: self.id.clone(),
|
||||||
|
texture,
|
||||||
|
bind_group,
|
||||||
|
conversion_matrix: matrix,
|
||||||
|
ready: Arc::clone(&self.ready),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if self.size != video.texture.size() {
|
||||||
|
let new_texture = video
|
||||||
|
.texture
|
||||||
|
.resize("iced-video-texture-resized", self.size, device);
|
||||||
|
|
||||||
|
new_texture.write_conversion_matrix(&video.conversion_matrix, queue);
|
||||||
|
|
||||||
|
let new_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("iced-video-texture-bind-group"),
|
||||||
|
layout: &pipeline.bind_group_layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::TextureView(&new_texture.y_texture()),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: wgpu::BindingResource::TextureView(&new_texture.uv_texture()),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: wgpu::BindingResource::Sampler(&pipeline.sampler),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 3,
|
||||||
|
resource: wgpu::BindingResource::Buffer(
|
||||||
|
video
|
||||||
|
.texture
|
||||||
|
.conversion_matrix_buffer()
|
||||||
|
.as_entire_buffer_binding(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
video.texture = new_texture;
|
||||||
|
video.bind_group = new_bind_group;
|
||||||
|
}
|
||||||
|
if video.ready.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
|
let frame = self.frame.lock().expect("BUG: Mutex poisoned");
|
||||||
|
let buffer = frame
|
||||||
|
.buffer()
|
||||||
|
.expect("BUG: Failed to get frame data from gst::Sample");
|
||||||
|
|
||||||
|
let data = buffer
|
||||||
|
.map_readable()
|
||||||
|
.expect("BUG: Failed to map gst::Buffer readable");
|
||||||
|
|
||||||
|
video.texture.write_texture(&data, queue);
|
||||||
|
|
||||||
|
drop(data);
|
||||||
|
video
|
||||||
|
.ready
|
||||||
|
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(
|
||||||
|
&self,
|
||||||
|
pipeline: &Self::Pipeline,
|
||||||
|
encoder: &mut wgpu::CommandEncoder,
|
||||||
|
target: &wgpu::TextureView,
|
||||||
|
bounds: &iced_wgpu::core::Rectangle<u32>,
|
||||||
|
) {
|
||||||
|
let Some(video) = pipeline.videos.get(&self.id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: Some("iced-video-render-pass"),
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: target,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Load,
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
depth_slice: None,
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
timestamp_writes: None,
|
||||||
|
occlusion_query_set: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
render_pass.set_pipeline(&pipeline.pipeline);
|
||||||
|
render_pass.set_bind_group(0, &video.bind_group, &[]);
|
||||||
|
render_pass.set_scissor_rect(
|
||||||
|
bounds.x as _,
|
||||||
|
bounds.y as _,
|
||||||
|
bounds.width as _,
|
||||||
|
bounds.height as _,
|
||||||
|
);
|
||||||
|
render_pass.draw(0..3, 0..1);
|
||||||
|
// self.ready
|
||||||
|
// .store(false, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NV12 or P010 are only supported in DX12 and Vulkan backends.
|
||||||
|
/// While we can use vulkan with moltenvk on macos, I'd much rather use metal directly
|
||||||
|
/// Right now only supports interleaved UV formats.
|
||||||
|
/// For planar formats we would need 3 textures.
|
||||||
|
/// Also NV12 and P010 textures are not COPY_DST capable
|
||||||
|
/// This assumes 4:2:0 chroma subsampling (for now).
|
||||||
|
/// So for 4 Y samples there is 1 U and 1 V sample.
|
||||||
|
/// This means that the UV texture is half the width and half the height of the Y texture.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct VideoTexture {
|
||||||
|
y: wgpu::Texture,
|
||||||
|
uv: wgpu::Texture,
|
||||||
|
size: wgpu::Extent3d,
|
||||||
|
video_format: VideoFormat,
|
||||||
|
surface_format: wgpu::TextureFormat,
|
||||||
|
conversion_matrix_buffer: wgpu::Buffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoTexture {
|
||||||
|
pub fn size(&self) -> wgpu::Extent3d {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(
|
||||||
|
label: &str,
|
||||||
|
size: wgpu::Extent3d,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
surface_format: wgpu::TextureFormat,
|
||||||
|
video_format: VideoFormat,
|
||||||
|
) -> Self {
|
||||||
|
let surface_hdr = surface_format.is_wide();
|
||||||
|
let video_hdr = matches!(video_format, VideoFormat::P01010le | VideoFormat::P016Le);
|
||||||
|
|
||||||
|
if surface_hdr && !video_hdr {
|
||||||
|
tracing::warn!("Surface texture is HDR but video format is SDR");
|
||||||
|
} else if !surface_hdr && video_hdr {
|
||||||
|
tracing::warn!("Video format is HDR but surface does not support HDR");
|
||||||
|
}
|
||||||
|
|
||||||
|
let y_texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some(&format!("{}-y", label)),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: wgpu::TextureFormat::R16Unorm,
|
||||||
|
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
let uv_texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some(&format!("{}-uv", label)),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: size.width / 2,
|
||||||
|
height: size.height / 2,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: wgpu::TextureFormat::Rg16Unorm,
|
||||||
|
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("iced-video-conversion-matrix-buffer"),
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
|
size: core::mem::size_of::<ConversionMatrix>() as wgpu::BufferAddress,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
VideoTexture {
|
||||||
|
y: y_texture,
|
||||||
|
uv: uv_texture,
|
||||||
|
size,
|
||||||
|
surface_format,
|
||||||
|
video_format,
|
||||||
|
conversion_matrix_buffer: buffer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This return the surface texture format, not the video pixel format
|
||||||
|
pub fn format(&self) -> wgpu::TextureFormat {
|
||||||
|
self.surface_format
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn y_texture(&self) -> wgpu::TextureView {
|
||||||
|
self.y.create_view(&wgpu::TextureViewDescriptor::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uv_texture(&self) -> wgpu::TextureView {
|
||||||
|
self.uv.create_view(&wgpu::TextureViewDescriptor::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize(&self, name: &str, new_size: wgpu::Extent3d, device: &wgpu::Device) -> Self {
|
||||||
|
VideoTexture::new(name, new_size, device, self.format(), self.pixel_format())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pixel_format(&self) -> VideoFormat {
|
||||||
|
self.video_format
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This assumes that the data is laid out correctly for the texture format.
|
||||||
|
pub fn write_texture(&self, data: &[u8], queue: &wgpu::Queue) {
|
||||||
|
let Self { y, uv, .. } = self;
|
||||||
|
let y_size = y.size();
|
||||||
|
let uv_size = uv.size();
|
||||||
|
|
||||||
|
let y_data_size = (y_size.width * y_size.height * 2) as usize;
|
||||||
|
let uv_data_size = (y_data_size / 2) as usize; // UV is interleaved
|
||||||
|
|
||||||
|
let y_data = &data[0..y_data_size];
|
||||||
|
let uv_data = &data[y_data_size..y_data_size + uv_data_size];
|
||||||
|
|
||||||
|
queue.write_texture(
|
||||||
|
wgpu::TexelCopyTextureInfo {
|
||||||
|
texture: y,
|
||||||
|
mip_level: 0,
|
||||||
|
origin: wgpu::Origin3d::ZERO,
|
||||||
|
aspect: wgpu::TextureAspect::All,
|
||||||
|
},
|
||||||
|
y_data,
|
||||||
|
wgpu::TexelCopyBufferLayout {
|
||||||
|
offset: 0,
|
||||||
|
bytes_per_row: Some(y_size.width * 2),
|
||||||
|
rows_per_image: None,
|
||||||
|
},
|
||||||
|
y_size,
|
||||||
|
);
|
||||||
|
|
||||||
|
queue.write_texture(
|
||||||
|
wgpu::TexelCopyTextureInfo {
|
||||||
|
texture: uv,
|
||||||
|
mip_level: 0,
|
||||||
|
origin: wgpu::Origin3d::ZERO,
|
||||||
|
aspect: wgpu::TextureAspect::All,
|
||||||
|
},
|
||||||
|
uv_data,
|
||||||
|
wgpu::TexelCopyBufferLayout {
|
||||||
|
offset: 0,
|
||||||
|
bytes_per_row: Some(uv_size.width * 4),
|
||||||
|
rows_per_image: None,
|
||||||
|
},
|
||||||
|
uv_size,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_conversion_matrix(&self, matrix: &ConversionMatrix, queue: &wgpu::Queue) {
|
||||||
|
queue.write_buffer(
|
||||||
|
&self.conversion_matrix_buffer,
|
||||||
|
0,
|
||||||
|
bytemuck::bytes_of(matrix),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn conversion_matrix_buffer(&self) -> &wgpu::Buffer {
|
||||||
|
&self.conversion_matrix_buffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct VideoFrameData {
|
||||||
|
id: id::Id,
|
||||||
|
texture: VideoTexture,
|
||||||
|
bind_group: wgpu::BindGroup,
|
||||||
|
conversion_matrix: ConversionMatrix,
|
||||||
|
ready: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoFrameData {
|
||||||
|
pub fn is_hdr(&self) -> bool {
|
||||||
|
self.texture.format().is_wide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct VideoPipeline {
|
||||||
|
pipeline: wgpu::RenderPipeline,
|
||||||
|
bind_group_layout: wgpu::BindGroupLayout,
|
||||||
|
sampler: wgpu::Sampler,
|
||||||
|
format: wgpu::TextureFormat,
|
||||||
|
videos: BTreeMap<id::Id, VideoFrameData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait WideTextureFormatExt {
|
||||||
|
fn is_wide(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WideTextureFormatExt for wgpu::TextureFormat {
|
||||||
|
fn is_wide(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
wgpu::TextureFormat::Rgba16Float
|
||||||
|
| wgpu::TextureFormat::Rgba32Float
|
||||||
|
| wgpu::TextureFormat::Rgb10a2Unorm
|
||||||
|
| wgpu::TextureFormat::Rgb10a2Uint
|
||||||
|
| wgpu::TextureFormat::P010
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pipeline for VideoPipeline {
|
||||||
|
fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
if format.is_wide() {
|
||||||
|
tracing::info!("HDR texture format detected: {:?}", format);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
label: Some("iced-video-texture-bind-group-layout"),
|
||||||
|
entries: &[
|
||||||
|
// y
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Texture {
|
||||||
|
multisampled: false,
|
||||||
|
view_dimension: wgpu::TextureViewDimension::D2,
|
||||||
|
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
// uv
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 1,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Texture {
|
||||||
|
multisampled: false,
|
||||||
|
view_dimension: wgpu::TextureViewDimension::D2,
|
||||||
|
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
// sampler
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 2,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
// conversion matrix
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 3,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let shader_passthrough =
|
||||||
|
device.create_shader_module(wgpu::include_wgsl!("shaders/passthrough.wgsl"));
|
||||||
|
let render_pipeline_layout =
|
||||||
|
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("iced-video-render-pipeline-layout"),
|
||||||
|
bind_group_layouts: &[&bind_group_layout],
|
||||||
|
push_constant_ranges: &[],
|
||||||
|
});
|
||||||
|
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("iced-video-render-pipeline"),
|
||||||
|
layout: Some(&render_pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader_passthrough,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[],
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader_passthrough,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format,
|
||||||
|
blend: Some(wgpu::BlendState::REPLACE),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState::default(),
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState::default(),
|
||||||
|
multiview: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
|
label: Some("iced-video-sampler"),
|
||||||
|
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||||
|
mag_filter: wgpu::FilterMode::Linear,
|
||||||
|
min_filter: wgpu::FilterMode::Linear,
|
||||||
|
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
pipeline,
|
||||||
|
bind_group_layout,
|
||||||
|
sampler,
|
||||||
|
format,
|
||||||
|
videos: BTreeMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
crates/iced-video/src/shaders/passthrough.wgsl
Normal file
30
crates/iced-video/src/shaders/passthrough.wgsl
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) tex_coords: vec2<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(
|
||||||
|
@builtin(vertex_index) in_vertex_index: u32,
|
||||||
|
) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
let uv = vec2<f32>(f32((in_vertex_index << 1u) & 2u), f32(in_vertex_index & 2u));
|
||||||
|
out.clip_position = vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
|
||||||
|
out.clip_position.y = -out.clip_position.y;
|
||||||
|
out.tex_coords = uv;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@group(0) @binding(0) var y_texture: texture_2d<f32>;
|
||||||
|
@group(0) @binding(1) var uv_texture: texture_2d<f32>;
|
||||||
|
@group(0) @binding(2) var texture_sampler: sampler;
|
||||||
|
@group(0) @binding(3) var<uniform> rgb_primaries: mat3x3<f32>;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let y = textureSample(y_texture, texture_sampler, input.tex_coords).r;
|
||||||
|
let uv = textureSample(uv_texture, texture_sampler, input.tex_coords).rg;
|
||||||
|
let yuv = vec3f(y, uv.x - 0.5, uv.y - 0.5);
|
||||||
|
return vec4f(yuv * rgb_primaries, 1.0);
|
||||||
|
}
|
||||||
173
crates/iced-video/src/source.rs
Normal file
173
crates/iced-video/src/source.rs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
use crate::{Error, Result, ResultExt};
|
||||||
|
use gst::{
|
||||||
|
Bus, Gst, MessageType, MessageView, Sink, Source,
|
||||||
|
app::AppSink,
|
||||||
|
caps::{Caps, CapsType},
|
||||||
|
element::ElementExt,
|
||||||
|
pipeline::PipelineExt,
|
||||||
|
playback::{PlayFlags, Playbin3},
|
||||||
|
videoconvertscale::VideoConvert,
|
||||||
|
};
|
||||||
|
use std::sync::{Arc, Mutex, atomic::AtomicBool};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct VideoSource {
|
||||||
|
pub(crate) playbin: Playbin3,
|
||||||
|
pub(crate) appsink: AppSink,
|
||||||
|
pub(crate) bus: Bus,
|
||||||
|
pub(crate) ready: Arc<AtomicBool>,
|
||||||
|
pub(crate) frame: Arc<Mutex<gst::Sample>>,
|
||||||
|
pub(crate) size: std::sync::OnceLock<(i32, i32)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoSource {
|
||||||
|
/// Creates a new video source from the given URL.
|
||||||
|
/// Since this doesn't have to parse the pipeline manually, we aren't sanitizing the URL for
|
||||||
|
/// now.
|
||||||
|
pub fn new(url: impl AsRef<str>) -> Result<Self> {
|
||||||
|
Gst::new();
|
||||||
|
let mut appsink = AppSink::new("iced-video-sink").change_context(Error)?;
|
||||||
|
appsink
|
||||||
|
.drop(true)
|
||||||
|
.sync(true)
|
||||||
|
// .async_(true)
|
||||||
|
.emit_signals(true);
|
||||||
|
let playbin = Playbin3::new("iced-video")
|
||||||
|
.change_context(Error)?
|
||||||
|
.with_uri(url.as_ref())
|
||||||
|
.with_buffer_duration(core::time::Duration::from_secs(2))
|
||||||
|
.with_buffer_size(4096 * 4096 * 4 * 3)
|
||||||
|
.with_ring_buffer_max_size(4096 * 4096 * 4 * 3)
|
||||||
|
.with_flags(Playbin3::default_flags() | PlayFlags::DOWNLOAD)
|
||||||
|
.with_video_sink(&appsink);
|
||||||
|
let bus = playbin.bus().change_context(Error)?;
|
||||||
|
playbin.pause().change_context(Error)?;
|
||||||
|
let ready = Arc::new(AtomicBool::new(false));
|
||||||
|
let frame = Arc::new(Mutex::new(gst::Sample::new()));
|
||||||
|
|
||||||
|
appsink.on_new_sample({
|
||||||
|
let ready = Arc::clone(&ready);
|
||||||
|
let frame = Arc::clone(&frame);
|
||||||
|
move |appsink| {
|
||||||
|
let Ok(sample) = appsink.pull_sample() else {
|
||||||
|
tracing::error!("Failed to pull video sample from appsink despite being notified of new frame");
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
{
|
||||||
|
let mut guard = frame.lock().expect("BUG: Mutex poisoned");
|
||||||
|
core::mem::replace(&mut *guard, sample);
|
||||||
|
ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
playbin,
|
||||||
|
appsink,
|
||||||
|
bus,
|
||||||
|
ready,
|
||||||
|
frame,
|
||||||
|
size: std::sync::OnceLock::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wait(&self) -> Result<()> {
|
||||||
|
use futures_lite::StreamExt;
|
||||||
|
// self.bus_stream()
|
||||||
|
// .for_each(|msg: gst::Message| {
|
||||||
|
// use gst::gstreamer::prelude::*;
|
||||||
|
// match msg.view() {
|
||||||
|
// MessageView::Eos(_) => {
|
||||||
|
// tracing::info!("Video reached end of stream");
|
||||||
|
// }
|
||||||
|
// MessageView::Error(err) => {
|
||||||
|
// tracing::error!(
|
||||||
|
// "Video Error from {:?}: {} ({:?})",
|
||||||
|
// err.src().map(|s| s.path_string()),
|
||||||
|
// err.error(),
|
||||||
|
// err.debug()
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// view => tracing::info!("Video Message: {:#?}", view),
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .await;
|
||||||
|
self.playbin
|
||||||
|
.wait_for_states(&[gst::State::Paused, gst::State::Playing])
|
||||||
|
.await
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to wait for video initialisation")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(&self) -> Result<gst::VideoFormat> {
|
||||||
|
let caps = self
|
||||||
|
.appsink
|
||||||
|
.sink("sink")
|
||||||
|
.current_caps()
|
||||||
|
.change_context(Error)?;
|
||||||
|
let format = caps
|
||||||
|
.format()
|
||||||
|
.ok_or(Error)
|
||||||
|
.attach("Failed to get video caps structure")?;
|
||||||
|
Ok(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bus_stream(&self) -> impl futures_lite::Stream<Item = gst::Message> {
|
||||||
|
self.bus.stream()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_playing(&self) -> Result<bool> {
|
||||||
|
let state = self.playbin.state(None).change_context(Error)?;
|
||||||
|
Ok(state == gst::State::Playing)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle(&self) -> Result<()> {
|
||||||
|
if self.is_playing()? {
|
||||||
|
self.pause()?;
|
||||||
|
} else {
|
||||||
|
self.play()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play(&self) -> Result<()> {
|
||||||
|
self.playbin
|
||||||
|
.play()
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to play video")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pause(&self) -> Result<()> {
|
||||||
|
self.playbin
|
||||||
|
.pause()
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to pause video")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&self) -> Result<()> {
|
||||||
|
self.playbin
|
||||||
|
.stop()
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to stop video")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> Result<(i32, i32)> {
|
||||||
|
if let Some(size) = self.size.get() {
|
||||||
|
return Ok(*size);
|
||||||
|
}
|
||||||
|
let caps = self
|
||||||
|
.appsink
|
||||||
|
.sink("sink")
|
||||||
|
.current_caps()
|
||||||
|
.change_context(Error)?;
|
||||||
|
let out = caps
|
||||||
|
.width()
|
||||||
|
.and_then(|width| caps.height().map(|height| (width, height)))
|
||||||
|
.ok_or(Error)
|
||||||
|
.attach("Failed to get width, height")?;
|
||||||
|
self.size.set(out);
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
258
crates/iced-video/src/widget.rs
Normal file
258
crates/iced-video/src/widget.rs
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
use super::*;
|
||||||
|
use iced::Length;
|
||||||
|
use iced_core as iced;
|
||||||
|
use iced_wgpu::primitive::Renderer as PrimitiveRenderer;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
/// This is the Video widget that displays a video.
|
||||||
|
/// This should be used in the view function.
|
||||||
|
pub struct Video<'a, Message, Theme = iced::Theme, Renderer = iced_wgpu::Renderer>
|
||||||
|
where
|
||||||
|
Renderer: PrimitiveRenderer,
|
||||||
|
{
|
||||||
|
id: id::Id,
|
||||||
|
handle: &'a VideoHandle<Message, Ready>,
|
||||||
|
video_format: gst::VideoFormat,
|
||||||
|
content_fit: iced::ContentFit,
|
||||||
|
width: iced::Length,
|
||||||
|
height: iced::Length,
|
||||||
|
looping: bool,
|
||||||
|
__marker: PhantomData<(Renderer, Theme)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Video<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: PrimitiveRenderer,
|
||||||
|
Message: Clone + Send + Sync,
|
||||||
|
{
|
||||||
|
pub fn new(handle: &'a VideoHandle<Message, Ready>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: handle.id.clone(),
|
||||||
|
handle: &handle,
|
||||||
|
video_format: handle
|
||||||
|
.format()
|
||||||
|
.expect("Failed to get video format during widget creation"),
|
||||||
|
content_fit: iced::ContentFit::Contain,
|
||||||
|
width: Length::Shrink,
|
||||||
|
height: Length::Shrink,
|
||||||
|
looping: false,
|
||||||
|
__marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> Video<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: PrimitiveRenderer,
|
||||||
|
{
|
||||||
|
pub fn width(mut self, width: Length) -> Self {
|
||||||
|
self.width = width;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(mut self, height: Length) -> Self {
|
||||||
|
self.height = height;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content_fit(mut self, fit: iced::ContentFit) -> Self {
|
||||||
|
self.content_fit = fit;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn on_end_of_stream(mut self, message: Message) -> Self {
|
||||||
|
// self.on_end_of_stream = Some(message);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// pub fn on_new_frame(mut self, message: Message) -> Self {
|
||||||
|
// self.on_new_frame = Some(message);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub fn looping(mut self, looping: bool) -> Self {
|
||||||
|
self.looping = looping;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> iced::Widget<Message, Theme, Renderer>
|
||||||
|
for Video<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: Clone + Send + Sync,
|
||||||
|
Renderer: PrimitiveRenderer,
|
||||||
|
{
|
||||||
|
fn size(&self) -> iced::Size<Length> {
|
||||||
|
iced::Size {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The video player should take max space by default
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut iced::widget::Tree,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
limits: &iced::layout::Limits,
|
||||||
|
) -> iced::layout::Node {
|
||||||
|
iced::layout::Node::new(limits.max())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &iced::widget::Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &iced::renderer::Style,
|
||||||
|
layout: iced::Layout<'_>,
|
||||||
|
cursor: iced::mouse::Cursor,
|
||||||
|
viewport: &iced::Rectangle,
|
||||||
|
) {
|
||||||
|
if let Ok((width, height)) = self.handle.source.size() {
|
||||||
|
let video_size = iced::Size {
|
||||||
|
width: width as f32,
|
||||||
|
height: height as f32,
|
||||||
|
};
|
||||||
|
let bounds = layout.bounds();
|
||||||
|
let adjusted_fit = self.content_fit.fit(video_size, bounds.size());
|
||||||
|
let scale = iced::Vector::new(
|
||||||
|
adjusted_fit.width / video_size.width,
|
||||||
|
adjusted_fit.height / video_size.height,
|
||||||
|
);
|
||||||
|
let final_size = video_size * scale;
|
||||||
|
let position = match self.content_fit {
|
||||||
|
iced::ContentFit::None => iced::Point::new(
|
||||||
|
bounds.x + (video_size.width - adjusted_fit.width) / 2.0,
|
||||||
|
bounds.y + (video_size.height - adjusted_fit.height) / 2.0,
|
||||||
|
),
|
||||||
|
_ => iced::Point::new(
|
||||||
|
bounds.center_x() - final_size.width / 2.0,
|
||||||
|
bounds.center_y() - final_size.height / 2.0,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let drawing_bounds = iced::Rectangle::new(position, final_size);
|
||||||
|
|
||||||
|
let render = |renderer: &mut Renderer| {
|
||||||
|
renderer.draw_primitive(
|
||||||
|
drawing_bounds,
|
||||||
|
primitive::VideoFrame {
|
||||||
|
id: self.id.clone(),
|
||||||
|
size: iced_wgpu::wgpu::Extent3d {
|
||||||
|
width: width as u32,
|
||||||
|
height: height as u32,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
ready: Arc::clone(&self.handle.frame_ready),
|
||||||
|
frame: Arc::clone(&self.handle.source.frame),
|
||||||
|
format: self.video_format,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height {
|
||||||
|
renderer.with_layer(bounds, render);
|
||||||
|
} else {
|
||||||
|
render(renderer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
_tree: &mut iced_core::widget::Tree,
|
||||||
|
event: &iced::Event,
|
||||||
|
_layout: iced_core::Layout<'_>,
|
||||||
|
_cursor: iced_core::mouse::Cursor,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
_clipboard: &mut dyn iced_core::Clipboard,
|
||||||
|
shell: &mut iced_core::Shell<'_, Message>,
|
||||||
|
_viewport: &iced::Rectangle,
|
||||||
|
) {
|
||||||
|
if let iced::Event::Window(iced::window::Event::RedrawRequested(when)) = event {
|
||||||
|
if self
|
||||||
|
.handle
|
||||||
|
.frame_ready
|
||||||
|
.load(std::sync::atomic::Ordering::SeqCst)
|
||||||
|
{
|
||||||
|
shell.request_redraw();
|
||||||
|
} else {
|
||||||
|
shell.request_redraw_at(iced::window::RedrawRequest::At(
|
||||||
|
iced_core::time::Instant::now() + core::time::Duration::from_millis(16)
|
||||||
|
- when.elapsed(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<Video<'a, Message, Theme, Renderer>>
|
||||||
|
for iced::Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: Send + Sync + 'a + Clone,
|
||||||
|
Theme: 'a,
|
||||||
|
Renderer: 'a + iced_wgpu::primitive::Renderer,
|
||||||
|
{
|
||||||
|
fn from(video: Video<'a, Message, Theme, Renderer>) -> Self {
|
||||||
|
Self::new(video)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct VideoSubscription<Message> {
|
||||||
|
pub(crate) id: id::Id,
|
||||||
|
pub(crate) on_end_of_stream: Option<Box<Message>>,
|
||||||
|
pub(crate) on_new_frame: Option<Box<Message>>,
|
||||||
|
pub(crate) on_about_to_finish: Option<Box<Message>>,
|
||||||
|
// on_subtitle_text: Option<Box<dyn Fn(Option<String>) -> Message>>,
|
||||||
|
// on_error: Option<Box<dyn Fn(&glib::Error) -> Message>>,
|
||||||
|
pub(crate) bus: gst::Bus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message> VideoSubscription<Message> where Message: Clone {}
|
||||||
|
|
||||||
|
impl<Message> iced_futures::subscription::Recipe for VideoSubscription<Message>
|
||||||
|
where
|
||||||
|
Message: Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
type Output = Message;
|
||||||
|
fn hash(&self, state: &mut iced_futures::subscription::Hasher) {
|
||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
|
self.id.hash(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stream(
|
||||||
|
self: Box<Self>,
|
||||||
|
_input: core::pin::Pin<
|
||||||
|
Box<dyn iced_futures::futures::Stream<Item = iced_futures::subscription::Event> + Send>,
|
||||||
|
>,
|
||||||
|
) -> core::pin::Pin<Box<dyn iced_futures::futures::Stream<Item = Self::Output> + Send>> {
|
||||||
|
// use iced_futures::futures::StreamExt;
|
||||||
|
use futures_lite::stream::StreamExt;
|
||||||
|
Box::pin(
|
||||||
|
self.bus
|
||||||
|
.filtered_stream(&[gst::MessageType::Eos, gst::MessageType::Element])
|
||||||
|
.filter_map({
|
||||||
|
let eos = self.on_end_of_stream.clone();
|
||||||
|
let frame = self.on_new_frame.clone();
|
||||||
|
move |message: gst::Message| match message.view() {
|
||||||
|
gst::MessageView::Eos(_) => eos.clone().map(|m| *m),
|
||||||
|
gst::MessageView::Element(element_msg) => {
|
||||||
|
let structure = element_msg.structure();
|
||||||
|
if let Some(structure) = structure {
|
||||||
|
if structure.name() == "GstVideoFrameReady" {
|
||||||
|
frame.clone().map(|m| *m)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
10
deny.toml
10
deny.toml
@@ -92,7 +92,15 @@ allow = [
|
|||||||
"MIT",
|
"MIT",
|
||||||
"Apache-2.0",
|
"Apache-2.0",
|
||||||
"Unicode-3.0",
|
"Unicode-3.0",
|
||||||
#"Apache-2.0 WITH LLVM-exception",
|
"BSD-2-Clause",
|
||||||
|
"BSD-3-Clause",
|
||||||
|
"Apache-2.0 WITH LLVM-exception",
|
||||||
|
"Zlib",
|
||||||
|
"ISC",
|
||||||
|
"NCSA",
|
||||||
|
"CC0-1.0",
|
||||||
|
"BSL-1.0",
|
||||||
|
# "LGPL",
|
||||||
]
|
]
|
||||||
# The confidence threshold for detecting a license from license text.
|
# The confidence threshold for detecting a license from license text.
|
||||||
# The higher the value, the more closely the license text must be to the
|
# The higher the value, the more closely the license text must be to the
|
||||||
|
|||||||
2
examples/hdr-gstreamer-wgpu/.gitignore
vendored
Normal file
2
examples/hdr-gstreamer-wgpu/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
perf*
|
||||||
|
target/
|
||||||
20
examples/hdr-gstreamer-wgpu/Cargo.toml
Normal file
20
examples/hdr-gstreamer-wgpu/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "hdr-gstreamer-wgpu"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# gst = { workspace = true }
|
||||||
|
wgpu = "27"
|
||||||
|
gstreamer = { version = "0.24.4", features = ["v1_26"] }
|
||||||
|
gstreamer-app = { version = "0.24.4", features = ["v1_26"] }
|
||||||
|
gstreamer-base = { version = "0.24.4", features = ["v1_26"] }
|
||||||
|
gstreamer-video = { version = "0.24.4", features = ["v1_26"] }
|
||||||
|
winit = { version = "*", features = ["wayland"] }
|
||||||
|
anyhow = "*"
|
||||||
|
pollster = "0.4.0"
|
||||||
|
tracing = { version = "0.1.43", features = ["log"] }
|
||||||
|
tracing-subscriber = "0.3.22"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
debug = true
|
||||||
0
examples/hdr-gstreamer-wgpu/src/cli.rs
Normal file
0
examples/hdr-gstreamer-wgpu/src/cli.rs
Normal file
592
examples/hdr-gstreamer-wgpu/src/main.rs
Normal file
592
examples/hdr-gstreamer-wgpu/src/main.rs
Normal file
@@ -0,0 +1,592 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use gstreamer as gst;
|
||||||
|
use gstreamer_app as gst_app;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use winit::{
|
||||||
|
application::ApplicationHandler,
|
||||||
|
event::*,
|
||||||
|
event_loop::{ActiveEventLoop, EventLoop},
|
||||||
|
keyboard::*,
|
||||||
|
window::Window,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
state: Option<State>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { state: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HdrTextureFormatExt {
|
||||||
|
fn is_hdr_format(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HdrTextureFormatExt for wgpu::TextureFormat {
|
||||||
|
fn is_hdr_format(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
wgpu::TextureFormat::Rgba16Float
|
||||||
|
| wgpu::TextureFormat::Rgba32Float
|
||||||
|
| wgpu::TextureFormat::Rgb10a2Unorm
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct State {
|
||||||
|
window: Arc<Window>,
|
||||||
|
gst: Video,
|
||||||
|
surface: wgpu::Surface<'static>,
|
||||||
|
video_texture: wgpu::Texture,
|
||||||
|
device: wgpu::Device,
|
||||||
|
queue: wgpu::Queue,
|
||||||
|
config: wgpu::SurfaceConfiguration,
|
||||||
|
pipeline: wgpu::RenderPipeline,
|
||||||
|
bind_group: wgpu::BindGroup,
|
||||||
|
is_surface_initialized: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
async fn new(window: Arc<Window>) -> Result<State> {
|
||||||
|
let instance = wgpu::Instance::default();
|
||||||
|
let surface = instance
|
||||||
|
.create_surface(window.clone())
|
||||||
|
.context("Failed to create wgpu surface")?;
|
||||||
|
|
||||||
|
let adapter = instance
|
||||||
|
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
|
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||||
|
compatible_surface: Some(&surface),
|
||||||
|
force_fallback_adapter: false,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("Failed to request wgpu adapter")?;
|
||||||
|
|
||||||
|
let (device, queue) = adapter
|
||||||
|
.request_device(&wgpu::DeviceDescriptor {
|
||||||
|
label: None,
|
||||||
|
required_features: wgpu::Features::empty(),
|
||||||
|
required_limits: wgpu::Limits::default(),
|
||||||
|
memory_hints: wgpu::MemoryHints::default(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("Failed to request wgpu device")?;
|
||||||
|
let surface_caps = surface.get_capabilities(&adapter);
|
||||||
|
tracing::info!("Caps: {:#?}", &surface_caps);
|
||||||
|
let surface_format = surface_caps
|
||||||
|
.formats
|
||||||
|
.iter()
|
||||||
|
.rev() // float one comes first
|
||||||
|
.find(|f| f.is_hdr_format())
|
||||||
|
.expect("HDR format not supported")
|
||||||
|
.clone();
|
||||||
|
tracing::info!("Using surface format: {:?}", surface_format);
|
||||||
|
let size = window.inner_size();
|
||||||
|
let config = wgpu::SurfaceConfiguration {
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
format: surface_format,
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
present_mode: surface_caps.present_modes[0],
|
||||||
|
alpha_mode: surface_caps.alpha_modes[0],
|
||||||
|
view_formats: vec![],
|
||||||
|
desired_maximum_frame_latency: 2, // calculate upto 5 frames ahead
|
||||||
|
};
|
||||||
|
surface.configure(&device, &config);
|
||||||
|
|
||||||
|
let shader = device.create_shader_module(wgpu::include_wgsl!("shader.wgsl"));
|
||||||
|
|
||||||
|
let texture_bind_group_layout =
|
||||||
|
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
label: Some("texture_bind_group_layout"),
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Texture {
|
||||||
|
multisampled: false,
|
||||||
|
view_dimension: wgpu::TextureViewDimension::D2,
|
||||||
|
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 1,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let render_pipeline_layout =
|
||||||
|
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("Jello Render Pipeline Layout"),
|
||||||
|
bind_group_layouts: &[&texture_bind_group_layout],
|
||||||
|
push_constant_ranges: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("Jello Render Pipeline"),
|
||||||
|
layout: Some(&render_pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[],
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: surface_format,
|
||||||
|
blend: Some(wgpu::BlendState::REPLACE),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState::default(),
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: 1,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
|
multiview: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let texture_size = wgpu::Extent3d {
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
};
|
||||||
|
let video_texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
size: texture_size,
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: surface_format,
|
||||||
|
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||||
|
label: Some("Jello Video Texture"),
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Use a better sampler
|
||||||
|
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
|
label: Some("texture_sampler"),
|
||||||
|
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||||
|
mag_filter: wgpu::FilterMode::Linear,
|
||||||
|
min_filter: wgpu::FilterMode::Linear,
|
||||||
|
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
layout: &texture_bind_group_layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::TextureView(
|
||||||
|
&video_texture.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
label: Some("Jello Texture Bind Group"),
|
||||||
|
});
|
||||||
|
let gst = Video::new().context("Failed to create Video")?;
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(10));
|
||||||
|
// surface.configure(&device, &config);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
window,
|
||||||
|
gst,
|
||||||
|
surface,
|
||||||
|
video_texture,
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
config,
|
||||||
|
is_surface_initialized: true,
|
||||||
|
bind_group,
|
||||||
|
pipeline: render_pipeline,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// async fn next_frame(&mut self)
|
||||||
|
|
||||||
|
fn resize(&mut self, width: u32, height: u32) {
|
||||||
|
if width > 0 && height > 0 {
|
||||||
|
self.config.width = width;
|
||||||
|
self.config.height = height;
|
||||||
|
self.surface.configure(&self.device, &self.config);
|
||||||
|
self.is_surface_initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
|
||||||
|
if !self.is_surface_initialized {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.gst.poll();
|
||||||
|
|
||||||
|
self.copy_next_frame_to_texture()
|
||||||
|
.inspect_err(|e| {
|
||||||
|
tracing::error!("Failed to copy video frame to texture: {e:?}");
|
||||||
|
})
|
||||||
|
.map_err(|_| wgpu::SurfaceError::Lost)?;
|
||||||
|
|
||||||
|
let output = match self.surface.get_current_texture() {
|
||||||
|
Ok(output) => output,
|
||||||
|
Err(wgpu::SurfaceError::Lost) => {
|
||||||
|
self.surface.configure(&self.device, &self.config);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
let view = output
|
||||||
|
.texture
|
||||||
|
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
let mut encoder = self
|
||||||
|
.device
|
||||||
|
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("Jello Render Encoder"),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: Some("Jello Render Pass"),
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: &view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||||
|
r: 0.1,
|
||||||
|
g: 0.2,
|
||||||
|
b: 0.3,
|
||||||
|
a: 1.0,
|
||||||
|
}),
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
depth_slice: None,
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
occlusion_query_set: None,
|
||||||
|
timestamp_writes: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
render_pass.set_pipeline(&self.pipeline);
|
||||||
|
render_pass.set_bind_group(0, &self.bind_group, &[]);
|
||||||
|
render_pass.draw(0..3, 0..1);
|
||||||
|
drop(render_pass);
|
||||||
|
|
||||||
|
self.queue.submit(std::iter::once(encoder.finish()));
|
||||||
|
output.present();
|
||||||
|
self.window.request_redraw();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_next_frame_to_texture(&mut self) -> Result<()> {
|
||||||
|
let frame = self
|
||||||
|
.gst
|
||||||
|
.appsink
|
||||||
|
.try_pull_sample(gst::ClockTime::NONE)
|
||||||
|
.context("Failed to pull sample from appsink")?;
|
||||||
|
|
||||||
|
let caps = frame.caps().context("Failed to get caps from sample")?;
|
||||||
|
let size = caps
|
||||||
|
.structure(0)
|
||||||
|
.context("Failed to get structure from caps")?;
|
||||||
|
let width = size
|
||||||
|
.get::<i32>("width")
|
||||||
|
.context("Failed to get width from caps")? as u32;
|
||||||
|
let height = size
|
||||||
|
.get::<i32>("height")
|
||||||
|
.context("Failed to get height from caps")? as u32;
|
||||||
|
|
||||||
|
let texture_size = self.video_texture.size();
|
||||||
|
if texture_size.width != width || texture_size.height != height {
|
||||||
|
tracing::info!(
|
||||||
|
"Resizing video texture from {}x{} to {}x{}",
|
||||||
|
texture_size.width,
|
||||||
|
texture_size.height,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
);
|
||||||
|
self.video_texture = self.device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: width as u32,
|
||||||
|
height: height as u32,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: self.config.format,
|
||||||
|
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||||
|
label: Some("Jello Video Texture"),
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
let texture_bind_group_layout =
|
||||||
|
self.device
|
||||||
|
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
label: Some("texture_bind_group_layout"),
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Texture {
|
||||||
|
multisampled: false,
|
||||||
|
view_dimension: wgpu::TextureViewDimension::D2,
|
||||||
|
sample_type: wgpu::TextureSampleType::Float {
|
||||||
|
filterable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 1,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
|
label: Some("texture_sampler"),
|
||||||
|
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||||
|
mag_filter: wgpu::FilterMode::Linear,
|
||||||
|
min_filter: wgpu::FilterMode::Linear,
|
||||||
|
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
self.bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
layout: &texture_bind_group_layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::TextureView(
|
||||||
|
&self
|
||||||
|
.video_texture
|
||||||
|
.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
label: Some("Jello Texture Bind Group"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let texture = &self.video_texture;
|
||||||
|
|
||||||
|
let buffer = frame.buffer().context("Failed to get buffer from sample")?;
|
||||||
|
let map = buffer
|
||||||
|
.map_readable()
|
||||||
|
.context("Failed to map buffer readable")?;
|
||||||
|
self.queue.write_texture(
|
||||||
|
wgpu::TexelCopyTextureInfo {
|
||||||
|
texture: &texture,
|
||||||
|
mip_level: 0,
|
||||||
|
origin: wgpu::Origin3d::ZERO,
|
||||||
|
aspect: wgpu::TextureAspect::All,
|
||||||
|
},
|
||||||
|
&map,
|
||||||
|
wgpu::TexelCopyBufferLayout {
|
||||||
|
offset: 0,
|
||||||
|
bytes_per_row: Some(4 * width as u32),
|
||||||
|
rows_per_image: Some(height as u32),
|
||||||
|
},
|
||||||
|
texture.size(),
|
||||||
|
);
|
||||||
|
// drop(map);
|
||||||
|
// drop(frame);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationHandler<State> for App {
|
||||||
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut window_attributes = Window::default_attributes();
|
||||||
|
|
||||||
|
let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
|
||||||
|
|
||||||
|
// let monitor = event_loop
|
||||||
|
// .primary_monitor()
|
||||||
|
// .or_else(|| window.current_monitor());
|
||||||
|
// window.set_fullscreen(None);
|
||||||
|
// window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(monitor)));
|
||||||
|
self.state = Some(pollster::block_on(State::new(window)).expect("Failed to block"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: State) {
|
||||||
|
self.state = Some(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
||||||
|
let state = match &mut self.state {
|
||||||
|
Some(state) => state,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
state.window.request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(
|
||||||
|
&mut self,
|
||||||
|
event_loop: &ActiveEventLoop,
|
||||||
|
_window_id: winit::window::WindowId,
|
||||||
|
event: WindowEvent,
|
||||||
|
) {
|
||||||
|
let state = match &mut self.state {
|
||||||
|
Some(canvas) => canvas,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
match event {
|
||||||
|
WindowEvent::CloseRequested => event_loop.exit(),
|
||||||
|
WindowEvent::Resized(size) => {
|
||||||
|
tracing::info!("Window resized to {size:?}");
|
||||||
|
state.resize(size.width, size.height)
|
||||||
|
}
|
||||||
|
WindowEvent::RedrawRequested => {
|
||||||
|
// if state.gst.poll() {
|
||||||
|
// event_loop.exit();
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
match state.render() {
|
||||||
|
Ok(_) => {}
|
||||||
|
// Reconfigure the surface if lost
|
||||||
|
Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
|
||||||
|
let size = state.window.inner_size();
|
||||||
|
tracing::info!("Reconfiguring surface to {size:?}");
|
||||||
|
state.resize(size.width, size.height);
|
||||||
|
}
|
||||||
|
// The system is out of memory, we should probably quit
|
||||||
|
Err(wgpu::SurfaceError::OutOfMemory) => event_loop.exit(),
|
||||||
|
// All other errors (Outdated, Timeout) should be resolved by the next frame
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to render frame: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// WindowEvent::AboutToWait => {
|
||||||
|
// state.window.request_redraw();
|
||||||
|
// }
|
||||||
|
WindowEvent::KeyboardInput {
|
||||||
|
event:
|
||||||
|
KeyEvent {
|
||||||
|
physical_key: PhysicalKey::Code(code),
|
||||||
|
state,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
} => match (code, state.is_pressed()) {
|
||||||
|
(KeyCode::Escape, true) => event_loop.exit(),
|
||||||
|
(KeyCode::KeyQ, true) => event_loop.exit(),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() -> anyhow::Result<()> {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let event_loop = EventLoop::with_user_event().build()?;
|
||||||
|
let mut app = App::new();
|
||||||
|
event_loop.run_app(&mut app)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Video {
|
||||||
|
pipeline: gst::Pipeline,
|
||||||
|
bus: gst::Bus,
|
||||||
|
appsink: gst_app::AppSink,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Video {
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
gst::init()?;
|
||||||
|
use gst::prelude::*;
|
||||||
|
let pipeline = gst::parse::launch(
|
||||||
|
r##"playbin3 uri=https://jellyfin.tsuba.darksailor.dev/Items/6010382cf25273e624d305907010d773/Download?api_key=036c140222464878862231ef66a2bc9c video-sink="videoconvert ! video/x-raw,format=RGB10A2_LE ! appsink sync=true drop=true name=appsink""##
|
||||||
|
).context("Failed to parse gst pipeline")?;
|
||||||
|
let pipeline = pipeline
|
||||||
|
.downcast::<gst::Pipeline>()
|
||||||
|
.map_err(|_| anyhow::anyhow!("Failed to downcast gst element to Pipeline"))?;
|
||||||
|
|
||||||
|
let video_sink = pipeline.property::<gst::Bin>("video-sink");
|
||||||
|
let appsink = video_sink
|
||||||
|
.by_name("appsink")
|
||||||
|
.context("Failed to get appsink from video-sink")?
|
||||||
|
.downcast::<gst_app::AppSink>()
|
||||||
|
.map_err(|_| {
|
||||||
|
anyhow::anyhow!("Failed to downcast video-sink appsink to gst_app::AppSink")
|
||||||
|
})?;
|
||||||
|
// appsink.set_property("max-buffers", 2u32);
|
||||||
|
// appsink.set_property("emit-signals", true);
|
||||||
|
// appsink.set_callbacks(
|
||||||
|
// gst_app::AppSinkCallbacks::builder()
|
||||||
|
// .new_sample(|_appsink| Ok(gst::FlowSuccess::Ok))
|
||||||
|
// .build(),
|
||||||
|
// );
|
||||||
|
|
||||||
|
let bus = pipeline.bus().context("Failed to get gst pipeline bus")?;
|
||||||
|
pipeline.set_state(gst::State::Playing)?;
|
||||||
|
pipeline
|
||||||
|
.state(gst::ClockTime::from_seconds(5))
|
||||||
|
.0
|
||||||
|
.context("Failed to wait for pipeline")?;
|
||||||
|
Ok(Self {
|
||||||
|
pipeline,
|
||||||
|
bus,
|
||||||
|
appsink,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn poll(&mut self) -> bool {
|
||||||
|
use gst::prelude::*;
|
||||||
|
for msg in self.bus.iter_timed(gst::ClockTime::ZERO) {
|
||||||
|
use gst::MessageView;
|
||||||
|
|
||||||
|
match msg.view() {
|
||||||
|
MessageView::Eos(..) => {
|
||||||
|
tracing::info!("End of stream");
|
||||||
|
self.pipeline.set_state(gst::State::Null).ok();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MessageView::Error(err) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Error from {:?}: {} ({:?})",
|
||||||
|
err.src().map(|s| s.path_string()),
|
||||||
|
err.error(),
|
||||||
|
err.debug()
|
||||||
|
);
|
||||||
|
self.pipeline.set_state(gst::State::Null).ok();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
31
examples/hdr-gstreamer-wgpu/src/shader.wgsl
Normal file
31
examples/hdr-gstreamer-wgpu/src/shader.wgsl
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Vertex shader
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) tex_coords: vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(
|
||||||
|
@builtin(vertex_index) in_vertex_index: u32,
|
||||||
|
) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
let uv = vec2<f32>(f32((in_vertex_index << 1u) & 2u), f32(in_vertex_index & 2u));
|
||||||
|
out.clip_position = vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
|
||||||
|
out.clip_position.y = -out.clip_position.y;
|
||||||
|
out.tex_coords = uv;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fragment shader
|
||||||
|
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var t_diffuse: texture_2d<f32>;
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var s_diffuse: sampler;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
return textureSample(t_diffuse, s_diffuse, in.tex_coords);
|
||||||
|
}
|
||||||
|
|
||||||
35
flake.lock
generated
35
flake.lock
generated
@@ -3,11 +3,11 @@
|
|||||||
"advisory-db": {
|
"advisory-db": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763456551,
|
"lastModified": 1768679419,
|
||||||
"narHash": "sha256-z5NogiOp+1r7Fd39jVFN0kT3aXUef8sYkuBsrAUNB5g=",
|
"narHash": "sha256-l9rM4lXBeS2mIAJsJjVfl0UABx3S3zg5tul7bv+bn50=",
|
||||||
"owner": "rustsec",
|
"owner": "rustsec",
|
||||||
"repo": "advisory-db",
|
"repo": "advisory-db",
|
||||||
"rev": "6799e5dea99315eb8de85c6084fd99892b4a25d0",
|
"rev": "c700e1cd023ca87343cbd9217d50d47023e9adc7",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -18,11 +18,11 @@
|
|||||||
},
|
},
|
||||||
"crane": {
|
"crane": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1762538466,
|
"lastModified": 1768873933,
|
||||||
"narHash": "sha256-8zrIPl6J+wLm9MH5ksHcW7BUHo7jSNOu0/hA0ohOOaM=",
|
"narHash": "sha256-CfyzdaeLNGkyAHp3kT5vjvXhA1pVVK7nyDziYxCPsNk=",
|
||||||
"owner": "ipetkov",
|
"owner": "ipetkov",
|
||||||
"repo": "crane",
|
"repo": "crane",
|
||||||
"rev": "0cea393fffb39575c46b7a0318386467272182fe",
|
"rev": "0bda7e7d005ccb5522a76d11ccfbf562b71953ca",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -34,10 +34,10 @@
|
|||||||
"crates-io-index": {
|
"crates-io-index": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763363725,
|
"lastModified": 1769614137,
|
||||||
"narHash": "sha256-cxr5xIKZFP45yV1ZHFTB1sHo5YGiR3FA8D9vAfDizMo=",
|
"narHash": "sha256-3Td8fiv6iFVxeS0hYq3xdd10ZvUkC9INMAiQx/mECas=",
|
||||||
"ref": "refs/heads/master",
|
"ref": "refs/heads/master",
|
||||||
"rev": "0382002e816a4cbd17d8d5b172f08b848aa22ff6",
|
"rev": "c7e7d6394bc95555d6acd5c6783855f47d64c90d",
|
||||||
"shallow": true,
|
"shallow": true,
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/rust-lang/crates.io-index"
|
"url": "https://github.com/rust-lang/crates.io-index"
|
||||||
@@ -50,7 +50,9 @@
|
|||||||
},
|
},
|
||||||
"crates-nix": {
|
"crates-nix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"crates-io-index": "crates-io-index"
|
"crates-io-index": [
|
||||||
|
"crates-io-index"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763364255,
|
"lastModified": 1763364255,
|
||||||
@@ -106,11 +108,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763283776,
|
"lastModified": 1768564909,
|
||||||
"narHash": "sha256-Y7TDFPK4GlqrKrivOcsHG8xSGqQx3A6c+i7novT85Uk=",
|
"narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "50a96edd8d0db6cc8db57dab6bb6d6ee1f3dc49a",
|
"rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -124,6 +126,7 @@
|
|||||||
"inputs": {
|
"inputs": {
|
||||||
"advisory-db": "advisory-db",
|
"advisory-db": "advisory-db",
|
||||||
"crane": "crane",
|
"crane": "crane",
|
||||||
|
"crates-io-index": "crates-io-index",
|
||||||
"crates-nix": "crates-nix",
|
"crates-nix": "crates-nix",
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"nix-github-actions": "nix-github-actions",
|
"nix-github-actions": "nix-github-actions",
|
||||||
@@ -138,11 +141,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763433504,
|
"lastModified": 1768877311,
|
||||||
"narHash": "sha256-cVid5UNpk88sPYHkLAA5aZEHOFQXSB/2L1vl18Aq7IM=",
|
"narHash": "sha256-abSDl0cNr0B+YCsIDpO1SjXD9JMxE4s8EFnhLEFVovI=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "42ce16c6d8318a654d53f047c9400b7d902d6e61",
|
"rev": "59e4ab96304585fde3890025fd59bd2717985cc1",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
94
flake.nix
94
flake.nix
@@ -9,7 +9,14 @@
|
|||||||
url = "github:nix-community/nix-github-actions";
|
url = "github:nix-community/nix-github-actions";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
crates-nix.url = "github:uttarayan21/crates.nix";
|
crates-io-index = {
|
||||||
|
url = "git+https://github.com/rust-lang/crates.io-index?shallow=1";
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
|
crates-nix = {
|
||||||
|
url = "github:uttarayan21/crates.nix";
|
||||||
|
inputs.crates-io-index.follows = "crates-io-index";
|
||||||
|
};
|
||||||
rust-overlay = {
|
rust-overlay = {
|
||||||
url = "github:oxalica/rust-overlay";
|
url = "github:oxalica/rust-overlay";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
@@ -35,6 +42,7 @@
|
|||||||
system: let
|
system: let
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
inherit system;
|
inherit system;
|
||||||
|
config.allowUnfree = true;
|
||||||
overlays = [
|
overlays = [
|
||||||
rust-overlay.overlays.default
|
rust-overlay.overlays.default
|
||||||
];
|
];
|
||||||
@@ -56,7 +64,7 @@
|
|||||||
|
|
||||||
src = let
|
src = let
|
||||||
filterBySuffix = path: exts: lib.any (ext: lib.hasSuffix ext path) exts;
|
filterBySuffix = path: exts: lib.any (ext: lib.hasSuffix ext path) exts;
|
||||||
sourceFilters = path: type: (craneLib.filterCargoSources path type) || filterBySuffix path [".c" ".h" ".hpp" ".cpp" ".cc"];
|
sourceFilters = path: type: (craneLib.filterCargoSources path type) || filterBySuffix path [".c" ".h" ".hpp" ".cpp" ".cc" "wgsl"];
|
||||||
in
|
in
|
||||||
lib.cleanSourceWith {
|
lib.cleanSourceWith {
|
||||||
filter = sourceFilters;
|
filter = sourceFilters;
|
||||||
@@ -70,22 +78,56 @@
|
|||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
pkg-config
|
pkg-config
|
||||||
];
|
];
|
||||||
|
# LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [pkgs.wayland];
|
||||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;
|
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;
|
||||||
|
# SYSTEM_DEPS_LINK = "static";
|
||||||
|
# PKG_CONFIG_ALL_STATIC = "1";
|
||||||
|
|
||||||
buildInputs = with pkgs;
|
buildInputs = with pkgs;
|
||||||
[
|
[
|
||||||
vulkan-loader
|
gst_all_1.gst-libav
|
||||||
|
gst_all_1.gst-plugins-base
|
||||||
|
gst_all_1.gst-plugins-good
|
||||||
|
gst_all_1.gst-plugins-bad
|
||||||
|
gst_all_1.gst-plugins-ugly
|
||||||
|
gst_all_1.gst-plugins-rs
|
||||||
|
gst_all_1.gstreamer
|
||||||
|
glib
|
||||||
|
glib-networking
|
||||||
|
|
||||||
|
wrapGAppsHook4
|
||||||
|
|
||||||
|
# bzip2_1_1
|
||||||
|
# libsysprof-capture
|
||||||
|
# pcre2
|
||||||
|
# libunwind
|
||||||
|
# elfutils
|
||||||
|
# zstd
|
||||||
|
|
||||||
openssl
|
openssl
|
||||||
|
vulkan-loader
|
||||||
]
|
]
|
||||||
++ (lib.optionals pkgs.stdenv.isLinux [
|
++ (lib.optionals pkgs.stdenv.isLinux [
|
||||||
|
gst_all_1.gstreamermm
|
||||||
|
gst_all_1.gst-vaapi
|
||||||
|
cudatoolkit
|
||||||
|
|
||||||
|
# util-linux
|
||||||
|
# libselinux
|
||||||
|
# libsepol
|
||||||
|
|
||||||
alsa-lib-with-plugins
|
alsa-lib-with-plugins
|
||||||
libxkbcommon
|
libxkbcommon
|
||||||
udev
|
udev
|
||||||
wayland
|
wayland
|
||||||
|
wayland-protocols
|
||||||
|
# xorg.libX11
|
||||||
|
# xorg.libXi
|
||||||
|
# xorg.libXrandr
|
||||||
])
|
])
|
||||||
++ (lib.optionals pkgs.stdenv.isDarwin [
|
++ (lib.optionals pkgs.stdenv.isDarwin [
|
||||||
libiconv
|
libiconv
|
||||||
apple-sdk_13
|
apple-sdk_26
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
cargoArtifacts = craneLib.buildPackage commonArgs;
|
cargoArtifacts = craneLib.buildPackage commonArgs;
|
||||||
@@ -141,35 +183,55 @@
|
|||||||
default = pkg;
|
default = pkg;
|
||||||
};
|
};
|
||||||
|
|
||||||
devShells = {
|
devShells = rec {
|
||||||
default =
|
rust-shell =
|
||||||
pkgs.mkShell.override {
|
pkgs.mkShell.override {
|
||||||
stdenv = pkgs.clangStdenv;
|
stdenv = pkgs.clangStdenv;
|
||||||
# stdenv =
|
# if pkgs.stdenv.isLinux
|
||||||
# if pkgs.stdenv.isLinux
|
# then (pkgs.stdenvAdapters.useMoldLinker pkgs.clangStdenv)
|
||||||
# then (pkgs.stdenvAdapters.useMoldLinker pkgs.clangStdenv)
|
# else pkgs.clangStdenv;
|
||||||
# else pkgs.clangStdenv;
|
}
|
||||||
} (commonArgs
|
(commonArgs
|
||||||
// {
|
// {
|
||||||
|
# GST_PLUGIN_PATH = "/run/current-system/sw/lib/gstreamer-1.0/";
|
||||||
|
GIO_EXTRA_MODULES = "${pkgs.glib-networking}/lib/gio/modules";
|
||||||
packages = with pkgs;
|
packages = with pkgs;
|
||||||
[
|
[
|
||||||
toolchainWithRustAnalyzer
|
toolchainWithRustAnalyzer
|
||||||
cargo-nextest
|
bacon
|
||||||
|
cargo-audit
|
||||||
cargo-deny
|
cargo-deny
|
||||||
cargo-expand
|
cargo-expand
|
||||||
bacon
|
|
||||||
cargo-make
|
|
||||||
cargo-hack
|
cargo-hack
|
||||||
|
cargo-make
|
||||||
|
cargo-nextest
|
||||||
cargo-outdated
|
cargo-outdated
|
||||||
lld
|
lld
|
||||||
|
lldb
|
||||||
|
(crates.buildCrate "cargo-with" {doCheck = false;})
|
||||||
|
(crates.buildCrate "dioxus-cli" {
|
||||||
|
nativeBuildInputs = with pkgs; [pkg-config];
|
||||||
|
buildInputs = [openssl];
|
||||||
|
doCheck = false;
|
||||||
|
})
|
||||||
|
(crates.buildCrate "cargo-hot" {
|
||||||
|
nativeBuildInputs = with pkgs; [pkg-config];
|
||||||
|
buildInputs = [openssl];
|
||||||
|
})
|
||||||
]
|
]
|
||||||
++ (lib.optionals pkgs.stdenv.isDarwin [
|
++ (lib.optionals pkgs.stdenv.isDarwin [
|
||||||
apple-sdk_13
|
apple-sdk_26
|
||||||
])
|
])
|
||||||
++ (lib.optionals pkgs.stdenv.isLinux [
|
++ (lib.optionals pkgs.stdenv.isLinux [
|
||||||
mold
|
ffmpeg
|
||||||
|
heaptrack
|
||||||
|
samply
|
||||||
|
cargo-flamegraph
|
||||||
|
perf
|
||||||
|
# mold
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
default = rust-shell;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
62
gst/.github/workflows/build.yaml
vendored
Normal file
62
gst/.github/workflows/build.yaml
vendored
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
name: build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
checks-matrix:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- id: set-matrix
|
||||||
|
name: Generate Nix Matrix
|
||||||
|
run: |
|
||||||
|
set -Eeu
|
||||||
|
matrix="$(nix eval --json '.#githubActions.matrix')"
|
||||||
|
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
checks-build:
|
||||||
|
needs: checks-matrix
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix: ${{fromJSON(needs.checks-matrix.outputs.matrix)}}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- run: nix build -L '.#${{ matrix.attr }}'
|
||||||
|
|
||||||
|
codecov:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: "write"
|
||||||
|
contents: "read"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
|
- name: Run codecov
|
||||||
|
run: nix build .#checks.x86_64-linux.hello-llvm-cov
|
||||||
|
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
uses: codecov/codecov-action@v4.0.1
|
||||||
|
with:
|
||||||
|
flags: unittests
|
||||||
|
name: codecov-hello
|
||||||
|
fail_ci_if_error: true
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
files: ./result
|
||||||
|
verbose: true
|
||||||
|
|
||||||
38
gst/.github/workflows/docs.yaml
vendored
Normal file
38
gst/.github/workflows/docs.yaml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: "write"
|
||||||
|
contents: "read"
|
||||||
|
pages: "write"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: DeterminateSystems/flake-checker-action@main
|
||||||
|
|
||||||
|
- name: Generate docs
|
||||||
|
run: nix build .#checks.x86_64-linux.hello-docs
|
||||||
|
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v5
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: result/share/doc
|
||||||
|
|
||||||
|
- name: Deploy to gh-pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
|
|
||||||
3
gst/.gitignore
vendored
Normal file
3
gst/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/result
|
||||||
|
/target
|
||||||
|
.direnv
|
||||||
1040
gst/Cargo.lock
generated
Normal file
1040
gst/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
gst/Cargo.toml
Normal file
24
gst/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "gst"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
error-stack = "0.6"
|
||||||
|
futures = "0.3.31"
|
||||||
|
futures-lite = "2.6.1"
|
||||||
|
glib = "0.21.5"
|
||||||
|
glib-sys = "0.21.5"
|
||||||
|
gstreamer = { version = "0.24.4", features = ["v1_26"] }
|
||||||
|
gstreamer-app = { version = "0.24.4", features = ["v1_26"] }
|
||||||
|
gstreamer-video = { version = "0.24.4", features = ["v1_26"] }
|
||||||
|
gstreamer-base = { version = "0.24.4", features = ["v1_26"] }
|
||||||
|
thiserror = "2.0"
|
||||||
|
tracing = { version = "0.1", features = ["log"] }
|
||||||
|
bitflags = "2.10.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
smol = "2.0.2"
|
||||||
|
tracing-subscriber = "0.3.22"
|
||||||
38
gst/src/bin.rs
Normal file
38
gst/src/bin.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use crate::priv_prelude::*;
|
||||||
|
|
||||||
|
wrap_gst!(Bin);
|
||||||
|
parent_child!(Element, Bin);
|
||||||
|
|
||||||
|
impl Bin {
|
||||||
|
pub fn new(name: impl AsRef<str>) -> Self {
|
||||||
|
let bin = gstreamer::Bin::with_name(name.as_ref());
|
||||||
|
Bin { inner: bin }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, element: &impl ChildOf<Element>) -> Result<&mut Self> {
|
||||||
|
self.inner
|
||||||
|
.add(&element.upcast_ref().inner)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to add element to bin")?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_many<'a, E: ChildOf<Element> + 'a>(
|
||||||
|
&mut self,
|
||||||
|
elements: impl IntoIterator<Item = &'a E>,
|
||||||
|
) -> Result<&mut Self> {
|
||||||
|
self.inner
|
||||||
|
.add_many(elements.into_iter().map(|e| &e.upcast_ref().inner))
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to add elements to bin")?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_pad(&mut self, pad: &Pad) -> Result<&mut Self> {
|
||||||
|
self.inner
|
||||||
|
.add_pad(&pad.inner)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to add pad to bin")?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
27
gst/src/bus.rs
Normal file
27
gst/src/bus.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use crate::priv_prelude::*;
|
||||||
|
|
||||||
|
wrap_gst!(Bus);
|
||||||
|
|
||||||
|
impl Bus {
|
||||||
|
pub fn iter_timed(
|
||||||
|
&self,
|
||||||
|
timeout: impl Into<Option<core::time::Duration>>,
|
||||||
|
) -> gstreamer::bus::Iter<'_> {
|
||||||
|
let clocktime = match timeout.into() {
|
||||||
|
Some(dur) => gstreamer::ClockTime::try_from(dur).ok(),
|
||||||
|
None => gstreamer::ClockTime::NONE,
|
||||||
|
};
|
||||||
|
self.inner.iter_timed(clocktime)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stream(&self) -> gstreamer::bus::BusStream {
|
||||||
|
self.inner.stream()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn filtered_stream<'a>(
|
||||||
|
&self,
|
||||||
|
msg_types: &'a [gstreamer::MessageType],
|
||||||
|
) -> impl futures::stream::FusedStream<Item = gstreamer::Message> + Unpin + Send + 'a {
|
||||||
|
self.inner.stream_filtered(msg_types)
|
||||||
|
}
|
||||||
|
}
|
||||||
78
gst/src/caps.rs
Normal file
78
gst/src/caps.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use gstreamer::Fraction;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Caps {
|
||||||
|
pub(crate) inner: gstreamer::caps::Caps,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Caps {
|
||||||
|
pub fn builder(cs: CapsType) -> CapsBuilder {
|
||||||
|
CapsBuilder::new(cs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CapsBuilder {
|
||||||
|
inner: gstreamer::caps::Builder<gstreamer::caps::NoFeature>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CapsBuilder {
|
||||||
|
pub fn field<V: Into<glib::Value> + Send>(mut self, name: impl AsRef<str>, value: V) -> Self {
|
||||||
|
self.inner = self.inner.field(name.as_ref(), value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> Caps {
|
||||||
|
Caps {
|
||||||
|
inner: self.inner.build(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum CapsType {
|
||||||
|
Video,
|
||||||
|
Audio,
|
||||||
|
Text,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CapsType {
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
CapsType::Video => "video/x-raw",
|
||||||
|
CapsType::Audio => "audio/x-raw",
|
||||||
|
CapsType::Text => "text/x-raw",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CapsBuilder {
|
||||||
|
pub fn new(cs: CapsType) -> Self {
|
||||||
|
CapsBuilder {
|
||||||
|
inner: gstreamer::Caps::builder(cs.as_str()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Caps {
|
||||||
|
pub fn format(&self) -> Option<gstreamer_video::VideoFormat> {
|
||||||
|
self.inner
|
||||||
|
.structure(0)
|
||||||
|
.and_then(|s| s.get::<&str>("format").ok())
|
||||||
|
.map(|s| gstreamer_video::VideoFormat::from_string(s))
|
||||||
|
}
|
||||||
|
pub fn width(&self) -> Option<i32> {
|
||||||
|
self.inner
|
||||||
|
.structure(0)
|
||||||
|
.and_then(|s| s.get::<i32>("width").ok())
|
||||||
|
}
|
||||||
|
pub fn height(&self) -> Option<i32> {
|
||||||
|
self.inner
|
||||||
|
.structure(0)
|
||||||
|
.and_then(|s| s.get::<i32>("height").ok())
|
||||||
|
}
|
||||||
|
pub fn framerate(&self) -> Option<gstreamer::Fraction> {
|
||||||
|
self.inner
|
||||||
|
.structure(0)
|
||||||
|
.and_then(|s| s.get::<Fraction>("framerate").ok())
|
||||||
|
}
|
||||||
|
}
|
||||||
133
gst/src/element.rs
Normal file
133
gst/src/element.rs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
use crate::priv_prelude::*;
|
||||||
|
use crate::wrap_gst;
|
||||||
|
|
||||||
|
wrap_gst!(Element, gstreamer::Element);
|
||||||
|
|
||||||
|
// pub trait IsElement {
|
||||||
|
// fn upcast_ref(&self) -> ∈
|
||||||
|
// fn into_element(self) -> Element;
|
||||||
|
// fn pad(&self, name: &str) -> Option<Pad> {
|
||||||
|
// use gstreamer::prelude::*;
|
||||||
|
// self.upcast_ref().inner.static_pad(name).map(Pad::from)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl IsElement for Element {
|
||||||
|
// fn upcast_ref(&self) -> &Element {
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// fn into_element(self) -> Element {
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
impl Element {
|
||||||
|
pub fn pad(&self, name: impl AsRef<str>) -> Option<Pad> {
|
||||||
|
use gstreamer::prelude::*;
|
||||||
|
self.inner.static_pad(name.as_ref()).map(Pad::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bus(&self) -> Result<Bus> {
|
||||||
|
use gstreamer::prelude::*;
|
||||||
|
self.inner
|
||||||
|
.bus()
|
||||||
|
.map(Bus::from)
|
||||||
|
.ok_or(Error)
|
||||||
|
.attach_with(|| format!("Failed to get bus from Element: {}", self.inner.name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Sink: ChildOf<Element> {
|
||||||
|
fn sink(&self, name: impl AsRef<str>) -> Pad {
|
||||||
|
self.upcast_ref()
|
||||||
|
.pad(name)
|
||||||
|
.expect("Sink element has no sink pad")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub trait Source: ChildOf<Element> {
|
||||||
|
fn source(&self, name: impl AsRef<str>) -> Pad {
|
||||||
|
self.upcast_ref()
|
||||||
|
.pad(name)
|
||||||
|
.expect("Source element has no src pad")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn link<S: Sink>(&self, sink: &S) -> Result<Bin>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
use gstreamer::prelude::*;
|
||||||
|
if let Ok(bin) = self.upcast_ref().inner.clone().downcast::<gstreamer::Bin>() {
|
||||||
|
bin.add(&sink.upcast_ref().inner)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to add sink to bin")?;
|
||||||
|
self.upcast_ref()
|
||||||
|
.inner
|
||||||
|
.link(&sink.upcast_ref().inner)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to link elements")?;
|
||||||
|
Ok(Bin::from(bin))
|
||||||
|
} else {
|
||||||
|
let bin = gstreamer::Bin::builder()
|
||||||
|
.name(format!(
|
||||||
|
"{}-link-{}",
|
||||||
|
self.upcast_ref().inner.name(),
|
||||||
|
sink.upcast_ref().inner.name()
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
bin.add(&self.upcast_ref().inner)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to add source to bin")?;
|
||||||
|
bin.add(&sink.upcast_ref().inner)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to add sink to bin")?;
|
||||||
|
self.upcast_ref()
|
||||||
|
.inner
|
||||||
|
.link(&sink.upcast_ref().inner)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to link elements")?;
|
||||||
|
if let Some(sink_pad) = self.upcast_ref().pad("sink") {
|
||||||
|
let ghost_pad = Pad::ghost(&sink_pad)?;
|
||||||
|
bin.add_pad(&ghost_pad.inner)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to add src pad to bin")?;
|
||||||
|
ghost_pad.activate(true)?;
|
||||||
|
}
|
||||||
|
Ok(From::from(bin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn link_pad<S: Sink>(&self, sink: &S, src_pad_name: &str, sink_pad_name: &str) -> Result<()> {
|
||||||
|
// use gstreamer::prelude::*;
|
||||||
|
// let src_pad = self
|
||||||
|
// .upcast_ref()
|
||||||
|
// .pad(src_pad_name)
|
||||||
|
// .ok_or(Error)
|
||||||
|
// .attach("Source pad not found")?;
|
||||||
|
// let sink_pad = sink
|
||||||
|
// .upcast_ref()
|
||||||
|
// .pad(sink_pad_name)
|
||||||
|
// .ok_or(Error)
|
||||||
|
// .attach("Sink pad not found")?;
|
||||||
|
// src_pad
|
||||||
|
// .inner
|
||||||
|
// .link(&sink_pad.inner)
|
||||||
|
// .change_context(Error)
|
||||||
|
// .attach("Failed to link source pad to sink pad")?;
|
||||||
|
// Ok(())
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ElementExt: ChildOf<Element> + Sync {
|
||||||
|
#[track_caller]
|
||||||
|
fn bus(&self) -> Result<Bus> {
|
||||||
|
self.upcast_ref().bus()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn pad(&self, name: impl AsRef<str>) -> Option<Pad> {
|
||||||
|
self.upcast_ref().pad(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ChildOf<Element> + Sync> ElementExt for T {}
|
||||||
7
gst/src/errors.rs
Normal file
7
gst/src/errors.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
pub use error_stack::{Report, ResultExt};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[error("An error occurred")]
|
||||||
|
pub struct Error;
|
||||||
|
|
||||||
|
pub type Result<T, E = error_stack::Report<Error>> = core::result::Result<T, E>;
|
||||||
64
gst/src/lib.rs
Normal file
64
gst/src/lib.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
pub mod bin;
|
||||||
|
pub mod bus;
|
||||||
|
pub mod caps;
|
||||||
|
pub mod element;
|
||||||
|
pub mod errors;
|
||||||
|
pub mod pad;
|
||||||
|
pub mod pipeline;
|
||||||
|
pub mod plugins;
|
||||||
|
#[macro_use]
|
||||||
|
pub mod wrapper;
|
||||||
|
pub mod sample;
|
||||||
|
|
||||||
|
pub use bin::*;
|
||||||
|
pub use bus::*;
|
||||||
|
pub use caps::*;
|
||||||
|
pub use element::*;
|
||||||
|
pub use gstreamer;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use gstreamer::{Message, MessageType, MessageView, State};
|
||||||
|
pub use gstreamer_video::VideoFormat;
|
||||||
|
pub use pad::*;
|
||||||
|
pub use pipeline::*;
|
||||||
|
pub use plugins::*;
|
||||||
|
pub use sample::*;
|
||||||
|
|
||||||
|
pub(crate) mod priv_prelude {
|
||||||
|
pub use crate::errors::*;
|
||||||
|
pub use crate::wrapper::*;
|
||||||
|
pub use crate::*;
|
||||||
|
pub use gstreamer::prelude::ElementExt as _;
|
||||||
|
pub use gstreamer::prelude::*;
|
||||||
|
#[track_caller]
|
||||||
|
pub fn duration_to_clocktime(
|
||||||
|
timeout: impl Into<Option<core::time::Duration>>,
|
||||||
|
) -> Result<Option<gstreamer::ClockTime>> {
|
||||||
|
match timeout.into() {
|
||||||
|
Some(dur) => {
|
||||||
|
let clocktime = gstreamer::ClockTime::try_from(dur)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to convert duration to ClockTime")?;
|
||||||
|
Ok(Some(clocktime))
|
||||||
|
}
|
||||||
|
None => Ok(gstreamer::ClockTime::NONE),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
static GST: std::sync::LazyLock<std::sync::Arc<Gst>> = std::sync::LazyLock::new(|| {
|
||||||
|
gstreamer::init().expect("Failed to initialize GStreamer");
|
||||||
|
std::sync::Arc::new(Gst {
|
||||||
|
__private: core::marker::PhantomData,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
pub struct Gst {
|
||||||
|
__private: core::marker::PhantomData<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Gst {
|
||||||
|
pub fn new() -> Arc<Self> {
|
||||||
|
Arc::clone(&GST)
|
||||||
|
}
|
||||||
|
}
|
||||||
45
gst/src/pad.rs
Normal file
45
gst/src/pad.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use crate::priv_prelude::*;
|
||||||
|
|
||||||
|
wrap_gst!(Pad, gstreamer::Pad);
|
||||||
|
|
||||||
|
impl Pad {
|
||||||
|
#[track_caller]
|
||||||
|
pub fn ghost(target: &Pad) -> Result<Pad> {
|
||||||
|
let ghost_pad = gstreamer::GhostPad::with_target(&target.inner)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to create ghost pad")?;
|
||||||
|
Ok(Pad {
|
||||||
|
inner: ghost_pad.upcast(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn link(&self, peer: &Pad) -> Result<()> {
|
||||||
|
use gstreamer::prelude::*;
|
||||||
|
self.inner
|
||||||
|
.link(&peer.inner)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to link pads")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn current_caps(&self) -> Result<Caps> {
|
||||||
|
let caps = self
|
||||||
|
.inner
|
||||||
|
.current_caps()
|
||||||
|
.ok_or(Error)
|
||||||
|
.attach("Failed to get pad caps")?;
|
||||||
|
Ok(Caps { inner: caps })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn activate(&self, activate: bool) -> Result<()> {
|
||||||
|
use gstreamer::prelude::*;
|
||||||
|
self.inner
|
||||||
|
.set_active(activate)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to set_active pad")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
211
gst/src/pipeline.rs
Normal file
211
gst/src/pipeline.rs
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
use crate::priv_prelude::*;
|
||||||
|
|
||||||
|
wrap_gst!(Pipeline);
|
||||||
|
parent_child!(Element, Pipeline);
|
||||||
|
parent_child!(Bin, Pipeline);
|
||||||
|
|
||||||
|
impl Drop for Pipeline {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.inner.set_state(gstreamer::State::Null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pipeline {
|
||||||
|
#[track_caller]
|
||||||
|
pub fn bus(&self) -> Result<Bus> {
|
||||||
|
let bus = self
|
||||||
|
.inner
|
||||||
|
.bus()
|
||||||
|
.ok_or(Error)
|
||||||
|
.attach("Failed to get bus from pipeline")?;
|
||||||
|
Ok(Bus::from_gst(bus))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the state
|
||||||
|
pub fn state(
|
||||||
|
&self,
|
||||||
|
timeout: impl Into<Option<core::time::Duration>>,
|
||||||
|
) -> Result<gstreamer::State> {
|
||||||
|
let (result, current, _pending) = self.inner.state(duration_to_clocktime(timeout)?);
|
||||||
|
result.change_context(Error).attach("Failed to get state")?;
|
||||||
|
Ok(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play(&self) -> Result<()> {
|
||||||
|
self.inner
|
||||||
|
.set_state(gstreamer::State::Playing)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to set pipeline to Playing state")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pause(&self) -> Result<()> {
|
||||||
|
self.inner
|
||||||
|
.set_state(gstreamer::State::Paused)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to set pipeline to Paused state")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ready(&self) -> Result<()> {
|
||||||
|
self.inner
|
||||||
|
.set_state(gstreamer::State::Ready)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to set pipeline to Ready state")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&self) -> Result<()> {
|
||||||
|
self.inner
|
||||||
|
.set_state(gstreamer::State::Null)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to set pipeline to Null state")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_state(&self, state: gstreamer::State) -> Result<gstreamer::StateChangeSuccess> {
|
||||||
|
let result = self
|
||||||
|
.inner
|
||||||
|
.set_state(state)
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to set pipeline state")?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wait_for(&self, state: gstreamer::State) -> Result<()> {
|
||||||
|
let current_state = self.state(core::time::Duration::ZERO)?;
|
||||||
|
if current_state == state {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
// use futures::stream::StreamExt;
|
||||||
|
use futures_lite::stream::StreamExt as _;
|
||||||
|
self.bus()?
|
||||||
|
.filtered_stream(&[MessageType::StateChanged])
|
||||||
|
.find(|message: &gstreamer::Message| {
|
||||||
|
let view = message.view();
|
||||||
|
if let gstreamer::MessageView::StateChanged(changed) = view {
|
||||||
|
changed.current() == state
|
||||||
|
&& changed.src().is_some_and(|s| s == &self.inner)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wait_for_states(&self, states: impl AsRef<[gstreamer::State]>) -> Result<()> {
|
||||||
|
let current_state = self.state(core::time::Duration::ZERO)?;
|
||||||
|
let states = states.as_ref();
|
||||||
|
if states.contains(¤t_state) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
use futures_lite::stream::StreamExt as _;
|
||||||
|
self.bus()?
|
||||||
|
.filtered_stream(&[MessageType::StateChanged])
|
||||||
|
.find(|message: &gstreamer::Message| {
|
||||||
|
let view = message.view();
|
||||||
|
if let gstreamer::MessageView::StateChanged(changed) = view {
|
||||||
|
states.contains(&changed.current())
|
||||||
|
&& changed.src().is_some_and(|s| s == &self.inner)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wait_for_message<'a, F2>(
|
||||||
|
&self,
|
||||||
|
filter: Option<&'a [gstreamer::MessageType]>,
|
||||||
|
filter_fn: F2,
|
||||||
|
) -> Result<gstreamer::Message>
|
||||||
|
where
|
||||||
|
F2: Fn(&gstreamer::Message) -> bool + Send + 'a,
|
||||||
|
{
|
||||||
|
use futures_lite::stream::StreamExt as _;
|
||||||
|
match filter {
|
||||||
|
Some(filter) => {
|
||||||
|
let message = self.bus()?.filtered_stream(filter).find(filter_fn).await;
|
||||||
|
match message {
|
||||||
|
Some(msg) => Ok(msg),
|
||||||
|
None => {
|
||||||
|
Err(Error).attach("Failed to find message matching the provided filter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let message = self.bus()?.stream().find(filter_fn).await;
|
||||||
|
match message {
|
||||||
|
Some(msg) => Ok(msg),
|
||||||
|
None => {
|
||||||
|
Err(Error).attach("Failed to find message matching the provided filter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PipelineExt: ChildOf<Pipeline> + Sync {
|
||||||
|
// #[track_caller]
|
||||||
|
// fn bus(&self) -> Result<Bus> {
|
||||||
|
// self.upcast_ref().bus()
|
||||||
|
// }
|
||||||
|
#[track_caller]
|
||||||
|
fn play(&self) -> Result<()> {
|
||||||
|
self.upcast_ref().play()
|
||||||
|
}
|
||||||
|
#[track_caller]
|
||||||
|
fn pause(&self) -> Result<()> {
|
||||||
|
self.upcast_ref().pause()
|
||||||
|
}
|
||||||
|
#[track_caller]
|
||||||
|
fn ready(&self) -> Result<()> {
|
||||||
|
self.upcast_ref().ready()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn stop(&self) -> Result<()> {
|
||||||
|
self.upcast_ref().stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn set_state(&self, state: gstreamer::State) -> Result<gstreamer::StateChangeSuccess> {
|
||||||
|
self.upcast_ref().set_state(state)
|
||||||
|
}
|
||||||
|
#[track_caller]
|
||||||
|
fn state(&self, timeout: impl Into<Option<core::time::Duration>>) -> Result<gstreamer::State> {
|
||||||
|
self.upcast_ref().state(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_for(
|
||||||
|
&self,
|
||||||
|
state: gstreamer::State,
|
||||||
|
) -> impl std::future::Future<Output = Result<()>> + Send {
|
||||||
|
self.upcast_ref().wait_for(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_for_states(
|
||||||
|
&self,
|
||||||
|
states: impl AsRef<[gstreamer::State]> + Send,
|
||||||
|
) -> impl std::future::Future<Output = Result<()>> + Send {
|
||||||
|
self.upcast_ref().wait_for_states(states)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_for_message<'a, F2>(
|
||||||
|
&self,
|
||||||
|
filter: Option<&'a [gstreamer::MessageType]>,
|
||||||
|
filter_fn: F2,
|
||||||
|
) -> impl std::future::Future<Output = Result<gstreamer::Message>> + Send
|
||||||
|
where
|
||||||
|
F2: Fn(&gstreamer::Message) -> bool + Send + 'a,
|
||||||
|
{
|
||||||
|
self.upcast_ref().wait_for_message(filter, filter_fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ChildOf<Pipeline> + Sync> PipelineExt for T {}
|
||||||
4
gst/src/plugins.rs
Normal file
4
gst/src/plugins.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod app;
|
||||||
|
pub mod autodetect;
|
||||||
|
pub mod playback;
|
||||||
|
pub mod videoconvertscale;
|
||||||
2
gst/src/plugins/app.rs
Normal file
2
gst/src/plugins/app.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod appsink;
|
||||||
|
pub use appsink::*;
|
||||||
278
gst/src/plugins/app/appsink.rs
Normal file
278
gst/src/plugins/app/appsink.rs
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
use crate::priv_prelude::*;
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use gstreamer_app::AppSinkCallbacks;
|
||||||
|
|
||||||
|
wrap_gst!(AppSink, gstreamer::Element);
|
||||||
|
parent_child!(Element, AppSink);
|
||||||
|
|
||||||
|
pub struct AppSinkBuilder {
|
||||||
|
inner: AppSink,
|
||||||
|
callbacks: Option<gstreamer_app::app_sink::AppSinkCallbacksBuilder>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppSinkBuilder {
|
||||||
|
pub fn on_new_sample<F>(mut self, mut f: F) -> Self
|
||||||
|
where
|
||||||
|
F: FnMut(&AppSink) -> Result<(), gstreamer::FlowError> + Send + 'static,
|
||||||
|
{
|
||||||
|
let mut callbacks_builder = self
|
||||||
|
.callbacks
|
||||||
|
.take()
|
||||||
|
.unwrap_or_else(gstreamer_app::app_sink::AppSinkCallbacks::builder);
|
||||||
|
callbacks_builder = callbacks_builder.new_sample(move |appsink| {
|
||||||
|
use glib::object::Cast;
|
||||||
|
let element = appsink.upcast_ref::<gstreamer::Element>();
|
||||||
|
let appsink = AppSink::from_gst_ref(element);
|
||||||
|
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(appsink)))
|
||||||
|
.unwrap_or(Err(gstreamer::FlowError::Error))
|
||||||
|
.map(|_| gstreamer::FlowSuccess::Ok)
|
||||||
|
});
|
||||||
|
self.callbacks = Some(callbacks_builder);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_new_preroll<F>(mut self, mut f: F) -> Self
|
||||||
|
where
|
||||||
|
F: FnMut(&AppSink) -> Result<(), gstreamer::FlowError> + Send + 'static,
|
||||||
|
{
|
||||||
|
let mut callbacks_builder = self
|
||||||
|
.callbacks
|
||||||
|
.take()
|
||||||
|
.unwrap_or_else(gstreamer_app::app_sink::AppSinkCallbacks::builder);
|
||||||
|
callbacks_builder = callbacks_builder.new_preroll(move |appsink| {
|
||||||
|
use glib::object::Cast;
|
||||||
|
let element = appsink.upcast_ref::<gstreamer::Element>();
|
||||||
|
let appsink = AppSink::from_gst_ref(element);
|
||||||
|
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(appsink)))
|
||||||
|
.unwrap_or(Err(gstreamer::FlowError::Error))
|
||||||
|
.map(|_| gstreamer::FlowSuccess::Ok)
|
||||||
|
});
|
||||||
|
self.callbacks = Some(callbacks_builder);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> AppSink {
|
||||||
|
let AppSinkBuilder { inner, callbacks } = self;
|
||||||
|
if let Some(callbacks) = callbacks {
|
||||||
|
inner.appsink().set_callbacks(callbacks.build());
|
||||||
|
}
|
||||||
|
inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sink for AppSink {}
|
||||||
|
|
||||||
|
impl AppSink {
|
||||||
|
pub fn builder(name: impl AsRef<str>) -> AppSinkBuilder {
|
||||||
|
let inner = AppSink::new(name).expect("Failed to create AppSink");
|
||||||
|
AppSinkBuilder {
|
||||||
|
inner,
|
||||||
|
callbacks: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn appsink(&self) -> &gstreamer_app::AppSink {
|
||||||
|
self.inner
|
||||||
|
.downcast_ref::<gstreamer_app::AppSink>()
|
||||||
|
.expect("Failed to downcast to AppSink")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(name: impl AsRef<str>) -> Result<Self> {
|
||||||
|
let inner = gstreamer::ElementFactory::make("appsink")
|
||||||
|
.name(name.as_ref())
|
||||||
|
.build()
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to create appsink element")?;
|
||||||
|
Ok(AppSink { inner })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn emit_signals(&mut self, emit: bool) -> &mut Self {
|
||||||
|
self.inner.set_property("emit-signals", emit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn async_(&mut self, async_: bool) -> &mut Self {
|
||||||
|
self.inner.set_property("async", async_);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sync(&mut self, sync: bool) -> &mut Self {
|
||||||
|
self.inner.set_property("sync", sync);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn drop(&mut self, drop: bool) -> &mut Self {
|
||||||
|
self.inner.set_property("drop", drop);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn caps(&mut self, caps: Caps) -> &mut Self {
|
||||||
|
self.inner.set_property("caps", caps.inner);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn callbacks(&mut self, callbacks: gstreamer_app::AppSinkCallbacks) -> &mut Self {
|
||||||
|
self.appsink().set_callbacks(callbacks);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_new_sample<F>(&mut self, mut f: F) -> &mut Self
|
||||||
|
where
|
||||||
|
F: FnMut(&AppSink) -> Result<(), gstreamer::FlowError> + Send + 'static,
|
||||||
|
{
|
||||||
|
self.emit_signals(true).callbacks(
|
||||||
|
AppSinkCallbacks::builder()
|
||||||
|
.new_sample(move |appsink| {
|
||||||
|
use glib::object::Cast;
|
||||||
|
let element = appsink.upcast_ref::<gstreamer::Element>();
|
||||||
|
let appsink = AppSink::from_gst_ref(element);
|
||||||
|
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(appsink)))
|
||||||
|
.unwrap_or(Err(gstreamer::FlowError::Error))
|
||||||
|
.map(|_| gstreamer::FlowSuccess::Ok)
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pull_sample(&self) -> Result<Sample> {
|
||||||
|
self.appsink()
|
||||||
|
.pull_sample()
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to pull sample from AppSink")
|
||||||
|
.map(Sample::from)
|
||||||
|
}
|
||||||
|
pub fn try_pull_sample(
|
||||||
|
&self,
|
||||||
|
timeout: impl Into<Option<core::time::Duration>>,
|
||||||
|
) -> Result<Option<Sample>> {
|
||||||
|
Ok(self
|
||||||
|
.appsink()
|
||||||
|
.try_pull_sample(duration_to_clocktime(timeout)?)
|
||||||
|
.map(From::from))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pull_preroll(&self) -> Result<Sample> {
|
||||||
|
self.appsink()
|
||||||
|
.pull_preroll()
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to pull preroll sample from AppSink")
|
||||||
|
.map(Sample::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_pull_preroll(
|
||||||
|
&self,
|
||||||
|
timeout: impl Into<Option<core::time::Duration>>,
|
||||||
|
) -> Result<Option<Sample>> {
|
||||||
|
Ok(self
|
||||||
|
.appsink()
|
||||||
|
.try_pull_preroll(duration_to_clocktime(timeout)?)
|
||||||
|
.map(From::from))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_appsink() {
|
||||||
|
use gstreamer::prelude::*;
|
||||||
|
use tracing_subscriber::prelude::*;
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::fmt::layer()
|
||||||
|
.with_thread_ids(true)
|
||||||
|
.with_file(true),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
tracing::info!("Linking videoconvert to appsink");
|
||||||
|
Gst::new();
|
||||||
|
let playbin3 = playback::Playbin3::new("pppppppppppppppppppppppppppppp").unwrap().with_uri("https://jellyfin.tsuba.darksailor.dev/Items/6010382cf25273e624d305907010d773/Download?api_key=036c140222464878862231ef66a2bc9c");
|
||||||
|
|
||||||
|
let video_convert = plugins::videoconvertscale::VideoConvert::new("vcvcvcvcvcvcvcvcvcvcvcvcvc")
|
||||||
|
.expect("Create videoconvert");
|
||||||
|
let mut appsink = app::AppSink::new("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").expect("Create appsink");
|
||||||
|
appsink.caps(
|
||||||
|
Caps::builder(CapsType::Video)
|
||||||
|
.field("format", "RGB")
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let video_sink = video_convert
|
||||||
|
.link(&appsink)
|
||||||
|
.expect("Link videoconvert to appsink");
|
||||||
|
|
||||||
|
let playbin3 = playbin3.with_video_sink(&video_sink);
|
||||||
|
playbin3.play().expect("Play video");
|
||||||
|
let bus = playbin3.bus().unwrap();
|
||||||
|
for msg in bus.iter_timed(None) {
|
||||||
|
match msg.view() {
|
||||||
|
gstreamer::MessageView::Eos(..) => {
|
||||||
|
tracing::info!("End of stream reached");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
gstreamer::MessageView::Error(err) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Error from {:?}: {} ({:?})",
|
||||||
|
err.src().map(|s| s.path_string()),
|
||||||
|
err.error(),
|
||||||
|
err.debug()
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
gstreamer::MessageView::StateChanged(state) => {
|
||||||
|
eprintln!(
|
||||||
|
"State changed from {:?} to \x1b[33m{:?}\x1b[0m for {:?}",
|
||||||
|
state.old(),
|
||||||
|
state.current(),
|
||||||
|
state.src().map(|s| s.path_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
// tracing::info!("{:#?}", &msg.view());
|
||||||
|
}
|
||||||
|
// std::thread::sleep(std::time::Duration::from_secs(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_appsink_metadata() {
|
||||||
|
use tracing_subscriber::prelude::*;
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::fmt::layer()
|
||||||
|
.with_thread_ids(true)
|
||||||
|
.with_file(true),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
crate::Gst::new();
|
||||||
|
|
||||||
|
let url = "https://jellyfin.tsuba.darksailor.dev/Items/6010382cf25273e624d305907010d773/Download?api_key=036c140222464878862231ef66a2bc9c";
|
||||||
|
|
||||||
|
let videoconvert = crate::plugins::videoconvertscale::VideoConvert::new("iced-video-convert")
|
||||||
|
// .unwrap();
|
||||||
|
// .with_output_format(gst::plugins::videoconvertscale::VideoFormat::Rgba)
|
||||||
|
.unwrap();
|
||||||
|
let appsink = crate::plugins::app::AppSink::new("iced-video-sink")
|
||||||
|
.unwrap()
|
||||||
|
.with_async(true)
|
||||||
|
.with_sync(true);
|
||||||
|
|
||||||
|
let video_sink = videoconvert.link(&appsink).unwrap();
|
||||||
|
let playbin = crate::plugins::playback::Playbin3::new("iced-video")
|
||||||
|
.unwrap()
|
||||||
|
.with_uri(url)
|
||||||
|
.with_video_sink(&video_sink);
|
||||||
|
|
||||||
|
playbin.pause().unwrap();
|
||||||
|
|
||||||
|
smol::block_on(async {
|
||||||
|
playbin.wait_for(gstreamer::State::Paused).await.unwrap();
|
||||||
|
});
|
||||||
|
// std::thread::sleep(core::time::Duration::from_secs(1));
|
||||||
|
let pad = appsink.pad("sink").unwrap();
|
||||||
|
let caps = pad.current_caps().unwrap();
|
||||||
|
let format = caps.format();
|
||||||
|
let height = caps.height();
|
||||||
|
let width = caps.width();
|
||||||
|
let framerate = caps.framerate();
|
||||||
|
dbg!(&format, height, width, framerate);
|
||||||
|
dbg!(&caps);
|
||||||
|
}
|
||||||
2
gst/src/plugins/autodetect.rs
Normal file
2
gst/src/plugins/autodetect.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod autovideosink;
|
||||||
|
pub use autovideosink::*;
|
||||||
18
gst/src/plugins/autodetect/autovideosink.rs
Normal file
18
gst/src/plugins/autodetect/autovideosink.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use crate::priv_prelude::*;
|
||||||
|
|
||||||
|
wrap_gst!(AutoVideoSink, gstreamer::Element);
|
||||||
|
parent_child!(Element, AutoVideoSink);
|
||||||
|
parent_child!(Bin, AutoVideoSink, downcast);
|
||||||
|
|
||||||
|
impl Sink for AutoVideoSink {}
|
||||||
|
|
||||||
|
impl AutoVideoSink {
|
||||||
|
pub fn new(name: impl AsRef<str>) -> Result<Self> {
|
||||||
|
let element = gstreamer::ElementFactory::make("autovideosink")
|
||||||
|
.name(name.as_ref())
|
||||||
|
.build()
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to create autovideosink element")?;
|
||||||
|
Ok(AutoVideoSink { inner: element })
|
||||||
|
}
|
||||||
|
}
|
||||||
71
gst/src/plugins/playback.rs
Normal file
71
gst/src/plugins/playback.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
pub mod playbin3;
|
||||||
|
pub use playbin3::*;
|
||||||
|
pub mod playbin;
|
||||||
|
pub use playbin::*;
|
||||||
|
|
||||||
|
bitflags::bitflags! {
|
||||||
|
/// Extra flags to configure the behaviour of the sinks.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct PlayFlags: u32 {
|
||||||
|
/// Render the video stream
|
||||||
|
const VIDEO = (1 << 0);
|
||||||
|
/// Render the audio stream
|
||||||
|
const AUDIO = (1 << 1);
|
||||||
|
/// Render subtitles
|
||||||
|
const TEXT = (1 << 2);
|
||||||
|
/// Render visualisation when no video is present
|
||||||
|
const VIS = (1 << 3);
|
||||||
|
/// Use software volume
|
||||||
|
const SOFT_VOLUME = (1 << 4);
|
||||||
|
/// Only use native audio formats
|
||||||
|
const NATIVE_AUDIO = (1 << 5);
|
||||||
|
/// Only use native video formats
|
||||||
|
const NATIVE_VIDEO = (1 << 6);
|
||||||
|
/// Attempt progressive download buffering
|
||||||
|
const DOWNLOAD = (1 << 7);
|
||||||
|
/// Buffer demuxed/parsed data
|
||||||
|
const BUFFERING = (1 << 8);
|
||||||
|
/// Deinterlace video if necessary
|
||||||
|
const DEINTERLACE = (1 << 9);
|
||||||
|
/// Use software color balance
|
||||||
|
const SOFT_COLORBALANCE = (1 << 10);
|
||||||
|
/// Force audio/video filter(s) to be applied
|
||||||
|
const FORCE_FILTERS = (1 << 11);
|
||||||
|
/// Force only software-based decoders (no effect for playbin3)
|
||||||
|
const FORCE_SW_DECODERS = (1 << 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _: () = {
|
||||||
|
use glib::types::StaticType;
|
||||||
|
impl glib::types::StaticType for PlayFlags {
|
||||||
|
#[inline]
|
||||||
|
#[doc(alias = "gst_play_flags_get_type")]
|
||||||
|
fn static_type() -> glib::Type {
|
||||||
|
glib::Type::from_name("GstPlayFlags").expect("GstPlayFlags type not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl glib::value::ToValue for PlayFlags {
|
||||||
|
#[inline]
|
||||||
|
fn to_value(&self) -> glib::Value {
|
||||||
|
let value = self.bits().to_value();
|
||||||
|
value
|
||||||
|
.transform_with_type(Self::static_type())
|
||||||
|
.expect("Failed to transform PlayFlags(u32) to GstPlayFlags")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn value_type(&self) -> glib::Type {
|
||||||
|
Self::static_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PlayFlags> for glib::Value {
|
||||||
|
#[inline]
|
||||||
|
fn from(v: PlayFlags) -> Self {
|
||||||
|
// skip_assert_initialized!();
|
||||||
|
glib::value::ToValue::to_value(&v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
82
gst/src/plugins/playback/playbin.rs
Normal file
82
gst/src/plugins/playback/playbin.rs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
use crate::priv_prelude::*;
|
||||||
|
|
||||||
|
wrap_gst!(Playbin, gstreamer::Element);
|
||||||
|
parent_child!(Element, Playbin);
|
||||||
|
parent_child!(Pipeline, Playbin, downcast);
|
||||||
|
parent_child!(Bin, Playbin, downcast);
|
||||||
|
|
||||||
|
impl Drop for Playbin {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.set_state(gstreamer::State::Null).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Playbin {
|
||||||
|
pub fn new(name: impl AsRef<str>) -> Result<Self> {
|
||||||
|
gstreamer::ElementFactory::make("playbin3")
|
||||||
|
.name(name.as_ref())
|
||||||
|
.build()
|
||||||
|
.map(|element| Playbin { inner: element })
|
||||||
|
.change_context(Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_uri(self, uri: impl AsRef<str>) -> Self {
|
||||||
|
self.inner.set_property("uri", uri.as_ref());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_buffer_duration(self, duration: impl Into<Option<core::time::Duration>>) -> Self {
|
||||||
|
let duration = match duration.into() {
|
||||||
|
Some(dur) => dur.as_secs() as i64,
|
||||||
|
None => -1,
|
||||||
|
};
|
||||||
|
self.inner.set_property("buffer-duration", duration);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_buffer_size(self, size: impl Into<Option<u32>>) -> Self {
|
||||||
|
let size = match size.into() {
|
||||||
|
Some(size) => size as i32,
|
||||||
|
None => -1,
|
||||||
|
};
|
||||||
|
self.inner.set_property("buffer-size", size);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum size of the ring buffer in bytes.
|
||||||
|
pub fn with_ring_buffer_max_size(self, size: u64) -> Self {
|
||||||
|
self.inner.set_property("ring-buffer-max-size", size);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_video_sink(self, video_sink: &impl ChildOf<Element>) -> Self {
|
||||||
|
self.inner
|
||||||
|
.set_property("video-sink", &video_sink.upcast_ref().inner);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_text_sink(self, text_sink: &impl ChildOf<Element>) -> Self {
|
||||||
|
self.inner
|
||||||
|
.set_property("text-sink", &text_sink.upcast_ref().inner);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_audio_sink(self, audio_sink: &impl ChildOf<Element>) -> Self {
|
||||||
|
self.inner
|
||||||
|
.set_property("audio-sink", &audio_sink.upcast_ref().inner);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_volume(&self, volume: f64) {
|
||||||
|
self.inner.set_property("volume", volume.clamp(1.0, 100.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_volume(&self) -> f64 {
|
||||||
|
self.inner.property::<f64>("volume")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_flags(self, flags: playback::PlayFlags) -> Self {
|
||||||
|
self.inner.set_property("flags", flags);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
95
gst/src/plugins/playback/playbin3.rs
Normal file
95
gst/src/plugins/playback/playbin3.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use crate::priv_prelude::*;
|
||||||
|
use playback::PlayFlags;
|
||||||
|
|
||||||
|
wrap_gst!(Playbin3, gstreamer::Element);
|
||||||
|
parent_child!(Element, Playbin3);
|
||||||
|
parent_child!(Pipeline, Playbin3, downcast);
|
||||||
|
parent_child!(Bin, Playbin3, downcast);
|
||||||
|
|
||||||
|
impl Drop for Playbin3 {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.set_state(gstreamer::State::Null).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Playbin3 {
|
||||||
|
pub fn new(name: impl AsRef<str>) -> Result<Self> {
|
||||||
|
gstreamer::ElementFactory::make("playbin3")
|
||||||
|
.name(name.as_ref())
|
||||||
|
.build()
|
||||||
|
.map(|element| Playbin3 { inner: element })
|
||||||
|
.change_context(Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_uri(self, uri: impl AsRef<str>) -> Self {
|
||||||
|
self.inner.set_property("uri", uri.as_ref());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_buffer_duration(self, duration: impl Into<Option<core::time::Duration>>) -> Self {
|
||||||
|
let duration = match duration.into() {
|
||||||
|
Some(dur) => dur.as_secs() as i64,
|
||||||
|
None => -1,
|
||||||
|
};
|
||||||
|
self.inner.set_property("buffer-duration", duration);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_buffer_size(self, size: impl Into<Option<u32>>) -> Self {
|
||||||
|
let size = match size.into() {
|
||||||
|
Some(size) => size as i32,
|
||||||
|
None => -1,
|
||||||
|
};
|
||||||
|
self.inner.set_property("buffer-size", size);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum size of the ring buffer in bytes.
|
||||||
|
pub fn with_ring_buffer_max_size(self, size: u64) -> Self {
|
||||||
|
self.inner.set_property("ring-buffer-max-size", size);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_video_sink(self, video_sink: &impl ChildOf<Element>) -> Self {
|
||||||
|
self.inner
|
||||||
|
.set_property("video-sink", &video_sink.upcast_ref().inner);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_text_sink(self, text_sink: &impl ChildOf<Element>) -> Self {
|
||||||
|
self.inner
|
||||||
|
.set_property("text-sink", &text_sink.upcast_ref().inner);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_audio_sink(self, audio_sink: &impl ChildOf<Element>) -> Self {
|
||||||
|
self.inner
|
||||||
|
.set_property("audio-sink", &audio_sink.upcast_ref().inner);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_volume(&self, volume: f64) {
|
||||||
|
self.inner.set_property("volume", volume.clamp(1.0, 100.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_volume(&self) -> f64 {
|
||||||
|
self.inner.property::<f64>("volume")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_flags(self, flags: playback::PlayFlags) -> Self {
|
||||||
|
self.inner.set_property("flags", flags);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Playbin3 {
|
||||||
|
pub fn default_flags() -> PlayFlags {
|
||||||
|
PlayFlags::SOFT_COLORBALANCE
|
||||||
|
| PlayFlags::DEINTERLACE
|
||||||
|
| PlayFlags::BUFFERING
|
||||||
|
| PlayFlags::SOFT_VOLUME
|
||||||
|
| PlayFlags::TEXT
|
||||||
|
| PlayFlags::AUDIO
|
||||||
|
| PlayFlags::VIDEO
|
||||||
|
}
|
||||||
|
}
|
||||||
2
gst/src/plugins/videoconvertscale.rs
Normal file
2
gst/src/plugins/videoconvertscale.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod videoconvert;
|
||||||
|
pub use videoconvert::*;
|
||||||
36
gst/src/plugins/videoconvertscale/videoconvert.rs
Normal file
36
gst/src/plugins/videoconvertscale/videoconvert.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use crate::priv_prelude::*;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use gstreamer_video::VideoFormat;
|
||||||
|
|
||||||
|
wrap_gst!(VideoConvert, gstreamer::Element);
|
||||||
|
parent_child!(Element, VideoConvert);
|
||||||
|
|
||||||
|
impl Sink for VideoConvert {}
|
||||||
|
impl Source for VideoConvert {}
|
||||||
|
|
||||||
|
impl VideoConvert {
|
||||||
|
pub fn new(name: impl AsRef<str>) -> Result<Self> {
|
||||||
|
let element = gstreamer::ElementFactory::make("videoconvert")
|
||||||
|
.name(name.as_ref())
|
||||||
|
.build()
|
||||||
|
.change_context(Error)
|
||||||
|
.attach("Failed to create videoconvert element")?;
|
||||||
|
Ok(VideoConvert { inner: element })
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn with_caps(mut self, caps: &gstreamer::Caps) -> Self {
|
||||||
|
// use gstreamer::prelude::*;
|
||||||
|
// self.inner.set_property("caps", caps);
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
pub fn with_output_format(self, format: VideoFormat) -> Result<Self> {
|
||||||
|
use gstreamer::prelude::*;
|
||||||
|
let caps = Caps::builder(CapsType::Video)
|
||||||
|
.field("format", format.to_str())
|
||||||
|
.build();
|
||||||
|
self.inner.set_property("caps", &caps.inner);
|
||||||
|
// .change_context(Error)
|
||||||
|
// .attach("Failed to set output format on videoconvert")?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
37
gst/src/sample.rs
Normal file
37
gst/src/sample.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
impl From<gstreamer::Sample> for Sample {
|
||||||
|
fn from(inner: gstreamer::Sample) -> Self {
|
||||||
|
Sample { inner }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Sample {
|
||||||
|
pub inner: gstreamer::Sample,
|
||||||
|
}
|
||||||
|
|
||||||
|
use gstreamer::BufferRef;
|
||||||
|
impl Sample {
|
||||||
|
#[doc(alias = "empty")]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: gstreamer::Sample::builder().build(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer(&self) -> Option<&BufferRef> {
|
||||||
|
self.inner.buffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn caps(&self) -> Option<&gstreamer::CapsRef> {
|
||||||
|
self.inner.caps()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn info(&self) -> Option<&gstreamer::StructureRef> {
|
||||||
|
self.inner.info()
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn set_buffer(&mut self) {
|
||||||
|
// self.inner.set_buffer(None);
|
||||||
|
// }
|
||||||
|
}
|
||||||
2
gst/src/wgpu.rs
Normal file
2
gst/src/wgpu.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
|
||||||
145
gst/src/wrapper.rs
Normal file
145
gst/src/wrapper.rs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
pub trait GstWrapper {
|
||||||
|
type GstType: glib::prelude::ObjectType;
|
||||||
|
fn from_gst(gst: Self::GstType) -> Self;
|
||||||
|
// fn into_gst(self) -> Self::GstType;
|
||||||
|
fn as_gst_ref(&self) -> &Self::GstType;
|
||||||
|
fn from_gst_ref(gst: &Self::GstType) -> &Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! wrap_gst {
|
||||||
|
($name:ident) => {
|
||||||
|
$crate::wrap_gst!($name, gstreamer::$name);
|
||||||
|
};
|
||||||
|
($name:ident, $inner:ty) => {
|
||||||
|
$crate::wrap_gst!(core $name, $inner);
|
||||||
|
$crate::wrap_gst!($name, $inner, into_inner);
|
||||||
|
};
|
||||||
|
($name:ident, $inner:ty, skip_inner) => {
|
||||||
|
$crate::wrap_gst!(core $name, $inner);
|
||||||
|
};
|
||||||
|
|
||||||
|
(core $name:ident, $inner:ty) => {
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct $name {
|
||||||
|
pub(crate) inner: $inner,
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl From<$name> for $inner {
|
||||||
|
// fn from(wrapper: $name) -> Self {
|
||||||
|
// wrapper.into_inner()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
impl $name {
|
||||||
|
pub fn into_inner(self) -> $inner {
|
||||||
|
self.inner.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl $crate::wrapper::GstWrapper for $name {
|
||||||
|
type GstType = $inner;
|
||||||
|
|
||||||
|
fn from_gst(gst: Self::GstType) -> Self {
|
||||||
|
Self { inner: gst }
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn into_gst(self) -> Self::GstType {
|
||||||
|
// self.inner.clone()
|
||||||
|
// }
|
||||||
|
|
||||||
|
fn as_gst_ref(&self) -> &Self::GstType {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_gst_ref(gst: &Self::GstType) -> &Self {
|
||||||
|
unsafe { &*(gst as *const Self::GstType as *const Self) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChildOf<$name> for $name {
|
||||||
|
fn upcast_ref(&self) -> &$name {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($name:ident, $inner:ty, into_inner) => {
|
||||||
|
impl From<$inner> for $name {
|
||||||
|
fn from(inner: $inner) -> Self {
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait for types that can be upcasted to type T.
|
||||||
|
pub trait ChildOf<T> {
|
||||||
|
fn upcast_ref(&self) -> &T;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! parent_child {
|
||||||
|
($parent:ty, $child:ty) => {
|
||||||
|
impl ChildOf<$parent> for $child
|
||||||
|
where
|
||||||
|
$child: GstWrapper,
|
||||||
|
$parent: GstWrapper,
|
||||||
|
{
|
||||||
|
fn upcast_ref(&self) -> &$parent {
|
||||||
|
let upcasted = self.inner.upcast_ref::<<$parent as GstWrapper>::GstType>();
|
||||||
|
unsafe { &*(upcasted as *const <$parent as GstWrapper>::GstType as *const $parent) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($parent:ty, $child:ty, downcast) => {
|
||||||
|
impl ChildOf<$parent> for $child
|
||||||
|
where
|
||||||
|
$child: GstWrapper,
|
||||||
|
$parent: GstWrapper,
|
||||||
|
{
|
||||||
|
fn upcast_ref(&self) -> &$parent {
|
||||||
|
let downcasted = self
|
||||||
|
.inner
|
||||||
|
.downcast_ref::<<$parent as GstWrapper>::GstType>()
|
||||||
|
.expect(
|
||||||
|
format!(
|
||||||
|
"BUG: Failed to downcast GStreamer type from child {} to parent {}",
|
||||||
|
stringify!($child),
|
||||||
|
stringify!($parent)
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
|
unsafe {
|
||||||
|
&*(downcasted as *const <$parent as GstWrapper>::GstType as *const $parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}; // ($parent:ty, $child:ty, deref) => {
|
||||||
|
// $crate::parent_child!($parent, $child);
|
||||||
|
// $crate::parent_child!($parent, $child, __deref);
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// ($parent:ty, $child:ty, downcast, deref) => {
|
||||||
|
// $crate::parent_child!($parent, $child, downcast);
|
||||||
|
// $crate::parent_child!($parent, $child, __deref);
|
||||||
|
// };
|
||||||
|
// ($parent:ty, $child:ty, deref, downcast) => {
|
||||||
|
// $crate::parent_child!($parent, $child, downcast);
|
||||||
|
// $crate::parent_child!($parent, $child, __deref);
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// ($parent:ty, $child:ty, __deref) => {
|
||||||
|
// impl core::ops::Deref for $child
|
||||||
|
// where
|
||||||
|
// $child: GstWrapper,
|
||||||
|
// $parent: GstWrapper,
|
||||||
|
// {
|
||||||
|
// type Target = $parent;
|
||||||
|
//
|
||||||
|
// fn deref(&self) -> &Self::Target {
|
||||||
|
// self.upcast_ref()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
}
|
||||||
8
jello-types/Cargo.toml
Normal file
8
jello-types/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "jello-types"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
uuid = { version = "1.18.1", features = ["serde"] }
|
||||||
6
jello-types/src/lib.rs
Normal file
6
jello-types/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct User {
|
||||||
|
id: uuid::Uuid,
|
||||||
|
name: Option<String>,
|
||||||
|
primary_image_tag: Option<String>,
|
||||||
|
}
|
||||||
12
justfile
12
justfile
@@ -1,6 +1,18 @@
|
|||||||
|
jello:
|
||||||
|
cargo r -r -- -vv
|
||||||
|
# iced-video:
|
||||||
|
# cd crates/iced-video && cargo run --release --example minimal
|
||||||
typegen:
|
typegen:
|
||||||
@echo "Generating jellyfin type definitions..."
|
@echo "Generating jellyfin type definitions..."
|
||||||
cd typegen && cargo run
|
cd typegen && cargo run
|
||||||
cp typegen/jellyfin.rs api/src/jellyfin.rs
|
cp typegen/jellyfin.rs api/src/jellyfin.rs
|
||||||
rm typegen/jellyfin.rs
|
rm typegen/jellyfin.rs
|
||||||
|
|
||||||
|
hdrtest:
|
||||||
|
GST_DEBUG=3 gst-launch-1.0 playbin3 uri=https://jellyfin.tsuba.darksailor.dev/Items/6010382cf25273e624d305907010d773/Download?api_key=036c140222464878862231ef66a2bc9c video-sink="videoconvert ! video/x-raw,format=(string)RGB10A2_LE ! fakesink"
|
||||||
|
|
||||||
|
codec:
|
||||||
|
GST_DEBUG=3 gst-discoverer-1.0 https://jellyfin.tsuba.darksailor.dev/Items/6010382cf25273e624d305907010d773/Download?api_key=036c140222464878862231ef66a2bc9c
|
||||||
|
ffprobe:
|
||||||
|
ffprobe -v error -show_format -show_streams "https://jellyfin.tsuba.darksailor.dev/Items/6010382cf25273e624d305907010d773/Download?api_key=036c140222464878862231ef66a2bc9c" | grep pix_fmt
|
||||||
|
|
||||||
|
|||||||
66
src/cli.rs
66
src/cli.rs
@@ -1,36 +1,38 @@
|
|||||||
#[derive(Debug, clap::Parser)]
|
#[derive(Debug, clap::Parser)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
#[clap(subcommand)]
|
// #[clap(subcommand)]
|
||||||
pub cmd: SubCommand,
|
// pub cmd: SubCommand,
|
||||||
|
#[command(flatten)]
|
||||||
|
pub verbosity: clap_verbosity_flag::Verbosity,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, clap::Subcommand)]
|
// #[derive(Debug, clap::Subcommand)]
|
||||||
pub enum SubCommand {
|
// pub enum SubCommand {
|
||||||
#[clap(name = "add")]
|
// #[clap(name = "add")]
|
||||||
Add(Add),
|
// Add(Add),
|
||||||
#[clap(name = "list")]
|
// #[clap(name = "list")]
|
||||||
List(List),
|
// List(List),
|
||||||
#[clap(name = "completions")]
|
// #[clap(name = "completions")]
|
||||||
Completions { shell: clap_complete::Shell },
|
// Completions { shell: clap_complete::Shell },
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
#[derive(Debug, clap::Args)]
|
// #[derive(Debug, clap::Args)]
|
||||||
pub struct Add {
|
// pub struct Add {
|
||||||
#[clap(short, long)]
|
// #[clap(short, long)]
|
||||||
pub name: String,
|
// pub name: String,
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
#[derive(Debug, clap::Args)]
|
// #[derive(Debug, clap::Args)]
|
||||||
pub struct List {}
|
// pub struct List {}
|
||||||
|
//
|
||||||
impl Cli {
|
// impl Cli {
|
||||||
pub fn completions(shell: clap_complete::Shell) {
|
// pub fn completions(shell: clap_complete::Shell) {
|
||||||
let mut command = <Cli as clap::CommandFactory>::command();
|
// let mut command = <Cli as clap::CommandFactory>::command();
|
||||||
clap_complete::generate(
|
// clap_complete::generate(
|
||||||
shell,
|
// shell,
|
||||||
&mut command,
|
// &mut command,
|
||||||
env!("CARGO_BIN_NAME"),
|
// env!("CARGO_BIN_NAME"),
|
||||||
&mut std::io::stdout(),
|
// &mut std::io::stdout(),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
56
src/main.rs
56
src/main.rs
@@ -1,52 +1,16 @@
|
|||||||
|
mod cli;
|
||||||
mod errors;
|
mod errors;
|
||||||
use api::{JellyfinClient, JellyfinConfig};
|
use api::JellyfinConfig;
|
||||||
use errors::*;
|
use errors::*;
|
||||||
|
|
||||||
fn jellyfin_config_try() -> Result<JellyfinConfig> {
|
|
||||||
let file = std::fs::read("config.toml").change_context(Error)?;
|
|
||||||
let config: JellyfinConfig = toml::from_slice(&file)
|
|
||||||
.change_context(Error)
|
|
||||||
.attach("Failed to parse Jellyfin Config")?;
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn jellyfin_config() -> JellyfinConfig {
|
|
||||||
jellyfin_config_try().unwrap_or_else(|err| {
|
|
||||||
eprintln!("Error loading Jellyfin configuration: {:?}", err);
|
|
||||||
std::process::exit(1);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
tracing_subscriber::fmt::init();
|
color_backtrace::install();
|
||||||
ui_iced::ui(jellyfin_config).change_context(Error)?;
|
let args = <cli::Cli as clap::Parser>::parse();
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(args.verbosity)
|
||||||
|
.with_file(true)
|
||||||
|
.with_line_number(true)
|
||||||
|
.init();
|
||||||
|
ui_iced::ui().change_context(Error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[tokio::main]
|
|
||||||
// pub async fn main() -> Result<()> {
|
|
||||||
// dotenvy::dotenv()
|
|
||||||
// .change_context(Error)
|
|
||||||
// .inspect_err(|err| {
|
|
||||||
// eprintln!("Failed to load .env file: {}", err);
|
|
||||||
// })
|
|
||||||
// .ok();
|
|
||||||
// let config = JellyfinConfig::new(
|
|
||||||
// std::env::var("JELLYFIN_USERNAME").change_context(Error)?,
|
|
||||||
// std::env::var("JELLYFIN_PASSWORD").change_context(Error)?,
|
|
||||||
// std::env::var("JELLYFIN_SERVER_URL").change_context(Error)?,
|
|
||||||
// "jello".to_string(),
|
|
||||||
// );
|
|
||||||
// let mut jellyfin = api::JellyfinClient::new(config);
|
|
||||||
// jellyfin
|
|
||||||
// .authenticate_with_cached_token(".session")
|
|
||||||
// .await
|
|
||||||
// .change_context(Error)?;
|
|
||||||
//
|
|
||||||
// #[cfg(feature = "iced")]
|
|
||||||
// ui_iced::ui(jellyfin);
|
|
||||||
// #[cfg(feature = "gpui")]
|
|
||||||
// ui_gpui::ui(jellyfin);
|
|
||||||
//
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
|
|||||||
14
store/Cargo.toml
Normal file
14
store/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "store"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bson = { version = "3.1.0", features = ["serde"] }
|
||||||
|
futures = "0.3.31"
|
||||||
|
parking_lot = "0.12.5"
|
||||||
|
redb = { version = "3.1.0", features = ["uuid"] }
|
||||||
|
secrecy = "0.10.3"
|
||||||
|
serde = "1.0.228"
|
||||||
|
tokio = { version = "1.48.0", features = ["rt"] }
|
||||||
|
uuid = { version = "1.18.1", features = ["v4"] }
|
||||||
10
store/src/lib.rs
Normal file
10
store/src/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub struct ApiKey {
|
||||||
|
inner: secrecy::SecretBox<String>,
|
||||||
|
}
|
||||||
|
pub struct SecretStore {
|
||||||
|
api_keys: BTreeMap<Uuid, ApiKey>,
|
||||||
|
}
|
||||||
225
store/src/redb.rs
Normal file
225
store/src/redb.rs
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
// use std::{
|
||||||
|
// borrow::Borrow,
|
||||||
|
// collections::VecDeque,
|
||||||
|
// marker::PhantomData,
|
||||||
|
// path::Path,
|
||||||
|
// sync::{Arc, RwLock, atomic::AtomicBool},
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// use futures::task::AtomicWaker;
|
||||||
|
// use redb::{Error, Key, ReadableDatabase, TableDefinition, Value};
|
||||||
|
// use serde::{Serialize, de::DeserializeOwned};
|
||||||
|
//
|
||||||
|
// const USERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("users");
|
||||||
|
// const SERVERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("servers");
|
||||||
|
// const SETTINGS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("settings");
|
||||||
|
//
|
||||||
|
// #[derive(Debug)]
|
||||||
|
// pub struct TableInner<T> {
|
||||||
|
// db: Arc<T>,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// impl<T> Clone for TableInner<T> {
|
||||||
|
// fn clone(&self) -> Self {
|
||||||
|
// Self {
|
||||||
|
// db: Arc::clone(&self.db),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// impl<T> TableInner<T> {
|
||||||
|
// fn new(db: Arc<T>) -> Self {
|
||||||
|
// Self { db }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// impl TableInner<DatabaseHandle> {
|
||||||
|
// async fn get<'a, K: Key, V: Serialize + DeserializeOwned>(
|
||||||
|
// &self,
|
||||||
|
// table: TableDefinition<'static, K, Vec<u8>>,
|
||||||
|
// key: impl Borrow<K::SelfType<'a>>,
|
||||||
|
// ) -> Result<Option<V>> {
|
||||||
|
// let db: &redb::Database = &self.db.as_ref().database;
|
||||||
|
// let db_reader = db.begin_read()?;
|
||||||
|
// let table = db_reader.open_table(table)?;
|
||||||
|
// table
|
||||||
|
// .get(key)?
|
||||||
|
// .map(|value| bson::deserialize_from_slice(&value.value()))
|
||||||
|
// .transpose()
|
||||||
|
// .map_err(|e| redb::Error::Io(std::io::Error::other(e)))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// async fn insert<
|
||||||
|
// 'a,
|
||||||
|
// 'b,
|
||||||
|
// K: Key + Send + Sync,
|
||||||
|
// V: Serialize + DeserializeOwned + Send + Sync + 'a,
|
||||||
|
// >(
|
||||||
|
// &'b self,
|
||||||
|
// table: TableDefinition<'static, K, Vec<u8>>,
|
||||||
|
// key: impl Borrow<K::SelfType<'a>> + Send + 'b,
|
||||||
|
// value: V,
|
||||||
|
// ) -> Result<Option<V>> {
|
||||||
|
// let db: &redb::Database = &self.db.as_ref().database;
|
||||||
|
// // self.db
|
||||||
|
// // .writing
|
||||||
|
// // .store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
//
|
||||||
|
// // let out = tokio::task::spawn_blocking(move || -> Result<Option<V>>
|
||||||
|
//
|
||||||
|
// let out = tokio::task::spawn_blocking(|| -> Result<Option<V>> {
|
||||||
|
// let db_writer = db.begin_write()?;
|
||||||
|
// let out = {
|
||||||
|
// let mut table = db_writer.open_table(table)?;
|
||||||
|
// let serialized_value = bson::serialize_to_vec(&value)
|
||||||
|
// .map_err(|e| redb::Error::Io(std::io::Error::other(e)))?;
|
||||||
|
// let previous = table.insert(key, &serialized_value)?;
|
||||||
|
// let out = previous
|
||||||
|
// .map(|value| bson::deserialize_from_slice(&value.value()))
|
||||||
|
// .transpose()
|
||||||
|
// .map_err(|e| redb::Error::Io(std::io::Error::other(e)));
|
||||||
|
// out
|
||||||
|
// };
|
||||||
|
// db_writer.commit()?;
|
||||||
|
// out
|
||||||
|
// })
|
||||||
|
// .await
|
||||||
|
// .expect("Task panicked");
|
||||||
|
//
|
||||||
|
// out
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // impl<K: Key, V: Serialize + DeserializeOwned> Table<K, V> for TableInner {
|
||||||
|
// // async fn get(&self, key: K) -> Result<Option<Value>> {}
|
||||||
|
// // async fn insert(&self, key: K, value: V) -> Result<Option<Value>> {}
|
||||||
|
// // async fn modify(&self, key: K, v: FnOnce(V) -> V) -> Result<bool> {}
|
||||||
|
// // async fn remove(&self, key: K) -> Result<Option<Value>> {}
|
||||||
|
// // }
|
||||||
|
//
|
||||||
|
// #[derive(Debug)]
|
||||||
|
// pub struct Users<T>(TableInner<T>);
|
||||||
|
//
|
||||||
|
// impl<T> Clone for Users<T> {
|
||||||
|
// fn clone(&self) -> Self {
|
||||||
|
// Self(self.0.clone())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// impl<T> Users<T> {
|
||||||
|
// const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = USERS;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[derive(Debug)]
|
||||||
|
// pub struct Servers<T>(TableInner<T>);
|
||||||
|
// impl<T> Clone for Servers<T> {
|
||||||
|
// fn clone(&self) -> Self {
|
||||||
|
// Self(self.0.clone())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// impl<T> Servers<T> {
|
||||||
|
// const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SERVERS;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[derive(Debug)]
|
||||||
|
// pub struct Settings<T>(TableInner<T>);
|
||||||
|
// impl<T> Clone for Settings<T> {
|
||||||
|
// fn clone(&self) -> Self {
|
||||||
|
// Self(self.0.clone())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// impl<T> Settings<T> {
|
||||||
|
// const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SETTINGS;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[derive(Debug, Clone)]
|
||||||
|
// pub struct Database {
|
||||||
|
// users: Users<DatabaseHandle>,
|
||||||
|
// servers: Servers<DatabaseHandle>,
|
||||||
|
// settings: Settings<DatabaseHandle>,
|
||||||
|
// handle: Arc<DatabaseHandle>,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[derive(Debug)]
|
||||||
|
// pub struct DatabaseHandle {
|
||||||
|
// database: redb::Database,
|
||||||
|
// writing: AtomicBool,
|
||||||
|
// wakers: RwLock<VecDeque<AtomicWaker>>,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[derive(Debug)]
|
||||||
|
// pub struct DatabaseWriterGuard<'a> {
|
||||||
|
// handle: &'a DatabaseHandle,
|
||||||
|
// dropper: Arc<AtomicBool>,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // impl Drop for DatabaseWriterGuard<'_> {
|
||||||
|
// // fn drop(&mut self) {
|
||||||
|
// // self.handle
|
||||||
|
// // .writing
|
||||||
|
// // .store(false, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
// // let is_panicking = std::thread::panicking();
|
||||||
|
// // let Ok(writer) = self.handle.wakers.write() else {
|
||||||
|
// // if is_panicking {
|
||||||
|
// // return;
|
||||||
|
// // } else {
|
||||||
|
// // panic!("Wakers lock poisoned");
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
// // if let Some(waker) = (self.handle.wakers.write()).pop() {
|
||||||
|
// // waker.wake();
|
||||||
|
// // };
|
||||||
|
// // // let mut wakers = self.handle.wakers.write().expect();
|
||||||
|
// // // if let Some(waker) = self.handle.wakers.write().expect("Wakers lock poisoned").pop_front() {
|
||||||
|
// // // waker.wake();
|
||||||
|
// // // }
|
||||||
|
// // // while let Some(waker) = wakers.pop_front() {
|
||||||
|
// // // waker.wake();
|
||||||
|
// // // }
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
//
|
||||||
|
// type Result<O, E = redb::Error> = core::result::Result<O, E>;
|
||||||
|
//
|
||||||
|
// pub trait Table<K: Key> {
|
||||||
|
// fn insert<V: Serialize + DeserializeOwned>(
|
||||||
|
// &self,
|
||||||
|
// key: K,
|
||||||
|
// value: V,
|
||||||
|
// ) -> impl Future<Output = Result<Option<V>>> + Send;
|
||||||
|
// fn modify<V: Serialize + DeserializeOwned, O: Serialize + DeserializeOwned>(
|
||||||
|
// &self,
|
||||||
|
// key: K,
|
||||||
|
// v: impl FnOnce(V) -> O,
|
||||||
|
// ) -> impl Future<Output = Result<bool>> + Send;
|
||||||
|
// fn remove<V: Serialize + DeserializeOwned>(
|
||||||
|
// &self,
|
||||||
|
// key: K,
|
||||||
|
// ) -> impl Future<Output = Result<Option<V>>> + Send;
|
||||||
|
// fn get<V: Serialize + DeserializeOwned>(
|
||||||
|
// &self,
|
||||||
|
// key: K,
|
||||||
|
// ) -> impl Future<Output = Result<Option<V>>> + Send;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// impl Database {
|
||||||
|
// pub fn create(path: impl AsRef<Path>) -> Result<Self, Error> {
|
||||||
|
// let writing = AtomicBool::new(false);
|
||||||
|
// let wakers = RwLock::new(VecDeque::new());
|
||||||
|
// let db = redb::Database::create(path)?;
|
||||||
|
// let db = Arc::new(DatabaseHandle {
|
||||||
|
// database: db,
|
||||||
|
// writing,
|
||||||
|
// wakers,
|
||||||
|
// });
|
||||||
|
// let table_inner = TableInner::new(Arc::clone(&db));
|
||||||
|
// let users = Users(table_inner.clone());
|
||||||
|
// let servers = Servers(table_inner.clone());
|
||||||
|
// let settings = Settings(table_inner.clone());
|
||||||
|
// Ok(Self {
|
||||||
|
// servers,
|
||||||
|
// users,
|
||||||
|
// settings,
|
||||||
|
// handle: db,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
1
store/src/sqlite.rs
Normal file
1
store/src/sqlite.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
1
store/src/toml.rs
Normal file
1
store/src/toml.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -184,7 +184,6 @@ fn main() {
|
|||||||
quote::quote! {
|
quote::quote! {
|
||||||
#[doc = #desc]
|
#[doc = #desc]
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum #key {
|
pub enum #key {
|
||||||
#(#variants),*
|
#(#variants),*
|
||||||
}
|
}
|
||||||
@@ -192,7 +191,6 @@ fn main() {
|
|||||||
} else {
|
} else {
|
||||||
quote::quote! {
|
quote::quote! {
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub enum #key {
|
pub enum #key {
|
||||||
#(#variants),*
|
#(#variants),*
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,262 +1,262 @@
|
|||||||
use ::tap::*;
|
// use ::tap::*;
|
||||||
|
//
|
||||||
use std::{collections::BTreeMap, sync::Arc};
|
// use std::{collections::BTreeMap, sync::Arc};
|
||||||
|
//
|
||||||
use gpui::{
|
// use gpui::{
|
||||||
App, Application, Bounds, ClickEvent, Context, ImageId, ImageSource, RenderImage, Resource,
|
// App, Application, Bounds, ClickEvent, Context, ImageId, ImageSource, RenderImage, Resource,
|
||||||
SharedString, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size,
|
// SharedString, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size,
|
||||||
};
|
// };
|
||||||
|
//
|
||||||
#[derive(Clone, Debug)]
|
// #[derive(Clone, Debug)]
|
||||||
pub struct AppState {
|
// pub struct AppState {
|
||||||
pub title: SharedString,
|
// pub title: SharedString,
|
||||||
pub items: BTreeMap<SharedString, Item>,
|
// pub items: BTreeMap<SharedString, Item>,
|
||||||
pub item_ids: BTreeMap<usize, SharedString>,
|
// pub item_ids: BTreeMap<usize, SharedString>,
|
||||||
pub current_item: Option<SharedString>,
|
// pub current_item: Option<SharedString>,
|
||||||
pub errors: Vec<String>,
|
// pub errors: Vec<String>,
|
||||||
pub jellyfin_client: api::JellyfinClient,
|
// pub jellyfin_client: api::JellyfinClient,
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
#[derive(Clone, Debug)]
|
// #[derive(Clone, Debug)]
|
||||||
pub struct Item {
|
// pub struct Item {
|
||||||
pub id: SharedString,
|
// pub id: SharedString,
|
||||||
pub name: SharedString,
|
// pub name: SharedString,
|
||||||
pub item_type: SharedString,
|
// pub item_type: SharedString,
|
||||||
pub media_type: SharedString,
|
// pub media_type: SharedString,
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
impl Render for AppState {
|
// impl Render for AppState {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
// fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
div()
|
// div()
|
||||||
.flex()
|
// .flex()
|
||||||
.flex_col()
|
// .flex_col()
|
||||||
.size_full()
|
// .size_full()
|
||||||
.justify_center()
|
// .justify_center()
|
||||||
.text_color(rgb(0xffffff))
|
// .text_color(rgb(0xffffff))
|
||||||
.child(Self::header())
|
// .child(Self::header())
|
||||||
.child(Self::body(self, window, cx))
|
// .child(Self::body(self, window, cx))
|
||||||
.child(Self::footer())
|
// .child(Self::footer())
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
actions!(jello_actions, [OpenItem, OnLoadItem, MouseDownEvent]);
|
// actions!(jello_actions, [OpenItem, OnLoadItem, MouseDownEvent]);
|
||||||
|
//
|
||||||
impl AppState {
|
// impl AppState {
|
||||||
fn new(title: impl AsRef<str>, jellyfin_client: api::JellyfinClient) -> Self {
|
// fn new(title: impl AsRef<str>, jellyfin_client: api::JellyfinClient) -> Self {
|
||||||
AppState {
|
// AppState {
|
||||||
title: SharedString::new(title.as_ref()),
|
// title: SharedString::new(title.as_ref()),
|
||||||
items: BTreeMap::new(),
|
// items: BTreeMap::new(),
|
||||||
item_ids: BTreeMap::new(),
|
// item_ids: BTreeMap::new(),
|
||||||
current_item: None,
|
// current_item: None,
|
||||||
errors: Vec::new(),
|
// errors: Vec::new(),
|
||||||
jellyfin_client,
|
// jellyfin_client,
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// fn on_mouse_down(
|
// // fn on_mouse_down(
|
||||||
// &mut self,
|
// // &mut self,
|
||||||
// event: &MouseDownEvent,
|
// // event: &MouseDownEvent,
|
||||||
// window: &mut Window,
|
// // window: &mut Window,
|
||||||
// cx: &mut Context<Self>,
|
// // cx: &mut Context<Self>,
|
||||||
// ) {
|
// // ) {
|
||||||
// // Handle mouse down event
|
// // // Handle mouse down event
|
||||||
// }
|
// // }
|
||||||
|
//
|
||||||
fn load_item(id: usize) -> impl Fn(&mut Self, &ClickEvent, &mut Window, &mut Context<Self>) {
|
// fn load_item(id: usize) -> impl Fn(&mut Self, &ClickEvent, &mut Window, &mut Context<Self>) {
|
||||||
move |state: &mut Self, event: &ClickEvent, window: &mut Window, cx: &mut Context<Self>| {
|
// move |state: &mut Self, event: &ClickEvent, window: &mut Window, cx: &mut Context<Self>| {
|
||||||
let item_id = id;
|
// let item_id = id;
|
||||||
cx.spawn(async move |entity, app| {
|
// cx.spawn(async move |entity, app| {
|
||||||
tracing::info!("Loading item with id: {}", item_id);
|
// tracing::info!("Loading item with id: {}", item_id);
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
fn hover_item(id: usize) -> impl Fn(&mut Self, &bool, &mut Window, &mut Context<Self>) {
|
// fn hover_item(id: usize) -> impl Fn(&mut Self, &bool, &mut Window, &mut Context<Self>) {
|
||||||
move |state: &mut Self, item: &bool, window: &mut Window, cx: &mut Context<Self>| {
|
// move |state: &mut Self, item: &bool, window: &mut Window, cx: &mut Context<Self>| {
|
||||||
dbg!("Hovering over item: {:?}", id);
|
// dbg!("Hovering over item: {:?}", id);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
fn header() -> impl IntoElement {
|
// fn header() -> impl IntoElement {
|
||||||
div()
|
// div()
|
||||||
.flex()
|
// .flex()
|
||||||
.flex_row()
|
// .flex_row()
|
||||||
.w_full()
|
// .w_full()
|
||||||
.justify_end()
|
// .justify_end()
|
||||||
.h_20()
|
// .h_20()
|
||||||
.border_10()
|
// .border_10()
|
||||||
.bg(rgb(0x333333))
|
// .bg(rgb(0x333333))
|
||||||
.child(Self::button("Refresh"))
|
// .child(Self::button("Refresh"))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
fn footer() -> impl IntoElement {
|
// fn footer() -> impl IntoElement {
|
||||||
div().flex().flex_row().w_full().h_20().bg(rgb(0x333333))
|
// div().flex().flex_row().w_full().h_20().bg(rgb(0x333333))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
fn body(&mut self, window: &mut Window, cx: &mut Context<AppState>) -> impl IntoElement {
|
// fn body(&mut self, window: &mut Window, cx: &mut Context<AppState>) -> impl IntoElement {
|
||||||
div()
|
// div()
|
||||||
.flex()
|
// .flex()
|
||||||
.flex_row()
|
// .flex_row()
|
||||||
.size_full()
|
// .size_full()
|
||||||
.child(Self::content(self, window, cx))
|
// .child(Self::content(self, window, cx))
|
||||||
.child(Self::sidebar(self, window, cx))
|
// .child(Self::sidebar(self, window, cx))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
fn button(label: &str) -> impl IntoElement {
|
// fn button(label: &str) -> impl IntoElement {
|
||||||
div()
|
// div()
|
||||||
.flex()
|
// .flex()
|
||||||
.justify_center()
|
// .justify_center()
|
||||||
.items_center()
|
// .items_center()
|
||||||
.bg(rgb(0xff00ff))
|
// .bg(rgb(0xff00ff))
|
||||||
.text_color(rgb(0xffffff))
|
// .text_color(rgb(0xffffff))
|
||||||
.border_5()
|
// .border_5()
|
||||||
.rounded_lg()
|
// .rounded_lg()
|
||||||
.child(label.to_string())
|
// .child(label.to_string())
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
fn content(&mut self, window: &mut Window, cx: &mut Context<AppState>) -> impl IntoElement {
|
// fn content(&mut self, window: &mut Window, cx: &mut Context<AppState>) -> impl IntoElement {
|
||||||
div()
|
// div()
|
||||||
.debug_below()
|
// .debug_below()
|
||||||
.w_3_4()
|
// .w_3_4()
|
||||||
// .flex()
|
// // .flex()
|
||||||
// .flex_wrap()
|
// // .flex_wrap()
|
||||||
.bg(rgb(0x111111))
|
// .bg(rgb(0x111111))
|
||||||
.justify_start()
|
// .justify_start()
|
||||||
.items_start()
|
// .items_start()
|
||||||
.overflow_hidden()
|
// .overflow_hidden()
|
||||||
.child(
|
// .child(
|
||||||
div()
|
// div()
|
||||||
.size_full()
|
// .size_full()
|
||||||
.flex()
|
// .flex()
|
||||||
.flex_wrap()
|
// .flex_wrap()
|
||||||
.justify_start()
|
// .justify_start()
|
||||||
.items_start()
|
// .items_start()
|
||||||
.content_start()
|
// .content_start()
|
||||||
.gap_y_10()
|
// .gap_y_10()
|
||||||
.gap_x_10()
|
// .gap_x_10()
|
||||||
.border_t_10()
|
// .border_t_10()
|
||||||
.p_5()
|
// .p_5()
|
||||||
.child(Self::card(cx, 1))
|
// .child(Self::card(cx, 1))
|
||||||
.child(Self::card(cx, 2))
|
// .child(Self::card(cx, 2))
|
||||||
.child(Self::card(cx, 3))
|
// .child(Self::card(cx, 3))
|
||||||
.child(Self::card(cx, 4))
|
// .child(Self::card(cx, 4))
|
||||||
.child(Self::card(cx, 5))
|
// .child(Self::card(cx, 5))
|
||||||
.child(Self::card(cx, 6))
|
// .child(Self::card(cx, 6))
|
||||||
.child(Self::card(cx, 7))
|
// .child(Self::card(cx, 7))
|
||||||
.child(Self::card(cx, 8))
|
// .child(Self::card(cx, 8))
|
||||||
.child(Self::card(cx, 9)),
|
// .child(Self::card(cx, 9)),
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
fn sidebar(&mut self, window: &mut Window, cx: &mut Context<AppState>) -> impl IntoElement {
|
// fn sidebar(&mut self, window: &mut Window, cx: &mut Context<AppState>) -> impl IntoElement {
|
||||||
div()
|
// div()
|
||||||
.flex()
|
// .flex()
|
||||||
.flex_col()
|
// .flex_col()
|
||||||
.w_1_4()
|
// .w_1_4()
|
||||||
.min_w_1_6()
|
// .min_w_1_6()
|
||||||
.bg(rgb(0x222222))
|
// .bg(rgb(0x222222))
|
||||||
.child(div().size_full().bg(gpui::yellow()))
|
// .child(div().size_full().bg(gpui::yellow()))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
fn card(cx: &mut Context<AppState>, number: usize) -> impl IntoElement {
|
// fn card(cx: &mut Context<AppState>, number: usize) -> impl IntoElement {
|
||||||
div()
|
// div()
|
||||||
.id(number)
|
// .id(number)
|
||||||
.on_click(cx.listener(Self::load_item(number)))
|
// .on_click(cx.listener(Self::load_item(number)))
|
||||||
.on_hover(cx.listener(Self::hover_item(number)))
|
// .on_hover(cx.listener(Self::hover_item(number)))
|
||||||
.flex()
|
// .flex()
|
||||||
.flex_col()
|
// .flex_col()
|
||||||
.w_48()
|
// .w_48()
|
||||||
.h_64()
|
// .h_64()
|
||||||
.p_10()
|
// .p_10()
|
||||||
.bg(rgb(0xff00ff))
|
// .bg(rgb(0xff00ff))
|
||||||
.rounded_lg()
|
// .rounded_lg()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
pub fn ui(jellyfin_client: api::JellyfinClient) {
|
// pub fn ui(jellyfin_client: api::JellyfinClient) {
|
||||||
Application::new().run(|cx: &mut App| {
|
// Application::new().run(|cx: &mut App| {
|
||||||
let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx);
|
// let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx);
|
||||||
cx.open_window(
|
// cx.open_window(
|
||||||
WindowOptions {
|
// WindowOptions {
|
||||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
// window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||||
..Default::default()
|
// ..Default::default()
|
||||||
},
|
// },
|
||||||
|_, cx| cx.new(|_| AppState::new("Jello Media Browser", jellyfin_client)),
|
// |_, cx| cx.new(|_| AppState::new("Jello Media Browser", jellyfin_client)),
|
||||||
)
|
// )
|
||||||
.expect("Failed to open window");
|
// .expect("Failed to open window");
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
#[derive(Clone, Debug)]
|
// #[derive(Clone, Debug)]
|
||||||
pub struct Card {
|
// pub struct Card {
|
||||||
pub id: usize,
|
// pub id: usize,
|
||||||
pub title: SharedString,
|
// pub title: SharedString,
|
||||||
pub description: SharedString,
|
// pub description: SharedString,
|
||||||
pub image: SharedString,
|
// pub image: SharedString,
|
||||||
pub image_blurhash: BlurHash,
|
// pub image_blurhash: BlurHash,
|
||||||
pub media_type: SharedString,
|
// pub media_type: SharedString,
|
||||||
pub loading: bool,
|
// pub loading: bool,
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
impl Render for Card {
|
// impl Render for Card {
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
// fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
div()
|
// div()
|
||||||
.id(self.id)
|
// .id(self.id)
|
||||||
.flex()
|
// .flex()
|
||||||
.flex_col()
|
// .flex_col()
|
||||||
.w_48()
|
// .w_48()
|
||||||
.h_64()
|
// .h_64()
|
||||||
.p_10()
|
// .p_10()
|
||||||
.bg(rgb(0xff00ff))
|
// .bg(rgb(0xff00ff))
|
||||||
.rounded_lg()
|
// .rounded_lg()
|
||||||
.pipe(|card| {
|
// .pipe(|card| {
|
||||||
if self.loading {
|
// if self.loading {
|
||||||
card.child(self.image_blurhash.clone())
|
// card.child(self.image_blurhash.clone())
|
||||||
} else {
|
// } else {
|
||||||
card.child(gpui::img(self.image.clone()))
|
// card.child(gpui::img(self.image.clone()))
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
#[derive(Clone, Debug)]
|
// #[derive(Clone, Debug)]
|
||||||
pub struct BlurHash {
|
// pub struct BlurHash {
|
||||||
pub id: ImageId,
|
// pub id: ImageId,
|
||||||
pub data: Arc<RenderImage>,
|
// pub data: Arc<RenderImage>,
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
impl BlurHash {
|
// impl BlurHash {
|
||||||
pub fn new(
|
// pub fn new(
|
||||||
data: impl AsRef<str>,
|
// data: impl AsRef<str>,
|
||||||
width: u32,
|
// width: u32,
|
||||||
height: u32,
|
// height: u32,
|
||||||
punch: f32,
|
// punch: f32,
|
||||||
) -> Result<Self, error_stack::Report<crate::Error>> {
|
// ) -> Result<Self, error_stack::Report<crate::Error>> {
|
||||||
use error_stack::ResultExt;
|
// use error_stack::ResultExt;
|
||||||
let decoded =
|
// let decoded =
|
||||||
blurhash::decode(data.as_ref(), width, height, punch).change_context(crate::Error)?;
|
// blurhash::decode(data.as_ref(), width, height, punch).change_context(crate::Error)?;
|
||||||
let buffer = image::RgbaImage::from_raw(width, height, decoded)
|
// let buffer = image::RgbaImage::from_raw(width, height, decoded)
|
||||||
.ok_or(crate::Error)
|
// .ok_or(crate::Error)
|
||||||
.attach("Failed to convert")?;
|
// .attach("Failed to convert")?;
|
||||||
let frame = image::Frame::new(buffer);
|
// let frame = image::Frame::new(buffer);
|
||||||
let render_image = RenderImage::new([frame]);
|
// let render_image = RenderImage::new([frame]);
|
||||||
Ok(Self {
|
// Ok(Self {
|
||||||
id: render_image.id,
|
// id: render_image.id,
|
||||||
data: Arc::from(render_image),
|
// data: Arc::from(render_image),
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
impl Render for BlurHash {
|
// impl Render for BlurHash {
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
// fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
gpui::img(ImageSource::Render(self.data.clone()))
|
// gpui::img(ImageSource::Render(self.data.clone()))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
impl IntoElement for BlurHash {
|
// impl IntoElement for BlurHash {
|
||||||
type Element = gpui::Img;
|
// type Element = gpui::Img;
|
||||||
|
//
|
||||||
fn into_element(self) -> Self::Element {
|
// fn into_element(self) -> Self::Element {
|
||||||
gpui::img(ImageSource::Render(self.data.clone()))
|
// gpui::img(ImageSource::Render(self.data.clone()))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -2,16 +2,31 @@
|
|||||||
name = "ui-iced"
|
name = "ui-iced"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
api = { version = "0.1.0", path = "../api" }
|
api = { version = "0.1.0", path = "../api" }
|
||||||
blurhash = "0.2.3"
|
blurhash = "0.2.3"
|
||||||
|
bytes = "1.11.0"
|
||||||
gpui_util = "0.2.2"
|
gpui_util = "0.2.2"
|
||||||
iced = { git = "https://github.com/iced-rs/iced", features = [
|
iced = { workspace = true, features = [
|
||||||
"advanced",
|
"advanced",
|
||||||
"canvas",
|
"canvas",
|
||||||
"image",
|
"image",
|
||||||
|
"sipper",
|
||||||
"tokio",
|
"tokio",
|
||||||
] }
|
"debug",
|
||||||
|
"hot",
|
||||||
|
], default-features = true }
|
||||||
|
|
||||||
|
|
||||||
|
iced-video = { workspace = true }
|
||||||
|
iced_aw = "0.13.0"
|
||||||
|
iced_wgpu = "0.14.0"
|
||||||
|
iced_winit = "0.14.0"
|
||||||
|
reqwest = "0.13"
|
||||||
|
tap = "1.0.1"
|
||||||
|
toml = "0.9.8"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
|
url = "2.5.7"
|
||||||
uuid = "1.18.1"
|
uuid = "1.18.1"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use std::sync::{Arc, LazyLock, atomic::AtomicBool};
|
use iced::{Element, Length, advanced::Widget};
|
||||||
|
use std::sync::Arc;
|
||||||
use iced::{Element, advanced::Widget, widget::Image};
|
|
||||||
|
|
||||||
use crate::shared_string::SharedString;
|
use crate::shared_string::SharedString;
|
||||||
|
|
||||||
@@ -8,16 +7,27 @@ use crate::shared_string::SharedString;
|
|||||||
pub struct BlurHash {
|
pub struct BlurHash {
|
||||||
hash: SharedString,
|
hash: SharedString,
|
||||||
handle: Arc<iced::advanced::image::Handle>,
|
handle: Arc<iced::advanced::image::Handle>,
|
||||||
width: u32,
|
width: iced::Length,
|
||||||
height: u32,
|
height: iced::Length,
|
||||||
punch: f32,
|
punch: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Debug for BlurHash {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
f.debug_struct("BlurHash")
|
||||||
|
.field("hash", &self.hash)
|
||||||
|
.field("width", &self.width)
|
||||||
|
.field("height", &self.height)
|
||||||
|
.field("punch", &self.punch)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl BlurHash {
|
impl BlurHash {
|
||||||
pub fn recompute(&mut self) {
|
pub fn recompute(&mut self, width: u32, height: u32, punch: f32) {
|
||||||
let pixels = blurhash::decode(&self.hash, self.width, self.height, self.punch)
|
let pixels = blurhash::decode(&self.hash, width, height, punch)
|
||||||
.unwrap_or_else(|_| vec![0; (self.width * self.height * 4) as usize]);
|
.unwrap_or_else(|_| vec![0; (width * height * 4) as usize]);
|
||||||
let handle = iced::advanced::image::Handle::from_rgba(self.width, self.height, pixels);
|
let handle = iced::advanced::image::Handle::from_rgba(width, height, pixels);
|
||||||
self.handle = Arc::new(handle);
|
self.handle = Arc::new(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,27 +39,24 @@ impl BlurHash {
|
|||||||
BlurHash {
|
BlurHash {
|
||||||
hash,
|
hash,
|
||||||
handle,
|
handle,
|
||||||
width: 32,
|
width: 32.into(),
|
||||||
height: 32,
|
height: 32.into(),
|
||||||
punch: 1.0,
|
punch: 1.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn width(mut self, height: u32) -> Self {
|
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||||
self.width = height;
|
self.width = width.into();
|
||||||
self.recompute();
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn height(mut self, height: u32) -> Self {
|
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||||
self.height = height;
|
self.height = height.into();
|
||||||
self.recompute();
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn punch(mut self, punch: f32) -> Self {
|
pub fn punch(mut self, punch: f32) -> Self {
|
||||||
self.punch = punch;
|
self.punch = punch;
|
||||||
self.recompute();
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,8 +67,8 @@ where
|
|||||||
{
|
{
|
||||||
fn size(&self) -> iced::Size<iced::Length> {
|
fn size(&self) -> iced::Size<iced::Length> {
|
||||||
iced::Size {
|
iced::Size {
|
||||||
width: iced::Length::Fixed(self.width as f32),
|
width: self.width,
|
||||||
height: iced::Length::Fixed(self.height as f32),
|
height: self.height,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,17 +78,21 @@ where
|
|||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
limits: &iced::advanced::layout::Limits,
|
limits: &iced::advanced::layout::Limits,
|
||||||
) -> iced::advanced::layout::Node {
|
) -> iced::advanced::layout::Node {
|
||||||
iced::widget::image::layout(
|
let layout = iced::widget::image::layout(
|
||||||
renderer,
|
renderer,
|
||||||
limits,
|
limits,
|
||||||
&self.handle,
|
&self.handle,
|
||||||
self.width.into(),
|
self.width,
|
||||||
self.height.into(),
|
self.height,
|
||||||
None,
|
None,
|
||||||
iced::ContentFit::default(),
|
iced::ContentFit::default(),
|
||||||
iced::Rotation::default(),
|
iced::Rotation::default(),
|
||||||
false,
|
false,
|
||||||
)
|
);
|
||||||
|
let height = layout.bounds().height;
|
||||||
|
let width = layout.bounds().width;
|
||||||
|
self.recompute(width as u32, height as u32, self.punch);
|
||||||
|
layout
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(
|
fn draw(
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
|
mod settings;
|
||||||
|
mod video;
|
||||||
|
|
||||||
mod shared_string;
|
mod shared_string;
|
||||||
|
use iced_video::{Ready, Video, VideoHandle};
|
||||||
use shared_string::SharedString;
|
use shared_string::SharedString;
|
||||||
|
use tap::Pipe as _;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
mod blur_hash;
|
mod blur_hash;
|
||||||
use blur_hash::BlurHash;
|
use blur_hash::BlurHash;
|
||||||
|
|
||||||
|
mod preview;
|
||||||
|
// use preview::Preview;
|
||||||
|
|
||||||
use iced::{Alignment, Element, Length, Task, widget::*};
|
use iced::{Alignment, Element, Length, Task, widget::*};
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Loading {
|
pub struct Loading {}
|
||||||
to: Screen,
|
|
||||||
from: Screen,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct ItemCache {
|
pub struct ItemCache {
|
||||||
@@ -19,12 +26,15 @@ pub struct ItemCache {
|
|||||||
pub tree: BTreeMap<Option<uuid::Uuid>, BTreeSet<uuid::Uuid>>,
|
pub tree: BTreeMap<Option<uuid::Uuid>, BTreeSet<uuid::Uuid>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BACKGROUND_COLOR: iced::Color = iced::Color::from_rgba8(30, 30, 30, 0.7);
|
||||||
|
|
||||||
impl ItemCache {
|
impl ItemCache {
|
||||||
pub fn insert(&mut self, parent: impl Into<Option<uuid::Uuid>>, item: Item) {
|
pub fn insert(&mut self, parent: impl Into<Option<uuid::Uuid>>, item: Item) {
|
||||||
let parent = parent.into();
|
let parent = parent.into();
|
||||||
self.tree.entry(parent).or_default().insert(item.id);
|
self.tree.entry(parent).or_default().insert(item.id);
|
||||||
self.items.insert(item.id, item);
|
self.items.insert(item.id, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extend<I: IntoIterator<Item = Item>>(
|
pub fn extend<I: IntoIterator<Item = Item>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
parent: impl Into<Option<uuid::Uuid>>,
|
parent: impl Into<Option<uuid::Uuid>>,
|
||||||
@@ -35,6 +45,7 @@ impl ItemCache {
|
|||||||
self.insert(parent, item);
|
self.insert(parent, item);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn items_of(&self, parent: impl Into<Option<uuid::Uuid>>) -> Vec<&Item> {
|
pub fn items_of(&self, parent: impl Into<Option<uuid::Uuid>>) -> Vec<&Item> {
|
||||||
let parent = parent.into();
|
let parent = parent.into();
|
||||||
self.tree.get(&None);
|
self.tree.get(&None);
|
||||||
@@ -47,6 +58,10 @@ impl ItemCache {
|
|||||||
})
|
})
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, id: &uuid::Uuid) -> Option<&Item> {
|
||||||
|
self.items.get(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -72,6 +87,7 @@ impl From<api::jellyfin::BaseItemDto> for Item {
|
|||||||
.and_then(|hashes| hashes.get(&tag).cloned())
|
.and_then(|hashes| hashes.get(&tag).cloned())
|
||||||
.map(|s| s.clone().into()),
|
.map(|s| s.clone().into()),
|
||||||
}),
|
}),
|
||||||
|
_type: dto._type,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,6 +98,7 @@ pub struct Item {
|
|||||||
pub parent_id: Option<uuid::Uuid>,
|
pub parent_id: Option<uuid::Uuid>,
|
||||||
pub name: Option<SharedString>,
|
pub name: Option<SharedString>,
|
||||||
pub thumbnail: Option<Thumbnail>,
|
pub thumbnail: Option<Thumbnail>,
|
||||||
|
pub _type: api::jellyfin::BaseItemKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
@@ -89,120 +106,240 @@ pub enum Screen {
|
|||||||
#[default]
|
#[default]
|
||||||
Home,
|
Home,
|
||||||
Settings,
|
Settings,
|
||||||
Profile,
|
User,
|
||||||
|
Video,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub server_url: Option<String>,
|
||||||
|
pub device_id: Option<String>,
|
||||||
|
pub device_name: Option<String>,
|
||||||
|
pub client_name: Option<String>,
|
||||||
|
pub version: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Config {
|
||||||
|
server_url: Some("http://localhost:8096".to_string()),
|
||||||
|
device_id: Some("jello-iced".to_string()),
|
||||||
|
device_name: Some("Jello Iced".to_string()),
|
||||||
|
client_name: Some("Jello".to_string()),
|
||||||
|
version: Some("0.1.0".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct State {
|
struct State {
|
||||||
loading: Option<Loading>,
|
loading: Option<Loading>,
|
||||||
current: Option<uuid::Uuid>,
|
current: Option<uuid::Uuid>,
|
||||||
cache: ItemCache,
|
cache: ItemCache,
|
||||||
jellyfin_client: api::JellyfinClient,
|
jellyfin_client: Option<api::JellyfinClient>,
|
||||||
messages: Vec<String>,
|
messages: Vec<String>,
|
||||||
|
history: Vec<Option<uuid::Uuid>>,
|
||||||
|
query: Option<String>,
|
||||||
|
screen: Screen,
|
||||||
|
settings: settings::SettingsState,
|
||||||
|
is_authenticated: bool,
|
||||||
|
video: Option<Arc<VideoHandle<Message, Ready>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn new(jellyfin_client: api::JellyfinClient) -> Self {
|
pub fn new() -> Self {
|
||||||
State {
|
State {
|
||||||
loading: None,
|
loading: None,
|
||||||
current: None,
|
current: None,
|
||||||
cache: ItemCache::default(),
|
cache: ItemCache::default(),
|
||||||
jellyfin_client,
|
jellyfin_client: None,
|
||||||
messages: Vec::new(),
|
messages: Vec::new(),
|
||||||
|
history: Vec::new(),
|
||||||
|
query: None,
|
||||||
|
screen: Screen::Home,
|
||||||
|
settings: settings::SettingsState::default(),
|
||||||
|
is_authenticated: false,
|
||||||
|
video: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
OpenSettings,
|
Settings(settings::SettingsMessage),
|
||||||
Refresh,
|
Refresh,
|
||||||
|
Search,
|
||||||
|
SearchQueryChanged(String),
|
||||||
OpenItem(Option<uuid::Uuid>),
|
OpenItem(Option<uuid::Uuid>),
|
||||||
LoadedItem(Option<uuid::Uuid>, Vec<Item>),
|
LoadedItem(Option<uuid::Uuid>, Vec<Item>),
|
||||||
Error(String),
|
Error(String),
|
||||||
SetToken(String),
|
Back,
|
||||||
|
Home,
|
||||||
|
Video(video::VideoMessage),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(state: &mut State, message: Message) -> Task<Message> {
|
fn update(state: &mut State, message: Message) -> Task<Message> {
|
||||||
match message {
|
match message {
|
||||||
Message::OpenSettings => {
|
Message::Settings(msg) => settings::update(state, msg),
|
||||||
// Foo
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
Message::OpenItem(id) => {
|
Message::OpenItem(id) => {
|
||||||
let client = state.jellyfin_client.clone();
|
if let Some(client) = state.jellyfin_client.clone() {
|
||||||
Task::perform(
|
use api::jellyfin::BaseItemKind::*;
|
||||||
async move {
|
if let Some(cached) = id.as_ref().and_then(|id| state.cache.get(id))
|
||||||
let items: Result<Vec<Item>, api::JellyfinApiError> = client
|
&& matches!(cached._type, Video | Movie | Episode)
|
||||||
.items(id)
|
{
|
||||||
.await
|
let url = client
|
||||||
.map(|items| items.into_iter().map(Item::from).collect());
|
.stream_url(id.expect("ID exists"))
|
||||||
(id, items)
|
.expect("Failed to get stream URL");
|
||||||
},
|
Task::done(Message::Video(video::VideoMessage::Open(url)))
|
||||||
|(msg, items)| match items {
|
} else {
|
||||||
Err(e) => Message::Error(format!("Failed to load item: {}", e)),
|
Task::perform(
|
||||||
Ok(items) => Message::LoadedItem(msg, items),
|
async move {
|
||||||
},
|
let items: Result<Vec<Item>, api::JellyfinApiError> = client
|
||||||
)
|
.items(id)
|
||||||
|
.await
|
||||||
|
.map(|items| items.into_iter().map(Item::from).collect());
|
||||||
|
(id, items)
|
||||||
|
},
|
||||||
|
|(msg, items)| match items {
|
||||||
|
Err(e) => Message::Error(format!("Failed to load item: {}", e)),
|
||||||
|
Ok(items) => Message::LoadedItem(msg, items),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Message::LoadedItem(id, items) => {
|
Message::LoadedItem(id, items) => {
|
||||||
state.cache.extend(id, items);
|
state.cache.extend(id, items);
|
||||||
|
state.history.push(state.current);
|
||||||
state.current = id;
|
state.current = id;
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::Refresh => {
|
Message::Refresh => {
|
||||||
// Handle refresh logic
|
if let Some(client) = state.jellyfin_client.clone() {
|
||||||
let client = state.jellyfin_client.clone();
|
let current = state.current;
|
||||||
let current = state.current;
|
Task::perform(
|
||||||
Task::perform(
|
async move {
|
||||||
async move {
|
let items: Result<Vec<Item>, api::JellyfinApiError> = client
|
||||||
let items: Result<Vec<Item>, api::JellyfinApiError> = client
|
.items(current)
|
||||||
.items(current)
|
.await
|
||||||
.await
|
.map(|items| items.into_iter().map(Item::from).collect());
|
||||||
.map(|items| items.into_iter().map(Item::from).collect());
|
(current, items)
|
||||||
(current, items)
|
},
|
||||||
},
|
|(msg, items)| match items {
|
||||||
|(msg, items)| match items {
|
Err(e) => Message::Error(format!("Failed to refresh items: {}", e)),
|
||||||
Err(e) => Message::Error(format!("Failed to refresh items: {}", e)),
|
Ok(items) => Message::LoadedItem(msg, items),
|
||||||
Ok(items) => Message::LoadedItem(msg, items),
|
},
|
||||||
},
|
)
|
||||||
)
|
} else {
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Message::Error(err) => {
|
Message::Error(err) => {
|
||||||
tracing::error!("Error: {}", err);
|
tracing::error!("Error: {}", err);
|
||||||
state.messages.push(err);
|
state.messages.push(err);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::SetToken(token) => {
|
Message::Back => {
|
||||||
tracing::info!("Authenticated with token: {}", token);
|
state.current = state.history.pop().unwrap_or(None);
|
||||||
state.jellyfin_client.set_token(token);
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::Home => {
|
||||||
|
state.current = None;
|
||||||
|
Task::done(Message::Refresh)
|
||||||
|
}
|
||||||
|
Message::SearchQueryChanged(query) => {
|
||||||
|
state.query = Some(query);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::Search => {
|
||||||
|
// Handle search action
|
||||||
|
// let client = state.jellyfin_client.clone();
|
||||||
|
if let Some(client) = state.jellyfin_client.clone() {
|
||||||
|
let query = state.query.clone().unwrap_or_default();
|
||||||
|
Task::perform(async move { client.search(query).await }, |r| match r {
|
||||||
|
Err(e) => Message::Error(format!("Search failed: {}", e)),
|
||||||
|
Ok(items) => {
|
||||||
|
let items = items.into_iter().map(Item::from).collect();
|
||||||
|
Message::LoadedItem(None, items)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::Video(msg) => video::update(state, msg),
|
||||||
|
_ => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(state: &State) -> Element<'_, Message> {
|
fn view(state: &State) -> Element<'_, Message> {
|
||||||
column([header(state), body(state), footer(state)]).into()
|
let content = home(state);
|
||||||
|
match state.screen {
|
||||||
|
Screen::Settings => {
|
||||||
|
let settings = settings::settings(state);
|
||||||
|
let settings = container(settings)
|
||||||
|
.width(Length::FillPortion(4))
|
||||||
|
.height(Length::FillPortion(4))
|
||||||
|
.style(container::rounded_box)
|
||||||
|
.pipe(mouse_area)
|
||||||
|
.on_press(Message::Refresh)
|
||||||
|
.pipe(|c| iced::widget::column![space::vertical(), c, space::vertical()])
|
||||||
|
.pipe(container)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.align_x(Alignment::Center)
|
||||||
|
.style(|_| container::background(BACKGROUND_COLOR))
|
||||||
|
.padding(50)
|
||||||
|
.pipe(mouse_area)
|
||||||
|
.on_press(Message::Settings(settings::SettingsMessage::Close));
|
||||||
|
stack![content, settings].into()
|
||||||
|
}
|
||||||
|
Screen::Home | _ => content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn home(state: &State) -> Element<'_, Message> {
|
||||||
|
column([header(state), body(state), footer(state)])
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn body(state: &State) -> Element<'_, Message> {
|
fn body(state: &State) -> Element<'_, Message> {
|
||||||
container(
|
if let Some(ref video) = state.video {
|
||||||
Grid::with_children(state.cache.items_of(state.current).into_iter().map(card)).spacing(70),
|
video::player(video)
|
||||||
)
|
} else {
|
||||||
.padding(70)
|
Grid::with_children(state.cache.items_of(state.current).into_iter().map(card))
|
||||||
.align_x(Alignment::Center)
|
.fluid(400)
|
||||||
// .align_y(Alignment::Center)
|
.spacing(50)
|
||||||
.height(Length::Fill)
|
.pipe(container)
|
||||||
.width(Length::Fill)
|
.padding(50)
|
||||||
.into()
|
.align_x(Alignment::Center)
|
||||||
|
// .align_y(Alignment::Center)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.pipe(scrollable)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header(state: &State) -> Element<'_, Message> {
|
fn header(state: &State) -> Element<'_, Message> {
|
||||||
row([
|
row([
|
||||||
container(
|
text(
|
||||||
Text::new(state.jellyfin_client.config.server_url.as_str())
|
state
|
||||||
.width(Length::Fill)
|
.jellyfin_client
|
||||||
.align_x(Alignment::Start),
|
.as_ref()
|
||||||
|
.map(|c| c.config.server_url.as_str())
|
||||||
|
.unwrap_or("No Server"),
|
||||||
)
|
)
|
||||||
|
.align_x(Alignment::Start)
|
||||||
|
.pipe(button)
|
||||||
|
.on_press(Message::Home)
|
||||||
|
.pipe(container)
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
@@ -210,13 +347,18 @@ fn header(state: &State) -> Element<'_, Message> {
|
|||||||
.align_y(Alignment::Center)
|
.align_y(Alignment::Center)
|
||||||
.style(container::rounded_box)
|
.style(container::rounded_box)
|
||||||
.into(),
|
.into(),
|
||||||
container(
|
search(state),
|
||||||
row([
|
row([
|
||||||
button("Settings").on_press(Message::OpenSettings).into(),
|
button("Refresh").on_press(Message::Refresh).into(),
|
||||||
button("Refresh").on_press(Message::Refresh).into(),
|
button("Settings")
|
||||||
])
|
.on_press(Message::Settings(settings::SettingsMessage::Open))
|
||||||
.spacing(10),
|
.into(),
|
||||||
)
|
button("TestVideo")
|
||||||
|
.on_press(Message::Video(video::VideoMessage::Test))
|
||||||
|
.into(),
|
||||||
|
])
|
||||||
|
.spacing(10)
|
||||||
|
.pipe(container)
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
@@ -231,6 +373,21 @@ fn header(state: &State) -> Element<'_, Message> {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn search(state: &State) -> Element<'_, Message> {
|
||||||
|
TextInput::new("Search...", state.query.as_deref().unwrap_or_default())
|
||||||
|
.padding(10)
|
||||||
|
.size(16)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.on_input(Message::SearchQueryChanged)
|
||||||
|
.on_submit(Message::Search)
|
||||||
|
.pipe(container)
|
||||||
|
.padding(10)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Shrink)
|
||||||
|
.style(container::rounded_box)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
fn footer(state: &State) -> Element<'_, Message> {
|
fn footer(state: &State) -> Element<'_, Message> {
|
||||||
container(
|
container(
|
||||||
column(
|
column(
|
||||||
@@ -255,47 +412,83 @@ fn card(item: &Item) -> Element<'_, Message> {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|s| s.as_ref())
|
.map(|s| s.as_ref())
|
||||||
.unwrap_or("Unnamed Item");
|
.unwrap_or("Unnamed Item");
|
||||||
container(
|
MouseArea::new(
|
||||||
column([
|
container(
|
||||||
BlurHash::new(
|
column([
|
||||||
item.thumbnail
|
BlurHash::new(
|
||||||
.as_ref()
|
item.thumbnail
|
||||||
.and_then(|t| t.blur_hash.as_ref())
|
.as_ref()
|
||||||
.map(|s| s.as_ref())
|
.and_then(|t| t.blur_hash.as_ref())
|
||||||
.unwrap_or(""),
|
.map(|s| s.as_ref())
|
||||||
)
|
.unwrap_or(""),
|
||||||
.width(200)
|
)
|
||||||
.height(400)
|
.width(Length::Fill)
|
||||||
.into(),
|
.height(Length::FillPortion(5))
|
||||||
Text::new(name).size(16).into(),
|
.into(),
|
||||||
])
|
Text::new(name)
|
||||||
.align_x(Alignment::Center)
|
.size(16)
|
||||||
.width(Length::Fill)
|
.align_y(Alignment::Center)
|
||||||
.height(Length::Fill),
|
.height(Length::FillPortion(1))
|
||||||
|
.into(),
|
||||||
|
])
|
||||||
|
.align_x(Alignment::Center)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill),
|
||||||
|
)
|
||||||
|
.style(container::rounded_box),
|
||||||
)
|
)
|
||||||
.padding(70)
|
.on_press(Message::OpenItem(Some(item.id)))
|
||||||
.width(Length::FillPortion(5))
|
|
||||||
.height(Length::FillPortion(5))
|
|
||||||
.style(container::rounded_box)
|
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(config: impl Fn() -> api::JellyfinConfig + 'static) -> impl Fn() -> (State, Task<Message>) {
|
fn init() -> (State, Task<Message>) {
|
||||||
move || {
|
// Create a default config for initial state
|
||||||
let mut jellyfin = api::JellyfinClient::new(config());
|
|
||||||
(
|
// let default_config = api::JellyfinConfig {
|
||||||
State::new(jellyfin.clone()),
|
// server_url: "http://localhost:8096".parse().expect("Valid URL"),
|
||||||
Task::perform(
|
// device_id: "jello-iced".to_string(),
|
||||||
async move { jellyfin.authenticate_with_cached_token(".session").await },
|
// device_name: "Jello Iced".to_string(),
|
||||||
|token| match token {
|
// client_name: "Jello".to_string(),
|
||||||
Ok(token) => Message::SetToken(token),
|
// version: "0.1.0".to_string(),
|
||||||
Err(e) => Message::Error(format!("Authentication failed: {}", e)),
|
// };
|
||||||
},
|
// let default_client = api::JellyfinClient::new_with_config(default_config);
|
||||||
),
|
|
||||||
|
(
|
||||||
|
State::new(),
|
||||||
|
Task::perform(
|
||||||
|
async move {
|
||||||
|
let config_str = std::fs::read_to_string("config.toml")
|
||||||
|
.map_err(|e| api::JellyfinApiError::IoError(e))?;
|
||||||
|
let config: api::JellyfinConfig = toml::from_str(&config_str).map_err(|e| {
|
||||||
|
api::JellyfinApiError::IoError(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
e,
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Try to load cached token and authenticate
|
||||||
|
match std::fs::read_to_string(".session") {
|
||||||
|
Ok(token) => {
|
||||||
|
let client = api::JellyfinClient::pre_authenticated(token.trim(), config)?;
|
||||||
|
Ok((client, true))
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// No cached token, create unauthenticated client
|
||||||
|
let client = api::JellyfinClient::new_with_config(config);
|
||||||
|
Ok((client, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|result: Result<_, api::JellyfinApiError>| match result {
|
||||||
|
// Ok((client, is_authenticated)) => Message::LoadedClient(client, is_authenticated),
|
||||||
|
Err(e) => Message::Error(format!("Initialization failed: {}", e)),
|
||||||
|
_ => Message::Error("Login Unimplemented".to_string()),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
.chain(Task::done(Message::Refresh)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(config: impl Fn() -> api::JellyfinConfig + 'static) -> iced::Result {
|
pub fn ui() -> iced::Result {
|
||||||
iced::application(init(config), update, view).run()
|
iced::application(init, update, view).run()
|
||||||
}
|
}
|
||||||
|
|||||||
224
ui-iced/src/preview.rs
Normal file
224
ui-iced/src/preview.rs
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
use iced::{Animation, Function, advanced::image::Handle, widget::image};
|
||||||
|
use reqwest::Method;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::blur_hash::BlurHash;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ImageDownloader {
|
||||||
|
client: reqwest::Client,
|
||||||
|
request_modifier:
|
||||||
|
Option<Arc<dyn Fn(reqwest::RequestBuilder) -> reqwest::RequestBuilder + Send + Sync>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Debug for ImageDownloader {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
f.debug_struct("ImageDownloader")
|
||||||
|
.field("client", &self.client)
|
||||||
|
.field("request_modifier", &self.request_modifier.is_some())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageDownloader {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
client: reqwest::Client::new(),
|
||||||
|
request_modifier: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_modifier<F>(mut self, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(reqwest::RequestBuilder) -> reqwest::RequestBuilder + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.request_modifier = Some(Arc::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn download(&self, url: &str) -> reqwest::Result<bytes::Bytes> {
|
||||||
|
use ::tap::*;
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.request(Method::GET, url)
|
||||||
|
.pipe(|builder| {
|
||||||
|
if let Some(ref modifier) = self.request_modifier {
|
||||||
|
modifier(builder)
|
||||||
|
} else {
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
let bytes = response.bytes().await?;
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Preview {
|
||||||
|
Thumbnail {
|
||||||
|
thumbnail: Image,
|
||||||
|
blur_hash: BlurHash,
|
||||||
|
},
|
||||||
|
BlurHash {
|
||||||
|
blur_hash: BlurHash,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ImageCache {
|
||||||
|
cache: std::collections::HashMap<uuid::Uuid, Preview>,
|
||||||
|
downloader: ImageDownloader,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Preview {
|
||||||
|
pub fn thumbnail(image: Image, blur_hash: BlurHash) -> Self {
|
||||||
|
Preview::Thumbnail {
|
||||||
|
thumbnail: image,
|
||||||
|
blur_hash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blur_hash(blur_hash: BlurHash) -> Self {
|
||||||
|
Preview::BlurHash { blur_hash }
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn upgrade(
|
||||||
|
// self,
|
||||||
|
// fut: impl core::future::Future<Output = bytes::Bytes> + 'static + Send,
|
||||||
|
// ) -> iced::Task<PreviewMessage> {
|
||||||
|
// // let sip = iced::task::sipper(async move |mut sender| {
|
||||||
|
// // let bytes = fut.await;
|
||||||
|
// // let handle = Handle::from_bytes(bytes.clone());
|
||||||
|
// // let allocation = image::allocate(handle);
|
||||||
|
// // let image = Image {
|
||||||
|
// // bytes,
|
||||||
|
// // handle,
|
||||||
|
// // allocation,
|
||||||
|
// // fade_in: Animation::new(false),
|
||||||
|
// // };
|
||||||
|
// // let _ = sender.send(image).await;
|
||||||
|
// // });
|
||||||
|
// // iced::Task::sip(sip, ||)
|
||||||
|
// Task::
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PreviewMessage {
|
||||||
|
BlurHashLoaded(uuid::Uuid, BlurHash),
|
||||||
|
|
||||||
|
ThumbnailDownloaded(uuid::Uuid, bytes::Bytes),
|
||||||
|
// ThumbnailAllocated(
|
||||||
|
// uuid::Uuid,
|
||||||
|
// Result<image::Allocation, iced::advanced::image::Error>,
|
||||||
|
// ),
|
||||||
|
ThumbnailLoaded(uuid::Uuid, Image),
|
||||||
|
|
||||||
|
Failed(uuid::Uuid, String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Image {
|
||||||
|
bytes: bytes::Bytes,
|
||||||
|
handle: Handle,
|
||||||
|
allocation: image::Allocation,
|
||||||
|
fade_in: Animation<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PreviewSource {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub url: String,
|
||||||
|
pub width: iced::Length,
|
||||||
|
pub height: iced::Length,
|
||||||
|
pub blur_hash: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreviewSource {
|
||||||
|
pub fn new(id: uuid::Uuid, url: String) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
url,
|
||||||
|
width: iced::Length::Fill,
|
||||||
|
height: iced::Length::Fill,
|
||||||
|
blur_hash: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blur_hash(mut self, blur_hash: String) -> Self {
|
||||||
|
self.blur_hash = Some(blur_hash);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn url(mut self, url: impl Into<String>) -> Self {
|
||||||
|
self.url = url.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn width(mut self, width: iced::Length) -> Self {
|
||||||
|
self.width = width;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn height(mut self, height: iced::Length) -> Self {
|
||||||
|
self.height = height;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn upgrade(self) -> Task<PreviewMessage> {
|
||||||
|
// let sip = iced::task::sipper(async move |mut sender| {
|
||||||
|
// let bytes = fut.await;
|
||||||
|
// let handle = Handle::from_bytes(bytes.clone());
|
||||||
|
// let allocation = image::allocate(handle);
|
||||||
|
// let image = Image {
|
||||||
|
// bytes,
|
||||||
|
// handle,
|
||||||
|
// allocation,
|
||||||
|
// fade_in: Animation::new(false),
|
||||||
|
// };
|
||||||
|
// let _ = sender.send(image).await;
|
||||||
|
// });
|
||||||
|
// // iced::Task::sip(sip, ||)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageCache {
|
||||||
|
pub fn upgrade(&self, source: PreviewSource) -> iced::Task<PreviewMessage> {
|
||||||
|
let downloader = self.downloader.clone();
|
||||||
|
let sipper = iced::task::sipper(async move |mut sender| {
|
||||||
|
if let Some(blur_hash_str) = source.blur_hash {
|
||||||
|
let blur_hash = BlurHash::new(&blur_hash_str);
|
||||||
|
let _ = sender.send(blur_hash).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = downloader.download(&source.url).await?;
|
||||||
|
reqwest::Result::<bytes::Bytes>::Ok(bytes)
|
||||||
|
});
|
||||||
|
iced::Task::sip(
|
||||||
|
sipper,
|
||||||
|
move |progress| PreviewMessage::BlurHashLoaded(source.id, progress),
|
||||||
|
move |output: reqwest::Result<bytes::Bytes>| match output {
|
||||||
|
Ok(bytes) => PreviewMessage::ThumbnailDownloaded(source.id, bytes),
|
||||||
|
Err(e) => PreviewMessage::Failed(source.id, e.to_string()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then(|message| match message {
|
||||||
|
PreviewMessage::ThumbnailDownloaded(id, bytes) => {
|
||||||
|
let handle = Handle::from_bytes(bytes.clone());
|
||||||
|
let allocation = image::allocate(&handle);
|
||||||
|
allocation.map(move |output| match output {
|
||||||
|
Ok(allocation) => {
|
||||||
|
let image = Image {
|
||||||
|
bytes: bytes.clone(),
|
||||||
|
handle: handle.clone(),
|
||||||
|
allocation,
|
||||||
|
fade_in: Animation::new(false),
|
||||||
|
};
|
||||||
|
PreviewMessage::ThumbnailLoaded(id, image)
|
||||||
|
}
|
||||||
|
Err(e) => PreviewMessage::Failed(id, e.to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
other => iced::Task::done(other),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
296
ui-iced/src/settings.rs
Normal file
296
ui-iced/src/settings.rs
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
use crate::*;
|
||||||
|
use iced::Element;
|
||||||
|
|
||||||
|
pub fn settings(state: &State) -> Element<'_, Message> {
|
||||||
|
screens::settings(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(state: &mut State, message: SettingsMessage) -> Task<Message> {
|
||||||
|
match message {
|
||||||
|
SettingsMessage::Open => {
|
||||||
|
tracing::trace!("Opening settings");
|
||||||
|
state.screen = Screen::Settings;
|
||||||
|
}
|
||||||
|
SettingsMessage::Close => {
|
||||||
|
tracing::trace!("Closing settings");
|
||||||
|
state.screen = Screen::Home;
|
||||||
|
}
|
||||||
|
SettingsMessage::Select(screen) => {
|
||||||
|
tracing::trace!("Switching settings screen to {:?}", screen);
|
||||||
|
state.settings.screen = screen;
|
||||||
|
}
|
||||||
|
SettingsMessage::User(user) => state.settings.login_form.update(user),
|
||||||
|
|
||||||
|
SettingsMessage::Server(server) => state.settings.server_form.update(server),
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty() -> Element<'static, Message> {
|
||||||
|
column([]).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct SettingsState {
|
||||||
|
login_form: LoginForm,
|
||||||
|
server_form: ServerForm,
|
||||||
|
screen: SettingsScreen,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum SettingsMessage {
|
||||||
|
Open,
|
||||||
|
Close,
|
||||||
|
Select(SettingsScreen),
|
||||||
|
User(UserMessage),
|
||||||
|
Server(ServerMessage),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum UserMessage {
|
||||||
|
Add,
|
||||||
|
UsernameChanged(String),
|
||||||
|
PasswordChanged(String),
|
||||||
|
// Edit(uuid::Uuid),
|
||||||
|
// Delete(uuid::Uuid),
|
||||||
|
Clear,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ServerMessage {
|
||||||
|
Add,
|
||||||
|
NameChanged(String),
|
||||||
|
UrlChanged(String),
|
||||||
|
// Edit(uuid::Uuid),
|
||||||
|
// Delete(uuid::Uuid),
|
||||||
|
Clear,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub enum SettingsScreen {
|
||||||
|
#[default]
|
||||||
|
Main,
|
||||||
|
Users,
|
||||||
|
Servers,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ServerItem {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub name: SharedString,
|
||||||
|
pub url: SharedString,
|
||||||
|
pub users: Vec<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UserItem {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub name: SharedString,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct LoginForm {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoginForm {
|
||||||
|
pub fn update(&mut self, message: UserMessage) {
|
||||||
|
match message {
|
||||||
|
UserMessage::UsernameChanged(data) => {
|
||||||
|
self.username = data;
|
||||||
|
}
|
||||||
|
UserMessage::PasswordChanged(data) => {
|
||||||
|
self.password = data;
|
||||||
|
}
|
||||||
|
UserMessage::Add => {
|
||||||
|
// Handle adding user
|
||||||
|
}
|
||||||
|
UserMessage::Clear => {
|
||||||
|
self.username.clear();
|
||||||
|
self.password.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn view(&self) -> Element<'_, Message> {
|
||||||
|
iced::widget::column![
|
||||||
|
text("Login Form"),
|
||||||
|
text_input("Enter Username", &self.username).on_input(|data| {
|
||||||
|
Message::Settings(SettingsMessage::User(UserMessage::UsernameChanged(data)))
|
||||||
|
}),
|
||||||
|
text_input("Enter Password", &self.password)
|
||||||
|
.secure(true)
|
||||||
|
.on_input(|data| {
|
||||||
|
Message::Settings(SettingsMessage::User(UserMessage::PasswordChanged(data)))
|
||||||
|
}),
|
||||||
|
row![
|
||||||
|
button(text("Add User")).on_press_maybe(self.validate()),
|
||||||
|
button(text("Cancel"))
|
||||||
|
.on_press(Message::Settings(SettingsMessage::User(UserMessage::Clear))),
|
||||||
|
]
|
||||||
|
.spacing(10),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.padding([10, 0])
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self) -> Option<Message> {
|
||||||
|
(!self.username.is_empty() && !self.password.is_empty())
|
||||||
|
.then(|| Message::Settings(SettingsMessage::User(UserMessage::Add)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ServerForm {
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerForm {
|
||||||
|
pub fn update(&mut self, message: ServerMessage) {
|
||||||
|
match message {
|
||||||
|
ServerMessage::NameChanged(data) => {
|
||||||
|
self.name = data;
|
||||||
|
}
|
||||||
|
ServerMessage::UrlChanged(data) => {
|
||||||
|
self.url = data;
|
||||||
|
}
|
||||||
|
ServerMessage::Add => {
|
||||||
|
// Handle adding server
|
||||||
|
}
|
||||||
|
ServerMessage::Clear => {
|
||||||
|
self.name.clear();
|
||||||
|
self.url.clear();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn view(&self) -> Element<'_, Message> {
|
||||||
|
iced::widget::column![
|
||||||
|
text("Add New Server"),
|
||||||
|
text_input("Enter server name", &self.name).on_input(|data| {
|
||||||
|
Message::Settings(SettingsMessage::Server(ServerMessage::NameChanged(data)))
|
||||||
|
}),
|
||||||
|
text_input("Enter server URL", &self.url).on_input(|data| {
|
||||||
|
Message::Settings(SettingsMessage::Server(ServerMessage::UrlChanged(data)))
|
||||||
|
}),
|
||||||
|
row![
|
||||||
|
button(text("Add Server")).on_press_maybe(self.validate()),
|
||||||
|
button(text("Cancel")).on_press(Message::Settings(SettingsMessage::Server(
|
||||||
|
ServerMessage::Clear
|
||||||
|
))),
|
||||||
|
]
|
||||||
|
.spacing(10),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.padding([10, 0])
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self) -> Option<Message> {
|
||||||
|
(!self.name.is_empty() && !self.url.is_empty())
|
||||||
|
.then(|| Message::Settings(SettingsMessage::Server(ServerMessage::Add)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod screens {
|
||||||
|
use iced_aw::Tabs;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
pub fn settings(state: &State) -> Element<'_, Message> {
|
||||||
|
Tabs::new(|f| Message::Settings(SettingsMessage::Select(f)))
|
||||||
|
.push(
|
||||||
|
SettingsScreen::Main,
|
||||||
|
iced_aw::TabLabel::Text("General".into()),
|
||||||
|
main(state),
|
||||||
|
)
|
||||||
|
.push(
|
||||||
|
SettingsScreen::Servers,
|
||||||
|
iced_aw::TabLabel::Text("Servers".into()),
|
||||||
|
server(state),
|
||||||
|
)
|
||||||
|
.push(
|
||||||
|
SettingsScreen::Users,
|
||||||
|
iced_aw::TabLabel::Text("Users".into()),
|
||||||
|
user(state),
|
||||||
|
)
|
||||||
|
.set_active_tab(&state.settings.screen)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn settings_screen(state: &State) -> Element<'_, Message> {
|
||||||
|
container(match state.settings.screen {
|
||||||
|
SettingsScreen::Main => main(state),
|
||||||
|
SettingsScreen::Servers => server(state),
|
||||||
|
SettingsScreen::Users => user(state),
|
||||||
|
})
|
||||||
|
.width(Length::FillPortion(10))
|
||||||
|
.height(Length::Fill)
|
||||||
|
.style(|theme| container::background(theme.extended_palette().background.base.color))
|
||||||
|
.pipe(container)
|
||||||
|
.padding(10)
|
||||||
|
.style(|theme| container::background(theme.extended_palette().secondary.base.color))
|
||||||
|
.width(Length::FillPortion(10))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn settings_list(state: &State) -> Element<'_, Message> {
|
||||||
|
column(
|
||||||
|
[
|
||||||
|
button(center_text("General")).on_press(Message::Settings(
|
||||||
|
SettingsMessage::Select(SettingsScreen::Main),
|
||||||
|
)),
|
||||||
|
button(center_text("Servers")).on_press(Message::Settings(
|
||||||
|
SettingsMessage::Select(SettingsScreen::Servers),
|
||||||
|
)),
|
||||||
|
button(center_text("Users")).on_press(Message::Settings(SettingsMessage::Select(
|
||||||
|
SettingsScreen::Users,
|
||||||
|
))),
|
||||||
|
]
|
||||||
|
.map(|p| p.clip(true).width(Length::Fill).into()),
|
||||||
|
)
|
||||||
|
.width(Length::FillPortion(2))
|
||||||
|
.spacing(10)
|
||||||
|
.padding(10)
|
||||||
|
.pipe(scrollable)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(state: &State) -> Element<'_, Message> {
|
||||||
|
Column::new()
|
||||||
|
.push(text("Main Settings"))
|
||||||
|
.push(toggler(true).label("HDR"))
|
||||||
|
.spacing(20)
|
||||||
|
.padding(20)
|
||||||
|
.pipe(container)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn server(state: &State) -> Element<'_, Message> {
|
||||||
|
Column::new()
|
||||||
|
.push(text("Server Settings"))
|
||||||
|
.push(state.settings.server_form.view())
|
||||||
|
.spacing(20)
|
||||||
|
.padding(20)
|
||||||
|
.pipe(container)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user(state: &State) -> Element<'_, Message> {
|
||||||
|
Column::new()
|
||||||
|
.push(text("User Settings"))
|
||||||
|
.push(state.settings.login_form.view())
|
||||||
|
.spacing(20)
|
||||||
|
.padding(20)
|
||||||
|
.pipe(container)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn center_text(content: &str) -> Element<'_, Message> {
|
||||||
|
text(content)
|
||||||
|
.align_x(Alignment::Center)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
@@ -49,6 +49,21 @@ impl std::ops::Deref for SharedString {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct SecretSharedString(ArcCow<'static, str>);
|
||||||
|
|
||||||
|
impl core::fmt::Debug for SecretSharedString {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("(..secret..)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for SecretSharedString {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Self(ArcCow::Owned(Arc::from(s)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||||
pub enum ArcCow<'a, T: ?Sized> {
|
pub enum ArcCow<'a, T: ?Sized> {
|
||||||
Borrowed(&'a T),
|
Borrowed(&'a T),
|
||||||
@@ -66,3 +81,9 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a, T> From<&'a T> for ArcCow<'a, T> {
|
||||||
|
fn from(value: &'a T) -> Self {
|
||||||
|
ArcCow::Borrowed(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
78
ui-iced/src/video.rs
Normal file
78
ui-iced/src/video.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use super::*;
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum VideoMessage {
|
||||||
|
EndOfStream,
|
||||||
|
Open(url::Url),
|
||||||
|
Loaded(VideoHandle<Message, Ready>),
|
||||||
|
Pause,
|
||||||
|
Play,
|
||||||
|
Seek(f64),
|
||||||
|
Stop,
|
||||||
|
Test,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(state: &mut State, message: VideoMessage) -> Task<Message> {
|
||||||
|
match message {
|
||||||
|
VideoMessage::EndOfStream => {
|
||||||
|
state.video = None;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
VideoMessage::Open(url) => {
|
||||||
|
Task::perform(VideoHandle::load(url.clone()), move |result| match result {
|
||||||
|
Ok(video) => Message::Video(VideoMessage::Loaded(video)),
|
||||||
|
Err(err) => Message::Error(format!("Error opening video at {}: {:?}", url, err)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
VideoMessage::Loaded(video) => {
|
||||||
|
state.video = Some(Arc::new(
|
||||||
|
video.on_end_of_stream(Message::Video(VideoMessage::EndOfStream)),
|
||||||
|
));
|
||||||
|
Task::done(VideoMessage::Play).map(Message::Video)
|
||||||
|
}
|
||||||
|
VideoMessage::Pause => {
|
||||||
|
if let Some(ref video) = state.video {
|
||||||
|
video.pause();
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
VideoMessage::Play => {
|
||||||
|
if let Some(ref video) = state.video {
|
||||||
|
video.play();
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
VideoMessage::Seek(position) => {
|
||||||
|
// if let Some(ref video) = state.video {
|
||||||
|
// // video.seek(position, true);
|
||||||
|
// }
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
VideoMessage::Stop => {
|
||||||
|
state.video.as_ref().map(|video| {
|
||||||
|
video.stop();
|
||||||
|
});
|
||||||
|
state.video = None;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
VideoMessage::Test => {
|
||||||
|
let url = url::Url::parse("https://jellyfin.tsuba.darksailor.dev/Items/6010382cf25273e624d305907010d773/Download?api_key=036c140222464878862231ef66a2bc9c")
|
||||||
|
.expect("Impossible: Failed to parse hardcoded URL");
|
||||||
|
Task::done(VideoMessage::Open(url)).map(Message::Video)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn player(video: &VideoHandle<Message, Ready>) -> Element<'_, Message> {
|
||||||
|
container(
|
||||||
|
Video::new(video)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.content_fit(iced::ContentFit::Contain),
|
||||||
|
)
|
||||||
|
.style(|_| container::background(iced::Color::BLACK))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.align_x(Alignment::Center)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user