C#でmp4ファイルのチャプターテキストを作成するアプリ

コンピュータ
mp4ファイルを読み込んでチャプターを作成しテキストファイルとして保存するアプリです。
作成したテキストファイルはフリーウェアソフト「Drax」でmp4ファイルにチャプターをセットします。
Releases ?? stevie910/drax
Contribute to stevie910/drax development by creating an account on GitHub.

実行環境構築

プロジェクトの作成

mkdir ChapterEditor
cd ChapterEditor
dotnet new winforms
dotnet add package OpenCvSharp4.Windows
dotnet add package OpenCvSharp4.Extensions
dotnet add package OpenCvSharp4.runtime.win
code .

ソースプログラム

namespace ChapterEditor;

using System;
using System.Windows.Forms;

using OpenCvSharp;
using OpenCvSharp.Extensions;

public partial class Form1 : Form
{
    SplitContainer splitContainer = new SplitContainer
    {
        Dock = DockStyle.Fill,
        Panel1MinSize = 100,
    };
    PictureBox picbox = new PictureBox
    {
        Dock = DockStyle.Fill,
        AllowDrop = true,
        SizeMode = PictureBoxSizeMode.Zoom,
    };
    FlowLayoutPanel panel = new FlowLayoutPanel
    {
        Dock = DockStyle.Bottom,
    };
    TrackBar trackBar = new System.Windows.Forms.TrackBar
    {
        Value = 1,
        Maximum = 1,
        Enabled = false,
    };
    Button addButton = new Button
    {
        Width = 100,
        Height = 40,
        Text = "Add",
        Enabled = false,
    };
    Button saveButton = new Button
    {
        Width = 100,
        Height = 40,
        Text = "Save",
        Enabled = false,
    };
    ListBox chapterList = new ListBox
    {
        Dock = DockStyle.Fill,
        Sorted = true,
        Enabled = false,
    };
    VideoCapture? vc = null;
    string path = "";
    string chapter_text_path = "";

    string getChapterItem()
    {
        if (vc == null) return "";
        int msec = (int)((1000.0/vc.Fps) * trackBar.Value);
        var ts = new TimeSpan(0,0,0,0,msec);
        var s = ts.ToString(@"hh\:mm\:ss\.fff") + " " + trackBar.Value;
        return s;
    }
    private void readFrame()
    {
        if (vc == null) return;
        using var mat = new Mat();
        if (!vc.Read(mat)) return;
        if (!mat.IsContinuous()) return;
        picbox.Invoke(new Action(() =>  {
            if (picbox.Image != null) {
                picbox.Image.Dispose();
            }
            picbox.Image = BitmapConverter.ToBitmap(mat);
        }));
        Text = getChapterItem();
    }
    public Form1()
    {
        InitializeComponent();

        Controls.Add(splitContainer);
        splitContainer.Panel1.Padding = new Padding(10);
        splitContainer.Panel2.Padding = new Padding(10);
        splitContainer.Panel1.Controls.Add(picbox);
        splitContainer.Panel2.Controls.Add(chapterList);
        panel.Controls.Add(trackBar);
        panel.Controls.Add(addButton);
        panel.Controls.Add(saveButton);
        Controls.Add(panel);

        trackBar.ValueChanged += (s, e) => {
            if (vc == null) return;
            vc.PosFrames = trackBar.Value;

            readFrame();
        };
        picbox.DragEnter += (o, e) => {
            if (e.Data == null) return;
            if(!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
            e.Effect = DragDropEffects.Copy;
        };
        picbox.DragDrop += async (sender, e) => {
            // ファイルドロップ
            if (e.Data == null) return;
            if(!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
            string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
            if (files.Length < 1) return;
            path = files[0];

            // 読み込み
            if (picbox.Image != null) picbox.Image.Dispose();
            if (vc != null) vc.Dispose();
            await Task.Run(()=>{
                vc = new VideoCapture(path);
            });
            if (vc== null || !vc.IsOpened()) return;

            trackBar.Maximum = vc.FrameCount;
            trackBar.Value = (trackBar.Value > 0) ? 0 : trackBar.Maximum;

            var filename = Path.GetFileNameWithoutExtension(path) + ".chapter.txt";
            chapter_text_path = Path.GetDirectoryName(path) + "\\" + filename;
            chapterList.Items.Clear();
            if (File.Exists(chapter_text_path)) {
                using var sr = new StreamReader(chapter_text_path);
                var line = "";
                while ((line = sr.ReadLine()) != null)
                    chapterList.Items.Add(line);
            }
            else {
                chapterList.Items.Add("00:00:00.000 0");
            }

            chapterList.Enabled = true;
            trackBar.Enabled = true;
            addButton.Enabled = true;
            saveButton.Enabled = true;
        };
        addButton.Click += (sender, e) => {
            chapterList.Items.Add(getChapterItem());
        };
        saveButton.Click += (sender, e) => {
            using var sw = new StreamWriter(chapter_text_path);

            foreach(var line in chapterList.Items)
                sw.WriteLine(line);
        };
        chapterList.SelectedIndexChanged += (sender, e) => {
            if (chapterList.SelectedItem == null) return;
            
            var s = chapterList.SelectedItem.ToString();
            if (s == null) return;
            string[] arr = s.Split(' ');
            if (arr.Length < 2) return;
            trackBar.Value = int.Parse(arr[1]);
        };
        chapterList.KeyDown += (sender, e) => {
            if (chapterList.Enabled == false || chapterList.SelectedIndex < 1) return;
            if (e.KeyCode == Keys.Delete) chapterList.Items.RemoveAt(chapterList.SelectedIndex);
        };
        this.Load += (sender, e) => {
            trackBar.Width = (int)(panel.Width * 0.7);
        };
        this.Resize += (sender, e) => {
            addButton.Width = (int)(panel.Width * 0.1);
            saveButton.Width = (int)(panel.Width * 0.1);
            trackBar.Width = (int)(panel.Width * 0.7);
        };
    }// Form1
}// class

実行

dotnet run

使い方

mp4ファイルをドラッグアンドドロップします。
トラックバーを動かすと連動して動画の位置が移動します。
「Add」ボタンを押すと現在位置でチャプターアイテムが追加されます。
チャプターアイテムを選択するとトラックバーが移動します。
チャプターアイテムを選択した状態で「Delete」キーを押すとアイテムが削除されます。
「Save」ボタンを押すとチャプターがテキストファイルとして保存されます。保存場所は動画ファイルと同じ場所に拡張子が.chapter.txtで保存されます。
保存したテキストファイルは「Drax」で動画ファイルにチャプターをセットします。

コメント