cairoのrust バインディングがあっさり使えたので、それをffmpegでエンコードして動画にするツールを作ってみました。
ソースコードは gist に貼りました。ここ
実用ではなくて私のrustの練習用です。
cairo のrustバインディング
cairoは2Dグラフィックスのライブラリです。
Rustからcairoを使う方法はこのページに丁寧に書いてあります。
Intro to Cairo Graphics in Rust
Ubuntu 18.04上で、このページの通りにやってcairoのライブラリのインストールからRustでのサンプルプログラムの実行まで問題なくできました。
プログラムの動作
とても単純です。
1フレームごとにcairoのグラフィックスライブラリで描画して、そのビットマップデータを子プロセスとして起動しておいたffmpegに食わせてエンコードしてmp4の動画ファイルを作ります。
プログラムの実行方法
全てソースコード内にハードコードしています。引数なしで実行させると、1280x720 30fps で10秒間の動画を作成します。これを変更するにはmain関数の定数を変更してください。
描画内容を変えるには関数draw()を編集してください。
作成できたmp4の動画ファイルをYouTubeに貼りました。
https://www.youtube.com/watch?v=JVZBuGC_HNU
描画する関数draw()は以下の通り。引数f に毎回インクリメントされるフレーム番号が入ります。
全面を白で塗りつぶし、扇形の描画、文字列の描画を行っています。
fn draw(surface: &ImageSurface, f: i32) {
let cr = Context::new(surface);
let width = surface.get_width() as f64;
let height = surface.get_height() as f64;
let dx = f as f64 * 1.0;
cr.set_source_rgb(1.0, 1.0, 1.0);
cr.paint();
let cx = width / 2.0;
let cy = height / 2.0;
let r = cy;
let cstart = -0.5 * PI;
let cend = cstart + 2.0 * PI * ((f + 1) as f64) / 300.0;
cr.move_to(cx, cy);
cr.line_to(cx, 0.0);
cr.arc(cx, cy, r, cstart, cend);
cr.line_to(cx, cy);
cr.set_source_rgba(0.0, 0.5, 0.0, 0.2);
cr.fill();
cr.select_font_face(
"sans-serif",
cairo::FontSlant::Normal,
cairo::FontWeight::Normal,
);
cr.set_font_size(70.0);
cr.move_to(600.0 - dx, 100.0);
cr.set_source_rgb(0.0, 0.0, 1.0);
cr.show_text("Hello, world! 1234567890");
cr.fill();
}
子プロセスとしてffmpegを起動して、そこにピクセルデータを送り込むところは以下の通り。
fn make_cmdline(width: i32, height: i32, framerate: i32) -> String {
format!(
"ffmpeg -f rawvideo -pix_fmt bgra -s {width}x{height} -i - -pix_fmt yuv420p -r {framerate} -y out.mp4",
width = width,
height = height,
framerate = framerate
)
}
fn make_movie(width: i32, height: i32, framerate: i32, frames: i32) {
let mut surface =
ImageSurface::create(Format::ARgb32, width, height).expect("Couldn't create surface");
let mut child = Command::new("/bin/sh")
.args(&["-c", &make_cmdline(width, height, framerate)])
.stdin(Stdio::piped())
.spawn()
.expect("failed to execute child");
{
// limited borrow of stdin
let child_stdin = child.stdin.as_mut().expect("failed to get stdin");
(0..frames).for_each(|f| {
draw(&surface, f);
let d = surface.get_data().expect("Failed to get_data");
child_stdin.write_all(&d).expect("Failed to write to file");
});
}
child.wait().expect("child process wasn't running");
}
fn main() {
make_movie(1280, 720, 30, 300);
}
つまずいたところ
get_data()を使用可能にする条件
cairo::ImageSurface::get_data()
がリファレンスカウントをチェックしていて、それが1より大きいとNoExclusive だという理由でエラーになります。描画のためにこのsurfaceのcairo::Context
を作るとそれでリファレンスカウンタが増えます。なので、get_data()を成功させるには、それまでにcairo::Context
をドロップさせなければなりません。
この関係がなかなかわからずに苦労しました。
なお、cairo::Context
は内部に可変の状態を持っているので意味的にはmutable のはずです。しかし、状態を変更する部分が全て unsafe
に囲まれているのでRustコンパイラとしてはチェックができないためimmutableの扱いです。
ピクセルデータの順番
cairo::ImageSurface
ではARGBの32bitでピクセルを扱います。get_data()
ではこれを[u8]
として取り出せます。これをこのままwrite_all()
で書くのが一番簡単なのですが、そうすると32bit単位のデータを8bit単位で扱うことになるので、順番がひっくり返ります。ですが、ffmpeg ではピクセルデータのフォーマットの対応の幅が広くてBGRAの32bitも扱えるので、単にそのように指定するだけで済んでいます。
もしも順番を変えずに転送したい場合は[u8]
で取り出したデータのraw pointerをunsafeな方法で[u32]
にキャストして扱うことになると思います。
Future work
このツールは、時系列のデータの可視化に使えると思っています。GPSから取得した位置情報のログを地図データ上にプロットするとか。
8コアのマシンで実行してCPUの負荷状態を見たのですが、まだCPUを使い切っていません。マルチスレッド化して複数のCPUに描画を割り振るなど工夫の余地がたくさんあります。それは今後の楽しみに。