728x90
반응형
오늘은 인스타 릴스를 보던 중, 레제춤을 아스키코드로 표현한 영상을 보게 되었습니다.
"나도 만들어 볼 수 있지않을까?
라는 생각을 하게 되었고, 이를 실천하기 위하여 프로젝트를 생성했습니다.
제가 현재 사용중인 Rust라는 언어를 사용하기로 했고, 터미널 출력은 rust의 라이브러리 중 하나인 Ratatui를 사용하기로 했습니다.
또한, youtube 영상을 가져올거기 때문에 yt-dlp.exe라는 프로그램을 다운로드 받고, 영상 출력을 하기 위하여 ffmpeg.exe라는 프로그램을 다운로드 받았습니다.
- video.rs
use std::{
io::Read,
process::{Child, Command, Stdio},
};
pub struct VideoStream {
ffmpeg: Child,
frame_size: usize,
pub width: usize,
pub height: usize,
}
impl VideoStream {
pub fn new(url: &str, width: u32, height: u32) -> anyhow::Result<Self> {
// yt-dlp → stdout
let mut ytdlp = Command::new("../tools/yt-dlp/yt-dlp.exe")
.args(["-f", "bestvideo[ext=mp4]/best", "-o", "-", url])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()?;
let ytdlp_stdout = ytdlp.stdout.take().unwrap();
// 터미널 문자 비율 보정
let char_aspect = 2.3;
let real_height = (height as f32 * char_aspect) as u32;
// 🔥 핵심: fps=30 강제
let ffmpeg = Command::new("../tools/ffmpeg/ffmpeg.exe")
.args([
"-i",
"pipe:0",
"-an",
"-vf",
&format!("scale={}:{},fps=30,format=rgb24", width, real_height),
"-f",
"rawvideo",
"pipe:1",
])
.stdin(Stdio::from(ytdlp_stdout))
.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()
}
}
다음과 같은 코드를 통하여 video를 가져오고
- axcii.rs
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use rayon::prelude::*; // Rayon 병렬 반복자 사용
const ASCII_TABLE: &[u8] = b" .:-=+*#%@";
pub fn rgb_to_colored_ascii(rgb: &[u8], width: usize, height: usize, out: &mut Vec<Line>) {
// 기존 벡터 재사용 대신 병렬 처리 결과를 수집
// par_chunks_exact 등을 사용하여 y 단위로 병렬 처리 가능하지만,
// Rayon은 collect()가 순서를 보장하므로 아래와 같이 작성 가능합니다.
let lines: Vec<Line> = (0..height - 1)
.into_par_iter() // 병렬 이터레이터로 변경
.step_by(2)
.map(|y| {
let mut spans = Vec::with_capacity(width);
let mut x = 0;
let table_len = ASCII_TABLE.len();
// ============================
// SIMD path (4 pixels) - 로직 동일
// ============================
while x + 4 <= width {
// ... (기존 로직과 동일) ...
let mut lum = [0u8; 4];
let mut colors = [(0u8, 0u8, 0u8); 4];
for i in 0..4 {
let idx1 = (y * width + (x + i)) * 3;
let idx2 = ((y + 1) * width + (x + i)) * 3;
// 범위 체크 없이 접근 (unsafe)를 사용하면 더 빠르지만 안전을 위해 유지
let r = (rgb[idx1] as u16 + rgb[idx2] as u16) >> 1;
let g = (rgb[idx1 + 1] as u16 + rgb[idx2 + 1] as u16) >> 1;
let b = (rgb[idx1 + 2] as u16 + rgb[idx2 + 2] as u16) >> 1;
colors[i] = (r as u8, g as u8, b as u8);
lum[i] = ((54 * r + 183 * g + 19 * b) >> 8) as u8;
}
for i in 0..4 {
let idx = lum[i] as usize * (table_len - 1) / 255;
let ch = ASCII_TABLE[idx] as char;
let (r, g, b) = colors[i];
// Style::default() 호출 비용을 줄이려면 미리 상수로 정의하거나 재사용 고려
spans.push(Span::styled(
ch.to_string(),
Style::new().fg(Color::Rgb(
(r as f32 * 0.8) as u8,
(g as f32 * 0.8) as u8,
(b as f32 * 0.8) as u8,
)),
));
}
x += 4;
}
// ============================
// Scalar fallback - 로직 동일
// ============================
while x < width {
// ... (기존 로직과 동일) ...
let i1 = (y * width + x) * 3;
let i2 = ((y + 1) * width + x) * 3;
let r = ((rgb[i1] as u16 + rgb[i2] as u16) >> 1) as u8;
let g = ((rgb[i1 + 1] as u16 + rgb[i2 + 1] as u16) >> 1) as u8;
let b = ((rgb[i1 + 2] as u16 + rgb[i2 + 2] as u16) >> 1) as u8;
let l = ((54 * r as u16 + 183 * g as u16 + 19 * b as u16) >> 8) as u8;
let idx = l as usize * (table_len - 1) / 255;
let ch = ASCII_TABLE[idx] as char;
spans.push(Span::styled(
ch.to_string(),
Style::new().fg(Color::Rgb(
(r as f32 * 0.8) as u8,
(g as f32 * 0.8) as u8,
(b as f32 * 0.8) as u8,
)),
));
x += 1;
}
Line::from(spans)
})
.collect();
*out = lines;
}
다음과 같은 코드를 통하여 ascii 코드를 표현합니다.
- tui.rs
use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, backend::CrosstermBackend};
use std::io::stdout;
pub type Tui = Terminal<CrosstermBackend<std::io::Stdout>>;
pub fn init() -> anyhow::Result<Tui> {
enable_raw_mode()?;
let backend = CrosstermBackend::new(stdout());
Ok(Terminal::new(backend)?)
}
pub fn restore() -> anyhow::Result<()> {
disable_raw_mode()?;
execute!(stdout(), crossterm::terminal::LeaveAlternateScreen)?;
Ok(())
}
이렇게 ratatui를 통하여 화면에 보여주는 방식으로 진행할 예정입니다.
- 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<()> {
println!("YouTube URL:");
let mut url = String::new();
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)?;
let mut video = VideoStream::new(url, width, height)?;
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(())
}
해당 메인문을 실행하면, youtube링크를 입력받고, 이를 로딩한 후, 터미널에서 아스키코드로 영상을 표현하는 프로젝트를 완성하게 됩니다.
해당 프로젝트에 대한 최신 코드는 여기서 만나보실 수 있습니다.
728x90
반응형
'Rust > 프로젝트' 카테고리의 다른 글
| 터미널에서 영상 시청하는 프로젝트 (2) (0) | 2026.01.13 |
|---|---|
| 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 |
댓글