Skip to content

Commit

Permalink
16.05.2024
Browse files Browse the repository at this point in the history
* `ffmpeg` feature implementation improved
  • Loading branch information
Mithronn committed May 15, 2024
1 parent 49a1e09 commit 317ba7e
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 107 deletions.
22 changes: 11 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rusty_ytdl"
version = "0.7.1"
version = "0.7.2"
authors = ["Mithronn"]
edition = "2021"
description = "A Rust library for Youtube video searcher and downloader"
Expand All @@ -22,46 +22,46 @@ members = [".", "cli"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
reqwest = { version = "0.12.3", features = [
reqwest = { version = "0.12.4", features = [
"cookies",
"gzip",
], default-features = false }
scraper = "0.19.0"
serde = "1.0.197"
serde_json = "1.0.114"
serde = "1.0.202"
serde_json = "1.0.117"
serde_qs = "0.13.0"
regex = "1.10.3"
url = "2.5.0"
urlencoding = "2.1.3"
thiserror = "1.0.57"
thiserror = "1.0.60"
derive_more = "0.99.17"
derivative = "2.2.0"
once_cell = "1.19.0"
tokio = { version = "1.36.0", default-features = false, features = ["sync"] }
tokio = { version = "1.37.0", default-features = false, features = ["sync"] }
rand = "0.8.5"
reqwest-middleware = { version = "0.3.0", features = ["json"] }
reqwest-middleware = { version = "0.3.1", features = ["json"] }
reqwest-retry = "0.5.0"
m3u8-rs = "6.0.0"
async-trait = "0.1.77"
async-trait = "0.1.80"
aes = "0.8.4"
cbc = { version = "0.1.2", features = ["std"] }
hex = "0.4.3"
boa_engine = "0.17.3"
mime = "0.3.17"
bytes = "1.5.0"
bytes = "1.6.0"
flame = { version = "0.2.2", optional = true }
flamer = { version = "0.5.0", optional = true }

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

[features]
default = ["search", "live", "default-tls"]
performance_analysis = ["flame", "flamer"]
live = ["tokio/time", "tokio/process"]
blocking = ["tokio/rt", "tokio/rt-multi-thread"]
search = []
ffmpeg = ["tokio/process"]
ffmpeg = ["tokio/process", "tokio/io-util"]
default-tls = ["reqwest/default-tls"]
native-tls = ["reqwest/native-tls"]
rustls-tls = ["reqwest/rustls-tls"]
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ Download videos **blazing-fast** without getting stuck on Youtube download speed

## Roadmap

- [x] ffmpeg feature
- [ ] benchmarks

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

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

```toml
[dependencies]
rusty_ytdl = "0.7.1"
rusty_ytdl = "0.7.2"
```
2 changes: 0 additions & 2 deletions src/info.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::{collections::HashMap, sync::Arc};

use scraper::{Html, Selector};

use crate::constants::{BASE_URL, FORMATS};
Expand Down
174 changes: 173 additions & 1 deletion src/stream/streams/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,33 @@
mod live;
mod non_live;

use async_trait::async_trait;
use bytes::Bytes;

#[cfg(feature = "ffmpeg")]
use bytes::BytesMut;

#[cfg(feature = "ffmpeg")]
use std::{process::Stdio, sync::Arc};

#[cfg(feature = "ffmpeg")]
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
process::{Child, Command},
sync::{
mpsc::{channel, Receiver},
Mutex, Notify,
},
task::JoinHandle,
};

#[cfg(feature = "live")]
pub use live::{LiveStream, LiveStreamOptions};
pub use non_live::{NonLiveStream, NonLiveStreamOptions};

#[cfg(feature = "ffmpeg")]
use crate::constants::DEFAULT_HEADERS;
use crate::VideoError;
use async_trait::async_trait;

#[async_trait]
pub trait Stream {
Expand All @@ -25,3 +44,156 @@ pub trait Stream {
0
}
}

#[cfg(feature = "ffmpeg")]
pub struct FFmpegStreamOptions {
pub client: reqwest_middleware::ClientWithMiddleware,
pub link: String,
pub content_length: u64,
pub dl_chunk_size: u64,
pub start: u64,
pub end: u64,
pub ffmpeg_args: Vec<String>,
}

#[cfg(feature = "ffmpeg")]
pub(crate) struct FFmpegStream {
pub refined_data_reciever: Option<Arc<Mutex<Receiver<Bytes>>>>,
download_notify: Arc<Notify>,
ffmpeg_child: Child,

tasks: Vec<JoinHandle<Result<(), VideoError>>>,
}

#[cfg(feature = "ffmpeg")]
impl FFmpegStream {
pub fn new(options: FFmpegStreamOptions) -> Result<Self, VideoError> {
let (tx, mut rx) = channel::<Bytes>(16384);
let (refined_tx, refined_rx) = channel::<Bytes>(16384);

// Spawn FFmpeg process
let mut ffmpeg_child = Command::new("ffmpeg")
.args(&options.ffmpeg_args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.kill_on_drop(true)
.spawn()
.map_err(|x| VideoError::FFmpeg(x.to_string()))?;

let mut stdin = ffmpeg_child.stdin.take().unwrap();
let mut stdout = ffmpeg_child.stdout.take().unwrap();

let read_stdout_task = tokio::spawn(async move {
let mut buffer = vec![0u8; 16384];
while let Ok(line) = stdout.read(&mut buffer).await {
match line {
0 => {
break;
}
n => {
if let Err(_err) = refined_tx.send(Bytes::from(buffer[..n].to_vec())).await
{
return Err(VideoError::FFmpeg("channel closed".to_string()));
// Error or channel closed
};
}
}
}

Ok(())
});

let write_stdin_task = tokio::spawn(async move {
while let Some(data) = rx.recv().await {
if let Err(err) = stdin.write_all(&data).await {
return Err(VideoError::FFmpeg(err.to_string())); // Error or channel closed
}
}
Ok(())
});

let download_notify = Arc::new(Notify::new());
let download_notify_task = download_notify.clone();

let download_task = tokio::spawn(async move {
let mut end = options.end;
let mut start = options.start;
let content_length = options.content_length;
let client = options.client;
let link = options.link;
let dl_chunk_size = options.dl_chunk_size;

download_notify_task.notified().await;

loop {
// Nothing else remain send break to finish
if end == 0 {
break;
}

if end >= content_length {
end = 0;
}

let mut headers = DEFAULT_HEADERS.clone();

let range_end = if end == 0 {
"".to_string()
} else {
end.to_string()
};

headers.insert(
reqwest::header::RANGE,
format!("bytes={}-{}", start, range_end).parse().unwrap(),
);

let mut response = client
.get(&link)
.headers(headers)
.send()
.await
.map_err(VideoError::ReqwestMiddleware)?;

let mut buf: BytesMut = BytesMut::new();

while let Some(chunk) = response.chunk().await.map_err(VideoError::Reqwest)? {
buf.extend(chunk);
}

if end != 0 {
start = end + 1;

end += dl_chunk_size;
}

tx.send(buf.into())
.await
.map_err(|x| VideoError::FFmpeg(x.to_string()))?;
}

Ok(())
});

Ok(Self {
refined_data_reciever: Some(Arc::new(Mutex::new(refined_rx))),
download_notify,
ffmpeg_child,
tasks: vec![download_task, write_stdin_task, read_stdout_task],
})
}

pub fn start_download(&self) {
self.download_notify.notify_one();
}
}

#[cfg(feature = "ffmpeg")]
impl Drop for FFmpegStream {
fn drop(&mut self) {
// kill tasks if they are still running
for handle in &self.tasks {
handle.abort();
}
}
}
Loading

0 comments on commit 317ba7e

Please sign in to comment.