附圖可以看到下載過程中 dram 的用量與下載的檔案大小約略一致(截圖時有時間差), 第三張圖可以看到 I/O次數為零, 代表下載過程中零回寫直到下載完成



use std::io::{self, Read, Write, BufWriter};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
// ========== 配置常數 ==========
const MAX_RAM_MB: usize = 2048;
const DISK_WRITE_BUFFER_SIZE: usize = 128 * 1024 * 1024; // 128MB
// 預設輸出路徑選項
const DEFAULT_PATH_OPTIONS: [(&str, &str); 2] = [
("1", "當前目錄"),
("2", "/data/data/com.termux/files/home/storage/downloads/linux"),
];
// ========== 工具函數 ==========
fn prompt_user(prompt: &str) -> String {
print!("{}", prompt);
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
input.trim().to_string()
}
fn prompt_user_with_default(prompt: &str, default_value: &str) -> String {
print!("{}", prompt);
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let trimmed = input.trim();
if trimmed.is_empty() {
default_value.to_string()
} else {
trimmed.to_string()
}
}
fn is_m3u8_url(url: &str) -> bool {
url.contains(".m3u8")
}
fn get_video_duration(url: &str) -> Option {
let output = Command::new("ffprobe")
.args(&[
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
url
])
.output()
.ok()?;
let duration_str = String::from_utf8_lossy(&output.stdout);
duration_str.trim().parse::().ok()
}
// ========== 路徑選擇函數 ==========
fn select_output_directory() -> PathBuf {
println!("\n=== 選擇輸出路徑 ===");
// 顯示預設選項
for (index, description) in &DEFAULT_PATH_OPTIONS {
println!("{}. {}", index, description);
}
println!("3. 自訂路徑");
loop {
let choice = prompt_user("\n請選擇 (1-3): ");
match choice.as_str() {
"1" => {
// 當前目錄
let current_dir = std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."));
println!("選擇: 當前目錄 ({})", current_dir.display());
return current_dir;
}
"2" => {
// 預設路徑
let path = PathBuf::from("/data/data/com.termux/files/home/storage/downloads/linux");
println!("選擇: {}", path.display());
// 檢查目錄是否存在,不存在則建立
if !path.exists() {
println!("目錄不存在,正在建立...");
if let Err(e) = std::fs::create_dir_all(&path) {
eprintln!("建立目錄失敗: {},將使用當前目錄", e);
return std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."));
}
}
return path;
}
"3" => {
// 自訂路徑
let custom_path = prompt_user("請輸入自訂路徑: ");
if custom_path.trim().is_empty() {
println!("路徑不能為空,請重新選擇");
continue;
}
let path = PathBuf::from(custom_path.trim());
println!("選擇: {}", path.display());
// 檢查目錄是否存在,不存在則建立
if !path.exists() {
println!("目錄不存在,正在建立...");
if let Err(e) = std::fs::create_dir_all(&path) {
eprintln!("建立目錄失敗: {},請重新選擇", e);
continue;
}
}
return path;
}
_ => {
println!("無效選項,請輸入 1、2 或 3");
}
}
}
}
// ========== 進度顯示函數 ==========
fn show_download_progress(
data_size: usize,
start_time: Instant,
last_update: &mut Instant,
prefix: &str
) -> bool {
if last_update.elapsed() < Duration::from_millis(500) {
return false;
}
let megabytes = data_size as f64 / (1024.0 * 1024.0);
let elapsed_seconds = start_time.elapsed().as_secs_f64().max(0.01);
let speed_mbps = megabytes / elapsed_seconds;
print!("\r{} | {:7.1} MB | {:5.2} MB/s", prefix, megabytes, speed_mbps);
io::stdout().flush().unwrap();
*last_update = Instant::now();
true
}
// ========== 核心下載函數 ==========
fn download_with_ram_buffering(url: &str, filename: &str, output_dir: &Path) {
println!("\n=== 啟動記憶體緩衝下載模式 (MKV 封裝) ===");
println!("預計寫入緩衝: {} MB", DISK_WRITE_BUFFER_SIZE / 1024 / 1024);
println!("輸出路徑: {}", output_dir.display());
let output_path = output_dir.join(format!("{}.mkv", filename));
// 階段 1: 下載影片到 RAM
println!("正在下載並暫存於 RAM...");
let video_data = match download_video_to_ram(url) {
Some(data) => data,
None => {
eprintln!("\n\n下載失敗,請檢查 URL 或更新 yt-dlp。");
return;
}
};
// 階段 2: 在 RAM 中重新封裝影片
println!("\n\n下載完成!正在進行 RAM 內 remux(建立索引)...");
match remux_in_ram(&video_data) {
Some(remuxed_data) => {
save_to_disk(&output_path, &remuxed_data, "Remux 完成!正在寫入硬碟(僅一次)...");
println!("\n完成!");
println!("檔案路徑: {}", output_path.display());
println!("總大小: {:.1} MB", remuxed_data.len() as f64 / 1024.0 / 1024.0);
}
None => {
eprintln!("Remux 失敗,直接寫入原始檔案");
save_to_disk(&output_path, &video_data, "寫入原始檔案...");
}
}
}
fn download_video_to_ram(url: &str) -> Option> {
let mut ytdlp_process = Command::new("yt-dlp")
.args(&[
"-f", "bestvideo+bestaudio/best",
"--merge-output-format", "mkv",
"--downloader", "ffmpeg",
"--downloader-args", "ffmpeg:-f matroska",
"-o", "-",
url
])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("無法啟動 yt-dlp");
let mut stdout = ytdlp_process.stdout.take()?;
let mut buffer = Vec::new();
let mut chunk = [0u8; 2 * 1024 * 1024]; // 2MB 塊
let start_time = Instant::now();
let mut last_progress_update = Instant::now();
loop {
match stdout.read(&mut chunk) {
Ok(0) => break, // 讀取結束
Ok(bytes_read) => {
buffer.extend_from_slice(&chunk[..bytes_read]);
show_download_progress(
buffer.len(),
start_time,
&mut last_progress_update,
"已下載"
);
}
Err(_) => break,
}
}
if !ytdlp_process.wait().expect("等待下載結束失敗").success() {
return None;
}
Some(buffer)
}
fn remux_in_ram(input_data: &[u8]) -> Option> {
let mut ffmpeg_process = Command::new("ffmpeg")
.args(&[
"-i", "pipe:0",
"-c", "copy",
"-f", "matroska",
"pipe:1"
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("無法啟動 ffmpeg remux");
let mut stdin = ffmpeg_process.stdin.take()?;
let mut stdout = ffmpeg_process.stdout.take()?;
// 在背景執行緒寫入資料
let input_clone = input_data.to_vec();
let write_thread = std::thread::spawn(move || {
stdin.write_all(&input_clone).expect("寫入 ffmpeg stdin 失敗");
});
// 讀取 remux 後的資料
let mut output_buffer = Vec::new();
let mut read_chunk = [0u8; 2 * 1024 * 1024];
while let Ok(bytes_read) = stdout.read(&mut read_chunk) {
if bytes_read == 0 {
break;
}
output_buffer.extend_from_slice(&read_chunk[..bytes_read]);
}
write_thread.join().expect("寫入執行緒失敗");
if ffmpeg_process.wait().expect("等待 ffmpeg 結束失敗").success() {
Some(output_buffer)
} else {
None
}
}
fn save_to_disk(file_path: &Path, data: &[u8], message: &str) {
println!("{}", message);
// 確保目錄存在
if let Some(parent) = file_path.parent() {
if !parent.exists() {
if let Err(e) = std::fs::create_dir_all(parent) {
eprintln!("建立目錄失敗: {}", e);
return;
}
}
}
let file = std::fs::File::create(file_path).expect("建立檔案失敗");
let mut writer = BufWriter::with_capacity(DISK_WRITE_BUFFER_SIZE, file);
writer.write_all(data).expect("寫入失敗");
writer.flush().expect("刷新失敗");
}
// ========== M3U8 專用下載函數 ==========
fn download_m3u8_with_ram(url: &str, filename: &str, output_dir: &Path) {
let trim_start = prompt_user_with_default("片頭裁切秒數(Enter = 0): ", "0")
.parse::()
.unwrap_or(0.0);
let trim_end = prompt_user_with_default("片尾裁切秒數(Enter = 0): ", "0")
.parse::()
.unwrap_or(0.0);
let output_path = output_dir.join(format!("{}.mp4", filename));
let video_data = match download_m3u8_to_ram(url, trim_start, trim_end) {
Some(data) => data,
None => return,
};
println!("\n下載完成,寫入檔案…");
// 確保目錄存在
if let Some(parent) = output_path.parent() {
if !parent.exists() {
if let Err(e) = std::fs::create_dir_all(parent) {
eprintln!("建立目錄失敗: {}", e);
return;
}
}
}
std::fs::write(&output_path, &video_data).expect("寫入檔案失敗");
println!("\n完成:{}({:.1} MB)",
output_path.display(),
video_data.len() as f64 / 1024.0 / 1024.0);
}
fn download_m3u8_to_ram(url: &str, trim_start: f64, trim_end: f64) -> Option> {
let mut ffmpeg_command = Command::new("ffmpeg");
ffmpeg_command
.arg("-loglevel").arg("error");
// 設定裁切參數
if trim_start > 0.0 {
ffmpeg_command.arg("-ss").arg(format!("{}", trim_start));
}
if trim_end > 0.0 {
println!("檢查影片長度...");
if let Some(total_duration) = get_video_duration(url) {
if total_duration > trim_end {
let end_time = total_duration - trim_end;
ffmpeg_command.arg("-to").arg(format!("{}", end_time));
println!("影片總長: {:.1} 秒,裁切後: {:.1} 秒",
total_duration, end_time - trim_start);
} else {
eprintln!("警告:影片總長 {:.1} 秒,裁切秒數不合法", total_duration);
return None;
}
} else {
eprintln!("無法取得影片長度,跳過片尾裁切");
}
}
ffmpeg_command
.args(&["-i", url])
.args(&["-c", "copy"])
.args(&["-f", "mpegts"])
.arg("pipe:1")
.stdout(Stdio::piped())
.stderr(Stdio::null());
println!("\n開始 RAM-only 下載 (純 m3u8 模式)");
let mut process = ffmpeg_command.spawn().expect("無法啟動 ffmpeg");
let mut stdout = process.stdout.take()?;
let mut buffer = Vec::new();
let mut chunk = [0u8; 64 * 1024];
let start_time = Instant::now();
let mut last_progress_update = Instant::now();
loop {
match stdout.read(&mut chunk) {
Ok(0) => break, // 讀取結束
Ok(bytes_read) => {
buffer.extend_from_slice(&chunk[..bytes_read]);
// 檢查 RAM 限制
if buffer.len() > MAX_RAM_MB * 1024 * 1024 {
eprintln!("\nRAM 已達上限,停止下載");
let _ = process.kill();
return None;
}
show_download_progress(
buffer.len(),
start_time,
&mut last_progress_update,
"下載中"
);
}
Err(e) => {
eprintln!("\n讀取錯誤: {}", e);
let _ = process.kill();
return None;
}
}
}
if !process.wait().unwrap().success() {
eprintln!("ffmpeg 執行失敗");
return None;
}
Some(buffer)
}
// ========== 主函數 ==========
fn main() {
println!("=== dlvideo-ram (SSD Protection Build) ===\n");
// 1. 選擇輸出路徑
let output_directory = select_output_directory();
// 2. 輸入影片資訊
let video_url = prompt_user("\n影片網址: ");
let output_filename = prompt_user("輸出檔名(不含副檔名): ");
// 3. 根據 URL 類型選擇下載方式
if is_m3u8_url(&video_url) {
download_m3u8_with_ram(&video_url, &output_filename, &output_directory);
} else {
download_with_ram_buffering(&video_url, &output_filename, &output_directory);
}
}




























































































