728x90
반응형
지난번 포스팅에서 유튜브 영상을 아스키 코드로 실행하는 프로젝트를 만들었었습니다.
그러나, 해당 프로젝트에서는 .exe 파일을 다운로드해서 불러와 사용한다는 불편한점이 있습니다.
해당 부분을 수정하기 위하여 확인해본 결과 yt-dlp.exe는 대체가 가능하다는 것이 확인되어, 해당 작업을 수행하였습니다.
rusty_ytdl이라는 라이브러리를 통하여 yt-dlp.exe없이 유튜브 영상을 실행할 수 있습니다.
[dependencies]
rusty_ytdl = "0.7.4"
다음과 같이 설정하여 프로젝트를 진행하고자 하였으나, 해당 버전에서는 문제가 발생하고 있는 상황이였습니다.
그래서 다음과 같이 버전명이 아닌 github를 import하는 방식으로 수정하여 해결하였습니다.
[dependencies]
rusty_ytdl = {git = "https://docs.rs/rusty_ytdl/latest/rusty_ytdl/"}
해당 라이브러리를 사용하면서 main.rs와 video.rs를 수정해주시면 됩니다.
- main.rs
mod app;
mod ascii;
mod tui;
mod video;
use std::{
io::{self, stdout},
time::{Duration, Instant},
};
use app::App;
use ascii::rgb_to_colored_ascii;
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::EnterAlternateScreen,
};
use ratatui::{
text::Text,
widgets::{Paragraph, Wrap},
};
use tokio::time::sleep;
use video::VideoStream;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut url = String::new();
println!("Input YouTube URL:");
io::stdin().read_line(&mut url)?;
let url = url.trim();
let width = 120;
let height = 50;
let frame_time = Duration::from_millis(27);
let mut app = App::new();
let mut terminal = tui::init()?;
execute!(stdout(), EnterAlternateScreen)?;
// VideoStream::new가 async 함수로 변경되었으므로 .await 추가
let mut video = VideoStream::new(url, width, height).await?;
let mut rgb_buf = Vec::new();
let mut ascii_lines = Vec::new();
while !app.should_quit {
let start = Instant::now();
if event::poll(Duration::from_millis(1))? {
if let Event::Key(k) = event::read()? {
match k.code {
KeyCode::Char('q') => app.should_quit = true,
KeyCode::Char(' ') => app.playing = !app.playing,
_ => {}
}
}
}
if app.playing {
if !video.read_frame(&mut rgb_buf) {
break;
}
// 🔥 문자 비율 보정은 ASCII 단계에서만
rgb_to_colored_ascii(&rgb_buf, video.width, video.height, &mut ascii_lines);
terminal.draw(|f| {
let p = Paragraph::new(Text::from(ascii_lines.clone())).wrap(Wrap { trim: false });
f.render_widget(p, f.area());
})?;
}
let elapsed = start.elapsed();
if elapsed < frame_time {
sleep(frame_time - elapsed).await;
}
}
tui::restore()?;
Ok(())
}
- video.rs
use std::{
io::Read,
process::{Child, Command, Stdio},
};
use rusty_ytdl::{Video, VideoOptions, VideoQuality, VideoSearchOptions};
pub struct VideoStream {
ffmpeg: Child,
frame_size: usize,
pub width: usize,
pub height: usize,
}
impl VideoStream {
pub async fn new(url: &str, width: u32, height: u32) -> anyhow::Result<Self> {
// rusty_ytdl을 사용하여 비디오 정보 가져오기
let video = Video::new(url)?;
let info = video.get_info().await?;
// 가장 좋은 화질의 비디오 포맷 선택 (오디오 제외, Video only)
// 수정: &info -> &info.formats
let format = rusty_ytdl::choose_format(&info.formats, &VideoOptions {
quality: VideoQuality::HighestVideo,
filter: VideoSearchOptions::Video,
..Default::default()
})?;
let stream_url = format.url.as_str();
// 터미널 문자 비율 보정
let char_aspect = 2.3;
let real_height = (height as f32 * char_aspect) as u32;
// 🔥 핵심: fps=30 강제
// yt-dlp 파이프 대신 직접 추출한 URL을 ffmpeg 입력으로 사용
let ffmpeg = Command::new("../tools/ffmpeg/ffmpeg.exe")
.args([
"-i",
stream_url, // URL 직접 전달
"-an",
"-vf",
&format!("scale={}:{},fps=30,format=rgb24", width, real_height),
"-f",
"rawvideo",
"pipe:1",
])
.stdin(Stdio::null()) // 입력 파이프 제거
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()?;
Ok(Self {
ffmpeg,
frame_size: (width * real_height * 3) as usize,
width: width as usize,
height: real_height as usize,
})
}
/// 그냥 "한 프레임"만 읽는다
pub fn read_frame(&mut self, buf: &mut Vec<u8>) -> bool {
buf.resize(self.frame_size, 0);
self.ffmpeg.stdout.as_mut().unwrap().read_exact(buf).is_ok()
}
}
이후, 영상만 재생되는게 너무 심심한나머지, 영상에 맞게 사운드를 추가하고 싶은 욕심이 생겼습니다.
영상에 사운드를 입히기 위해서는 rodio라는 라이브러리를 사용하면 됩니다.
[dependencies]
rodio = "0.19.0"
rodio를 사용하기 위해서는 main.rs와 video.rs를 수정하면 됩니다.
- main.rs
mod app;
mod ascii;
mod tui;
mod video;
use std::{
io::{self, stdout, Read},
time::{Duration, Instant},
};
use app::App;
use ascii::rgb_to_colored_ascii;
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::EnterAlternateScreen,
};
use ratatui::{
text::Text,
widgets::{Paragraph, Wrap},
};
// rodio 임포트 추가
use rodio::{OutputStream, Sink, Source};
use tokio::time::sleep;
use video::VideoStream;
// --- 추가된 구조체: 오디오 스트림 소스 정의 ---
// rodio가 이터레이터의 오디오 속성(채널, 샘플레이트)을 알 수 있도록 직접 Source를 구현합니다.
struct AudioStreamSource {
rx: std::sync::mpsc::IntoIter<i16>,
}
impl Iterator for AudioStreamSource {
type Item = i16;
fn next(&mut self) -> Option<Self::Item> {
self.rx.next()
}
}
impl Source for AudioStreamSource {
fn current_frame_len(&self) -> Option<usize> {
None // 길이를 알 수 없음 (스트리밍)
}
fn channels(&self) -> u16 {
2 // 스테레오
}
fn sample_rate(&self) -> u32 {
44100 // 44.1kHz
}
fn total_duration(&self) -> Option<Duration> {
None // 전체 길이를 알 수 없음
}
}
// ------------------------------------------
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut url = String::new();
println!("Input YouTube URL:");
io::stdin().read_line(&mut url)?;
let url = url.trim();
let width = 120;
let height = 50;
let frame_time = Duration::from_millis(30);
let mut app = App::new();
// 오디오 시스템 초기화
let (_stream, stream_handle) = OutputStream::try_default()?;
let sink = Sink::try_new(&stream_handle)?;
let mut terminal = tui::init()?;
execute!(stdout(), EnterAlternateScreen)?;
let (mut video, mut audio) = VideoStream::new(url, width, height).await?;
// --- 오디오 연결 로직 수정됨 ---
// mpsc 채널 생성
let (tx, rx) = std::sync::mpsc::channel::<i16>();
if let Some(mut stdout) = audio.ffmpeg.stdout.take() {
// 별도 스레드에서 ffmpeg 출력을 읽어 채널로 전송
std::thread::spawn(move || {
let mut buf = [0u8; 2];
while stdout.read_exact(&mut buf).is_ok() {
let sample = i16::from_le_bytes(buf);
if tx.send(sample).is_err() { break; }
}
});
}
// 커스텀 소스 사용
let source = AudioStreamSource { rx: rx.into_iter() };
sink.append(source);
sink.play();
// ----------------------------
let mut rgb_buf = Vec::new();
let mut ascii_lines = Vec::new();
while !app.should_quit {
let start = Instant::now();
if event::poll(Duration::from_millis(1))? {
if let Event::Key(k) = event::read()? {
match k.code {
KeyCode::Char('q') => app.should_quit = true,
KeyCode::Char(' ') => {
app.playing = !app.playing;
// 오디오 일시정지/재생 연동
if app.playing {
sink.play();
} else {
sink.pause();
}
},
_ => {}
}
}
}
if app.playing {
if !video.read_frame(&mut rgb_buf) {
break;
}
// 🔥 문자 비율 보정은 ASCII 단계에서만
rgb_to_colored_ascii(&rgb_buf, video.width, video.height, &mut ascii_lines);
terminal.draw(|f| {
let p = Paragraph::new(Text::from(ascii_lines.clone())).wrap(Wrap { trim: false });
f.render_widget(p, f.area());
})?;
}
let elapsed = start.elapsed();
if elapsed < frame_time {
sleep(frame_time - elapsed).await;
}
}
tui::restore()?;
Ok(())
}
- video.rs
use std::{
io::Read,
process::{Child, Command, Stdio},
};
use rusty_ytdl::{Video, VideoOptions, VideoQuality, VideoSearchOptions};
pub struct VideoStream {
ffmpeg: Child,
frame_size: usize,
pub width: usize,
pub height: usize,
}
// 오디오 프로세스를 별도로 관리하기 위한 구조체
pub struct AudioStream {
pub ffmpeg: Child,
}
impl VideoStream {
// 반환 타입을 변경하여 오디오 스트림도 함께 반환 (Video URL, Audio URL 각각 추출)
pub async fn new(url: &str, width: u32, height: u32) -> anyhow::Result<(Self, AudioStream)> {
// rusty_ytdl을 사용하여 비디오 정보 가져오기
let video = Video::new(url)?;
let info = video.get_info().await?;
// 1. 비디오 포맷 선택
let video_format = rusty_ytdl::choose_format(&info.formats, &VideoOptions {
quality: VideoQuality::HighestVideo,
filter: VideoSearchOptions::Video,
..Default::default()
})?;
// 2. 오디오 포맷 선택 (추가됨)
let audio_format = rusty_ytdl::choose_format(&info.formats, &VideoOptions {
quality: VideoQuality::HighestAudio,
filter: VideoSearchOptions::Audio,
..Default::default()
})?;
let video_url = video_format.url.as_str();
let audio_url = audio_format.url.as_str();
// 터미널 문자 비율 보정
let char_aspect = 2.3;
let real_height = (height as f32 * char_aspect) as u32;
// 비디오용 ffmpeg 실행
let ffmpeg_video = Command::new("../tools/ffmpeg/ffmpeg.exe")
.args([
"-i",
video_url,
"-an",
"-vf",
&format!("scale={}:{},fps=30,format=rgb24", width, real_height),
"-f",
"rawvideo",
"pipe:1",
])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()?;
// 오디오용 ffmpeg 실행 (추가됨)
// PCM s16le 포맷, 44.1kHz, 2채널로 디코딩하여 파이프로 전송
let ffmpeg_audio = Command::new("../tools/ffmpeg/ffmpeg.exe")
.args([
"-i",
audio_url,
"-vn", // 비디오 제외
"-f", "s16le", // Signed 16-bit Little Endian PCM
"-ac", "2", // 2채널 (Stereo)
"-ar", "44100", // 샘플 레이트
"-acodec", "pcm_s16le",
"pipe:1",
])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()?;
Ok((
Self {
ffmpeg: ffmpeg_video,
frame_size: (width * real_height * 3) as usize,
width: width as usize,
height: real_height as usize,
},
AudioStream {
ffmpeg: ffmpeg_audio,
}
))
}
/// 그냥 "한 프레임"만 읽는다
pub fn read_frame(&mut self, buf: &mut Vec<u8>) -> bool {
buf.resize(self.frame_size, 0);
self.ffmpeg.stdout.as_mut().unwrap().read_exact(buf).is_ok()
}
}
해당 버전까지 적용한 릴리즈 바이너리는 해당 사이트에서 다운로드 받으실 수 있습니다.
Release 1.1.0 · lms0806/tui_video
다음과 같은 사항이 수정되었습니다. 영상 사운드 추가 ascii 로직 변경 (싱글 스레드 -> 멀티 스레드) yt-dlp.exe 제거
github.com
728x90
반응형
'Rust > 프로젝트' 카테고리의 다른 글
| 터미널에서 영상 시청하는 프로젝트 (1) (0) | 2026.01.10 |
|---|---|
| Rust + wasm-pack + githuab pages로 배포하기 (0) | 2025.12.21 |
| Rust로 백엔드 개발기(feat. 메이플스토리) - 10 Not Found 처리, request 통합 (0) | 2025.04.13 |
| Rust로 백엔드 개발기(feat. 메이플스토리) - 09 Main 함수 정리 (0) | 2025.04.02 |
| Rust로 백엔드 개발기(feat. 메이플스토리) - 08 사용자별 요청 처리 (0) | 2025.03.31 |
댓글