Skip to content

Commit 3f4c042

Browse files
committed
06.03.2024
* FFmpeg feature implemented (Only non-live videos)
1 parent a2dad17 commit 3f4c042

File tree

9 files changed

+256
-95
lines changed

9 files changed

+256
-95
lines changed

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,17 @@ hex = "0.4.3"
4949
unicode-segmentation = "1.11.0"
5050
boa_engine = "0.17.3"
5151
mime = "0.3.17"
52+
bytes = "1.5.0"
5253

5354
[dev-dependencies]
5455
tokio = { version = "1.36.0", features = ["full"] }
5556

5657
[features]
57-
default = ["search", "live", "default-tls"]
58+
default = ["search", "live", "default-tls", "ffmpeg"]
5859
live = ["tokio/time", "tokio/process"]
5960
blocking = ["tokio/rt", "tokio/rt-multi-thread"]
6061
search = []
62+
ffmpeg = ["tokio/process"]
6163
default-tls = ["reqwest/default-tls"]
6264
native-tls = ["reqwest/native-tls"]
6365
rustls-tls = ["reqwest/rustls-tls"]

src/blocking/info.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ use crate::Video as AsyncVideo;
1010
use super::stream::LiveStreamOptions;
1111
use super::stream::{NonLiveStreamOptions, Stream};
1212

13+
#[cfg(feature = "ffmpeg")]
14+
use crate::structs::FFmpegArgs;
15+
1316
#[derive(Clone, Debug, derive_more::Display, PartialEq, Eq)]
1417
pub struct Video(AsyncVideo);
1518

@@ -53,7 +56,10 @@ impl Video {
5356
/// println!("{:#?}", chunk);
5457
/// }
5558
/// ```
56-
pub fn stream(&self) -> Result<Box<dyn Stream + Send + Sync>, VideoError> {
59+
pub fn stream(
60+
&self,
61+
#[cfg(feature = "ffmpeg")] ffmpeg_args: Option<FFmpegArgs>,
62+
) -> Result<Box<dyn Stream + Send + Sync>, VideoError> {
5763
let client = self.0.get_client();
5864

5965
let options = self.0.get_options();
@@ -124,14 +130,28 @@ impl Video {
124130
dl_chunk_size,
125131
start,
126132
end,
133+
#[cfg(feature = "ffmpeg")]
134+
ffmpeg_args,
127135
})?;
128136

129137
Ok(Box::new(stream))
130138
}
131139

132140
/// Download video directly to the file
133-
pub fn download<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), VideoError> {
134-
Ok(block_async!(self.0.download(path))?)
141+
pub fn download<P: AsRef<std::path::Path>>(
142+
&self,
143+
path: P,
144+
#[cfg(feature = "ffmpeg")] ffmpeg_args: Option<FFmpegArgs>,
145+
) -> Result<(), VideoError> {
146+
#[cfg(feature = "ffmpeg")]
147+
{
148+
Ok(block_async!(self.0.download(path, ffmpeg_args))?)
149+
}
150+
151+
#[cfg(not(feature = "ffmpeg"))]
152+
{
153+
Ok(block_async!(self.0.download(path))?)
154+
}
135155
}
136156

137157
/// Get video URL

src/info.rs

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ use crate::stream::{NonLiveStream, NonLiveStreamOptions, Stream};
1515

1616
use crate::structs::{VideoError, VideoFormat, VideoInfo, VideoOptions};
1717

18+
#[cfg(feature = "ffmpeg")]
19+
use crate::structs::FFmpegArgs;
20+
1821
use crate::utils::{
1922
add_format_meta, between, choose_format, clean_video_details, get_functions, get_html,
2023
get_html5player, get_random_v6_ip, get_video_id, is_not_yet_broadcasted, is_play_error,
@@ -330,7 +333,10 @@ impl Video {
330333
/// println!("{:#?}", chunk);
331334
/// }
332335
/// ```
333-
pub async fn stream(&self) -> Result<Box<dyn Stream + Send + Sync>, VideoError> {
336+
pub async fn stream(
337+
&self,
338+
#[cfg(feature = "ffmpeg")] ffmpeg_args: Option<FFmpegArgs>,
339+
) -> Result<Box<dyn Stream + Send + Sync>, VideoError> {
334340
let client = &self.client;
335341

336342
let info = self.get_info().await?;
@@ -364,11 +370,12 @@ impl Video {
364370
}
365371
}
366372

367-
let dl_chunk_size = if self.options.download_options.dl_chunk_size.is_some() {
368-
self.options.download_options.dl_chunk_size.unwrap()
369-
} else {
370-
1024 * 1024 * 10_u64 // -> Default is 10MB to avoid Youtube throttle (Bigger than this value can be throttle by Youtube)
371-
};
373+
let dl_chunk_size = self
374+
.options
375+
.download_options
376+
.dl_chunk_size
377+
// 1024 * 1024 * 10_u64 -> Default is 10MB to avoid Youtube throttle (Bigger than this value can be throttle by Youtube)
378+
.unwrap_or(1024 * 1024 * 10_u64);
372379

373380
let start = 0;
374381
let end = start + dl_chunk_size;
@@ -402,15 +409,32 @@ impl Video {
402409
dl_chunk_size,
403410
start,
404411
end,
412+
#[cfg(feature = "ffmpeg")]
413+
ffmpeg_args,
405414
})?;
406415

407416
Ok(Box::new(stream))
408417
}
409418

410419
/// Download video directly to the file
411-
pub async fn download<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), VideoError> {
420+
pub async fn download<P: AsRef<std::path::Path>>(
421+
&self,
422+
path: P,
423+
#[cfg(feature = "ffmpeg")] ffmpeg_args: Option<FFmpegArgs>,
424+
) -> Result<(), VideoError> {
412425
use std::io::Write;
413-
let stream = self.stream().await.unwrap();
426+
427+
let stream: Box<dyn Stream + Send + Sync>;
428+
429+
#[cfg(not(feature = "ffmpeg"))]
430+
{
431+
stream = self.stream().await.unwrap();
432+
}
433+
434+
#[cfg(feature = "ffmpeg")]
435+
{
436+
stream = self.stream(ffmpeg_args).await.unwrap();
437+
}
414438

415439
let mut file =
416440
std::fs::File::create(path).map_err(|e| VideoError::DownloadError(e.to_string()))?;

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ pub use structs::{
2323
RequestOptions, StoryBoard, Thumbnail, VideoDetails, VideoError, VideoFormat, VideoInfo,
2424
VideoOptions, VideoQuality, VideoSearchOptions,
2525
};
26+
27+
#[cfg(feature = "ffmpeg")]
28+
pub use structs::FFmpegArgs;
29+
2630
pub use utils::{choose_format, get_random_v6_ip, get_video_id};
2731
// export to access proxy feature
2832
pub use reqwest;

src/stream/streams/non_live.rs

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
use async_trait::async_trait;
2+
use tokio::sync::RwLock;
3+
14
use crate::constants::DEFAULT_HEADERS;
25
use crate::stream::streams::Stream;
36
use crate::structs::VideoError;
4-
use async_trait::async_trait;
5-
use tokio::sync::RwLock;
7+
8+
#[cfg(feature = "ffmpeg")]
9+
use crate::{structs::FFmpegArgs, utils::ffmpeg_cmd_run};
610

711
pub struct NonLiveStreamOptions {
812
pub client: Option<reqwest_middleware::ClientWithMiddleware>,
@@ -11,6 +15,9 @@ pub struct NonLiveStreamOptions {
1115
pub dl_chunk_size: u64,
1216
pub start: u64,
1317
pub end: u64,
18+
19+
#[cfg(feature = "ffmpeg")]
20+
pub ffmpeg_args: Option<FFmpegArgs>,
1421
}
1522

1623
pub struct NonLiveStream {
@@ -21,6 +28,13 @@ pub struct NonLiveStream {
2128
end: RwLock<u64>,
2229

2330
client: reqwest_middleware::ClientWithMiddleware,
31+
32+
#[cfg(feature = "ffmpeg")]
33+
ffmpeg_args: Option<FFmpegArgs>,
34+
#[cfg(feature = "ffmpeg")]
35+
ffmpeg_start_byte: RwLock<Vec<u8>>,
36+
#[cfg(feature = "ffmpeg")]
37+
ffmpeg_end_byte: RwLock<usize>,
2438
}
2539

2640
impl NonLiveStream {
@@ -52,6 +66,12 @@ impl NonLiveStream {
5266
dl_chunk_size: options.dl_chunk_size,
5367
start: RwLock::new(options.start),
5468
end: RwLock::new(options.end),
69+
#[cfg(feature = "ffmpeg")]
70+
ffmpeg_args: options.ffmpeg_args,
71+
#[cfg(feature = "ffmpeg")]
72+
ffmpeg_end_byte: RwLock::new(0),
73+
#[cfg(feature = "ffmpeg")]
74+
ffmpeg_start_byte: RwLock::new(vec![]),
5575
})
5676
}
5777

@@ -66,6 +86,16 @@ impl NonLiveStream {
6686
async fn start_index(&self) -> u64 {
6787
*self.start.read().await
6888
}
89+
90+
#[cfg(feature = "ffmpeg")]
91+
async fn ffmpeg_end_byte_index(&self) -> usize {
92+
*self.ffmpeg_end_byte.read().await
93+
}
94+
95+
#[cfg(feature = "ffmpeg")]
96+
async fn ffmpeg_start_byte_index(&self) -> Vec<u8> {
97+
self.ffmpeg_start_byte.read().await.to_vec()
98+
}
6999
}
70100

71101
#[async_trait]
@@ -100,13 +130,13 @@ impl Stream for NonLiveStream {
100130
.unwrap(),
101131
);
102132

103-
let response = self.client.get(&self.link).headers(headers).send().await;
104-
105-
if response.is_err() {
106-
return Err(VideoError::ReqwestMiddleware(response.err().unwrap()));
107-
}
108-
109-
let mut response = response.expect("IMPOSSIBLE");
133+
let mut response = self
134+
.client
135+
.get(&self.link)
136+
.headers(headers)
137+
.send()
138+
.await
139+
.map_err(VideoError::ReqwestMiddleware)?;
110140

111141
let mut buf: Vec<u8> = vec![];
112142

@@ -115,6 +145,39 @@ impl Stream for NonLiveStream {
115145
buf.extend(chunk.iter());
116146
}
117147

148+
#[cfg(feature = "ffmpeg")]
149+
{
150+
let ffmpeg_args = self
151+
.ffmpeg_args
152+
.clone()
153+
.map(|x| x.build())
154+
.unwrap_or_default();
155+
156+
if !ffmpeg_args.is_empty() {
157+
let ffmpeg_start_byte_index = self.ffmpeg_start_byte_index().await;
158+
159+
let cmd_output = ffmpeg_cmd_run(
160+
&ffmpeg_args,
161+
&[&ffmpeg_start_byte_index, buf.as_slice()].concat(),
162+
)
163+
.await?;
164+
165+
let end_index = self.ffmpeg_end_byte_index().await;
166+
167+
let mut first_buffer_trim = 1;
168+
if ffmpeg_start_byte_index.is_empty() {
169+
let mut start_byte = self.ffmpeg_start_byte.write().await;
170+
*start_byte = buf.clone();
171+
let mut end_byte = self.ffmpeg_end_byte.write().await;
172+
*end_byte = cmd_output.len();
173+
174+
first_buffer_trim = 0;
175+
}
176+
177+
buf = (cmd_output[(end_index + first_buffer_trim)..]).to_vec();
178+
}
179+
}
180+
118181
if end != 0 {
119182
let mut start = self.start.write().await;
120183
*start = end + 1;

src/structs.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,10 @@ pub enum VideoError {
259259
/// Downloading live streams not supported, compile with `live` feature to enable
260260
#[error("Downloading live streams not supported, compile with `live` feature to enable")]
261261
LiveStreamNotSupported,
262+
/// FFmpeg command error
263+
#[error("FFmpeg command error: {0}")]
264+
#[cfg(feature = "ffmpeg")]
265+
FFmpeg(String),
262266
}
263267

264268
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -650,3 +654,55 @@ impl<'de> Deserialize<'de> for MimeType {
650654
})
651655
}
652656
}
657+
658+
#[cfg(feature = "ffmpeg")]
659+
#[derive(Debug, Clone, PartialEq, Eq)]
660+
pub struct FFmpegArgs {
661+
pub format: Option<String>,
662+
pub audio_filter: Option<String>,
663+
pub video_filter: Option<String>,
664+
}
665+
666+
#[cfg(feature = "ffmpeg")]
667+
impl FFmpegArgs {
668+
pub fn build(&self) -> Vec<String> {
669+
let mut args: Vec<String> = vec![];
670+
671+
if let Some(format) = &self.format {
672+
args.push("-f".to_string());
673+
args.push(format.to_string());
674+
}
675+
676+
if let Some(audio_filter) = &self.audio_filter {
677+
args.push("-af".to_string());
678+
args.push(audio_filter.to_string());
679+
}
680+
681+
if let Some(video_filter) = &self.video_filter {
682+
args.push("-vf".to_string());
683+
args.push(video_filter.to_string());
684+
}
685+
686+
if self.format.is_some() || self.audio_filter.is_some() || self.video_filter.is_some() {
687+
args = [
688+
vec![
689+
// input as stdin
690+
"-i".to_string(),
691+
// aliases of pipe:0
692+
"-".to_string(),
693+
// "-analyzeduration".to_string(),
694+
// "0".to_string(),
695+
// "-loglevel".to_string(),
696+
// "0".to_string(),
697+
],
698+
args,
699+
]
700+
.concat();
701+
702+
// pipe to stdout
703+
args.push("pipe:1".to_string());
704+
}
705+
706+
args
707+
}
708+
}

src/utils.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use rand::Rng;
22
use regex::Regex;
33
use serde::{Deserialize, Serialize};
44
use std::collections::HashMap;
5+
use std::process::Stdio;
6+
use tokio::{io::AsyncWriteExt, process::Command};
57
use unicode_segmentation::UnicodeSegmentation;
68
use urlencoding::decode;
79

@@ -15,6 +17,30 @@ use crate::structs::{
1517
VideoOptions, VideoQuality, VideoSearchOptions,
1618
};
1719

20+
#[cfg(feature = "ffmpeg")]
21+
pub async fn ffmpeg_cmd_run(args: &Vec<String>, data: &[u8]) -> Result<Vec<u8>, VideoError> {
22+
let mut cmd = Command::new("ffmpeg");
23+
cmd.args(args)
24+
.stdin(Stdio::piped())
25+
.stdout(Stdio::piped())
26+
.kill_on_drop(true);
27+
28+
let mut process = cmd.spawn().map_err(|x| VideoError::FFmpeg(x.to_string()))?;
29+
let mut stdin = process
30+
.stdin
31+
.take()
32+
.ok_or(VideoError::FFmpeg("Failed to open stdin".to_string()))?;
33+
let cloned_data = data.to_owned();
34+
tokio::spawn(async move { stdin.write_all(&cloned_data).await });
35+
36+
let output = process
37+
.wait_with_output()
38+
.await
39+
.map_err(|x| VideoError::FFmpeg(x.to_string()))?;
40+
41+
Ok(output.stdout)
42+
}
43+
1844
#[allow(dead_code)]
1945
pub fn get_cver(info: &serde_json::Value) -> &str {
2046
info.get("responseContext")

0 commit comments

Comments
 (0)