Skip to content

Commit 317ba7e

Browse files
committed
16.05.2024
* `ffmpeg` feature implementation improved
1 parent 49a1e09 commit 317ba7e

File tree

6 files changed

+246
-107
lines changed

6 files changed

+246
-107
lines changed

Cargo.toml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rusty_ytdl"
3-
version = "0.7.1"
3+
version = "0.7.2"
44
authors = ["Mithronn"]
55
edition = "2021"
66
description = "A Rust library for Youtube video searcher and downloader"
@@ -22,46 +22,46 @@ members = [".", "cli"]
2222
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
2323

2424
[dependencies]
25-
reqwest = { version = "0.12.3", features = [
25+
reqwest = { version = "0.12.4", features = [
2626
"cookies",
2727
"gzip",
2828
], default-features = false }
2929
scraper = "0.19.0"
30-
serde = "1.0.197"
31-
serde_json = "1.0.114"
30+
serde = "1.0.202"
31+
serde_json = "1.0.117"
3232
serde_qs = "0.13.0"
3333
regex = "1.10.3"
3434
url = "2.5.0"
3535
urlencoding = "2.1.3"
36-
thiserror = "1.0.57"
36+
thiserror = "1.0.60"
3737
derive_more = "0.99.17"
3838
derivative = "2.2.0"
3939
once_cell = "1.19.0"
40-
tokio = { version = "1.36.0", default-features = false, features = ["sync"] }
40+
tokio = { version = "1.37.0", default-features = false, features = ["sync"] }
4141
rand = "0.8.5"
42-
reqwest-middleware = { version = "0.3.0", features = ["json"] }
42+
reqwest-middleware = { version = "0.3.1", features = ["json"] }
4343
reqwest-retry = "0.5.0"
4444
m3u8-rs = "6.0.0"
45-
async-trait = "0.1.77"
45+
async-trait = "0.1.80"
4646
aes = "0.8.4"
4747
cbc = { version = "0.1.2", features = ["std"] }
4848
hex = "0.4.3"
4949
boa_engine = "0.17.3"
5050
mime = "0.3.17"
51-
bytes = "1.5.0"
51+
bytes = "1.6.0"
5252
flame = { version = "0.2.2", optional = true }
5353
flamer = { version = "0.5.0", optional = true }
5454

5555
[dev-dependencies]
56-
tokio = { version = "1.36.0", features = ["full"] }
56+
tokio = { version = "1.37.0", features = ["full"] }
5757

5858
[features]
5959
default = ["search", "live", "default-tls"]
6060
performance_analysis = ["flame", "flamer"]
6161
live = ["tokio/time", "tokio/process"]
6262
blocking = ["tokio/rt", "tokio/rt-multi-thread"]
6363
search = []
64-
ffmpeg = ["tokio/process"]
64+
ffmpeg = ["tokio/process", "tokio/io-util"]
6565
default-tls = ["reqwest/default-tls"]
6666
native-tls = ["reqwest/native-tls"]
6767
rustls-tls = ["reqwest/rustls-tls"]

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ Download videos **blazing-fast** without getting stuck on Youtube download speed
1919

2020
## Roadmap
2121

22-
- [x] ffmpeg feature
2322
- [ ] benchmarks
2423

2524
## Features
@@ -28,7 +27,7 @@ Download videos **blazing-fast** without getting stuck on Youtube download speed
2827
- Search with query (Video, Playlist, Channel)
2928
- Blocking and asynchronous API
3029
- Proxy, IPv6, and cookie support on request
31-
- Built-in FFmpeg audio and video filter apply support. [Example](examples/download_with_ffmpeg.rs)
30+
- Built-in FFmpeg audio and video filter apply support (Non-live videos only) [Example](examples/download_with_ffmpeg.rs)
3231
- [CLI](https://crates.io/crates/rusty_ytdl-cli)
3332

3433
# Usage
@@ -159,5 +158,5 @@ Or add the following to your `Cargo.toml` file:
159158

160159
```toml
161160
[dependencies]
162-
rusty_ytdl = "0.7.1"
161+
rusty_ytdl = "0.7.2"
163162
```

src/info.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use std::{collections::HashMap, sync::Arc};
2-
31
use scraper::{Html, Selector};
42

53
use crate::constants::{BASE_URL, FORMATS};

src/stream/streams/mod.rs

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,33 @@
22
mod live;
33
mod non_live;
44

5+
use async_trait::async_trait;
56
use bytes::Bytes;
67

8+
#[cfg(feature = "ffmpeg")]
9+
use bytes::BytesMut;
10+
11+
#[cfg(feature = "ffmpeg")]
12+
use std::{process::Stdio, sync::Arc};
13+
14+
#[cfg(feature = "ffmpeg")]
15+
use tokio::{
16+
io::{AsyncReadExt, AsyncWriteExt},
17+
process::{Child, Command},
18+
sync::{
19+
mpsc::{channel, Receiver},
20+
Mutex, Notify,
21+
},
22+
task::JoinHandle,
23+
};
24+
725
#[cfg(feature = "live")]
826
pub use live::{LiveStream, LiveStreamOptions};
927
pub use non_live::{NonLiveStream, NonLiveStreamOptions};
1028

29+
#[cfg(feature = "ffmpeg")]
30+
use crate::constants::DEFAULT_HEADERS;
1131
use crate::VideoError;
12-
use async_trait::async_trait;
1332

1433
#[async_trait]
1534
pub trait Stream {
@@ -25,3 +44,156 @@ pub trait Stream {
2544
0
2645
}
2746
}
47+
48+
#[cfg(feature = "ffmpeg")]
49+
pub struct FFmpegStreamOptions {
50+
pub client: reqwest_middleware::ClientWithMiddleware,
51+
pub link: String,
52+
pub content_length: u64,
53+
pub dl_chunk_size: u64,
54+
pub start: u64,
55+
pub end: u64,
56+
pub ffmpeg_args: Vec<String>,
57+
}
58+
59+
#[cfg(feature = "ffmpeg")]
60+
pub(crate) struct FFmpegStream {
61+
pub refined_data_reciever: Option<Arc<Mutex<Receiver<Bytes>>>>,
62+
download_notify: Arc<Notify>,
63+
ffmpeg_child: Child,
64+
65+
tasks: Vec<JoinHandle<Result<(), VideoError>>>,
66+
}
67+
68+
#[cfg(feature = "ffmpeg")]
69+
impl FFmpegStream {
70+
pub fn new(options: FFmpegStreamOptions) -> Result<Self, VideoError> {
71+
let (tx, mut rx) = channel::<Bytes>(16384);
72+
let (refined_tx, refined_rx) = channel::<Bytes>(16384);
73+
74+
// Spawn FFmpeg process
75+
let mut ffmpeg_child = Command::new("ffmpeg")
76+
.args(&options.ffmpeg_args)
77+
.stdin(Stdio::piped())
78+
.stdout(Stdio::piped())
79+
.kill_on_drop(true)
80+
.spawn()
81+
.map_err(|x| VideoError::FFmpeg(x.to_string()))?;
82+
83+
let mut stdin = ffmpeg_child.stdin.take().unwrap();
84+
let mut stdout = ffmpeg_child.stdout.take().unwrap();
85+
86+
let read_stdout_task = tokio::spawn(async move {
87+
let mut buffer = vec![0u8; 16384];
88+
while let Ok(line) = stdout.read(&mut buffer).await {
89+
match line {
90+
0 => {
91+
break;
92+
}
93+
n => {
94+
if let Err(_err) = refined_tx.send(Bytes::from(buffer[..n].to_vec())).await
95+
{
96+
return Err(VideoError::FFmpeg("channel closed".to_string()));
97+
// Error or channel closed
98+
};
99+
}
100+
}
101+
}
102+
103+
Ok(())
104+
});
105+
106+
let write_stdin_task = tokio::spawn(async move {
107+
while let Some(data) = rx.recv().await {
108+
if let Err(err) = stdin.write_all(&data).await {
109+
return Err(VideoError::FFmpeg(err.to_string())); // Error or channel closed
110+
}
111+
}
112+
Ok(())
113+
});
114+
115+
let download_notify = Arc::new(Notify::new());
116+
let download_notify_task = download_notify.clone();
117+
118+
let download_task = tokio::spawn(async move {
119+
let mut end = options.end;
120+
let mut start = options.start;
121+
let content_length = options.content_length;
122+
let client = options.client;
123+
let link = options.link;
124+
let dl_chunk_size = options.dl_chunk_size;
125+
126+
download_notify_task.notified().await;
127+
128+
loop {
129+
// Nothing else remain send break to finish
130+
if end == 0 {
131+
break;
132+
}
133+
134+
if end >= content_length {
135+
end = 0;
136+
}
137+
138+
let mut headers = DEFAULT_HEADERS.clone();
139+
140+
let range_end = if end == 0 {
141+
"".to_string()
142+
} else {
143+
end.to_string()
144+
};
145+
146+
headers.insert(
147+
reqwest::header::RANGE,
148+
format!("bytes={}-{}", start, range_end).parse().unwrap(),
149+
);
150+
151+
let mut response = client
152+
.get(&link)
153+
.headers(headers)
154+
.send()
155+
.await
156+
.map_err(VideoError::ReqwestMiddleware)?;
157+
158+
let mut buf: BytesMut = BytesMut::new();
159+
160+
while let Some(chunk) = response.chunk().await.map_err(VideoError::Reqwest)? {
161+
buf.extend(chunk);
162+
}
163+
164+
if end != 0 {
165+
start = end + 1;
166+
167+
end += dl_chunk_size;
168+
}
169+
170+
tx.send(buf.into())
171+
.await
172+
.map_err(|x| VideoError::FFmpeg(x.to_string()))?;
173+
}
174+
175+
Ok(())
176+
});
177+
178+
Ok(Self {
179+
refined_data_reciever: Some(Arc::new(Mutex::new(refined_rx))),
180+
download_notify,
181+
ffmpeg_child,
182+
tasks: vec![download_task, write_stdin_task, read_stdout_task],
183+
})
184+
}
185+
186+
pub fn start_download(&self) {
187+
self.download_notify.notify_one();
188+
}
189+
}
190+
191+
#[cfg(feature = "ffmpeg")]
192+
impl Drop for FFmpegStream {
193+
fn drop(&mut self) {
194+
// kill tasks if they are still running
195+
for handle in &self.tasks {
196+
handle.abort();
197+
}
198+
}
199+
}

0 commit comments

Comments
 (0)