皆さんYouTubeは見ますか?
筆者は毎日のように見ています。
というかながら見とかしてるのでPCの前に居る時はほぼ確実に見ていますね。
今回はそんなYouTube中毒になってしまった筆者が「VTuber動画の切り抜きを簡単にやりたい!」というわがままを技術で解決すべく作った切り抜きツールの備忘録的な記事です。
YouTubeの動画を楽に切り抜きたい!
「VTuberの配信のおもしろシーンをとにかく楽して切り抜きたい!」と誰しもが思ったことがあると思います。
そもそもは
筆者「スマホとかのモバイル端末で動画編集するのはめんどいなぁ・・・」
↓
筆者「せや、ブラウザから簡単に切り抜けるいい感じのページを作ろ!」
という感じで作り始めました。
YouTubeの動画をとにかく楽して切り抜くためにPythonでなんかいい感じのライブラリがないか探ってみます。
動画をダウンロード→切り抜き→動画を出力
今回やりたいのは単純で、動画をダウンロードして指定した時間を切り抜いてから再度動画にエンコードするというもの。
配信のアーカイブなんかの長い動画から数十秒、数分の動画を切り抜くために動画全体をダウンロードするので正直効率は微妙です。
ですが、今回は家で筆者のみが使うシステムなのでそのへんの効率とかは度外視します。
とにかく動画を簡単にダウンロードする
とにかく簡単に動画をダウンロードするために、Pythonでサクッと動画をダウンロードできるライブラリを探していると「youtube-dl」といういかにもダウンロードできそうなライブラリを見つけました。
Pythonのパッケージ管理ソフト「pip」で簡単にインストールできるみたいなのでとりあえずインストールしてみます。
sudo -H pip install --upgrade youtube-dl
インストールが終わったらシェル上で
youtube-dl [ダウンロードしたい動画のURL]
とするだけでとにかく動画のダウンロードができるようになりました。
ちなみにyoutube-dlという名前ですが、YouTube以外にも様々な動画サイトに対応しており、niconico動画やTwitterの動画なんかもダウンロードできます。
youtube-dlが対応しているサイトは以下ページを見てください。
Pythonで動画をカットする
Pythonで動画をカットするためにMoviePyというライブラリを使う予定でしたが、結論から言うとカット後に再エンコードが必要で処理速度に不満があったので最終的にはffmpegを採用しました。
ffmpegというとエンコードに使われるライブラリのイメージですが、実はカットや音声ファイルの抽出などの簡単な編集もできます。
ちなみにMoviePyだとテキストを入れたりなんかの結構本格的な編集も可能なので、使い方次第では動画の音声を勝手に文字起こししてテロップを自動生成するなんてこともできそうです。
動画編集って臨機応変に色々考えないといけないものですが、MoviePyを使えばある程度自動化できてしまうかもしれません。
ffmpegのインストールはaptコマンドでサクッとしておきます。
sudo apt install ffmpeg
解像度が720p程度になってしまう
youtube-dlをインストールして適当にダウンロードすると最高画質ではなく、HD(1280×720, 720p)程度の画質でダウンロードされてしまいます。
デフォルトでは映像と音声が一緒になった状態でダウンロード出来る画質を自動的に選択するので画質が低めに設定されるようです。
楽して切り抜きを作りたいですが、だからといって画質を犠牲にはしたくなかったのでFHD(1920×1080, 1080p)程度でダウンロード出来るようにしてみたいと思います。
解像度はダウンロード時に渡すパラメーター「format
」で設定するのですが、デフォルトだとbest
になっています。
これは「映像と音声が一緒になった状態でダウンロード出来る中で一番いい画質」という意味になり、YouTubeだと大体の場合は720p程度の画質になります。
画質をより高くするには映像を音声を別にダウンロードしてから結合する必要があるのですが、適当に指定すると映像と音声の形式の相性などの問題でうまくダウンロード出来ないという問題が発生します。
youtube-dlを使いやすくするScript – Qiita
こちらの記事を参考に取得出来る画質の一覧を出力してみるとわかりますが、ダウンロードしたい形式の数字を入れる必要があります。
youtube-dl --list-format <動画のIDもしくはURL>
$ youtube-dl --list-format https://www.youtube.com/watch?v=pFgUluV_00s
[youtube] pFgUluV_00s: Downloading webpage
[info] Available formats for pFgUluV_00s:
format code extension resolution note
249 webm audio only tiny 56k , opus @ 50k (48000Hz), 1.54MiB
250 webm audio only tiny 73k , opus @ 70k (48000Hz), 2.03MiB
140 m4a audio only tiny 130k , m4a_dash container, mp4a.40.2@128k (44100Hz), 3.95MiB
251 webm audio only tiny 144k , opus @160k (48000Hz), 4.01MiB
394 mp4 256x144 144p 87k , av01.0.00M.08, 30fps, video only, 2.38MiB
278 webm 256x144 144p 107k , webm container, vp9, 30fps, video only, 2.85MiB
160 mp4 256x144 144p 130k , avc1.4d400c, 30fps, video only, 3.10MiB
395 mp4 426x240 240p 185k , av01.0.00M.08, 30fps, video only, 5.06MiB
242 webm 426x240 240p 241k , vp9, 30fps, video only, 6.36MiB
133 mp4 426x240 240p 265k , avc1.4d4015, 30fps, video only, 4.62MiB
396 mp4 640x360 360p 327k , av01.0.01M.08, 30fps, video only, 8.84MiB
243 webm 640x360 360p 464k , vp9, 30fps, video only, 11.48MiB
134 mp4 640x360 360p 575k , avc1.4d401e, 30fps, video only, 8.64MiB
397 mp4 854x480 480p 579k , av01.0.04M.08, 30fps, video only, 15.21MiB
244 webm 854x480 480p 781k , vp9, 30fps, video only, 20.23MiB
135 mp4 854x480 480p 791k , avc1.4d401f, 30fps, video only, 12.54MiB
398 mp4 1280x720 720p 1213k , av01.0.05M.08, 30fps, video only, 29.36MiB
136 mp4 1280x720 720p 1426k , avc1.4d401f, 30fps, video only, 21.86MiB
247 webm 1280x720 720p 1554k , vp9, 30fps, video only, 39.99MiB
399 mp4 1920x1080 1080p 2172k , av01.0.08M.08, 30fps, video only, 51.80MiB
248 webm 1920x1080 1080p 2708k , vp9, 30fps, video only, 71.32MiB
137 mp4 1920x1080 1080p 4378k , avc1.640028, 30fps, video only, 74.46MiB
18 mp4 640x360 360p 711k , avc1.42001E, mp4a.40.2@ 96k (44100Hz), 21.67MiB
22 mp4 1280x720 720p 846k , avc1.64001F, mp4a.40.2@192k (44100Hz) (best)
と言った具合に数字が出てくるので、これを組み合わせることで画質と音質の指定が出来ます。
ですが、video only
やaudio only
と書かれているものは映像のみ・音声のみをダウンロードするための設定です。
さらに楽に切り抜きたいということで作っているので、出力ファイルは手軽に扱えるmp4で出力したい→つまりmp4でダウンロードしたいという前提があるので映像はmp4、音声はm4aでダウンロードしたいのです。
上の数字から選ぶと映像は399 mp4 1920x1080 1080p 2172k , av01.0.08M.08, 30fps, video only, 51.80MiB
を、音声は140 m4a audio only tiny 130k , m4a_dash container, mp4a.40.2@128k (44100Hz), 3.95MiB
を選択したいので、format
には399+140
という値を渡せばいいのですが、中にはそのフォーマットをサポートしてない動画もあるので面倒です。
ということで、最終的には以下のようになりました。
ydl_opts = {
'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best',
'outtmpl': 'download/'+'%(id)s.%(ext)s',
'merge_output_format': 'mp4',
'add-metadata': True
}
format
にはbestvideo[ext=mp4]+bestaudio[ext=m4a]/best
を指定しました。
bestvideo
で一番画質の良いフォーマットを指定していますが、[ext=mp4]
をつけることで「mp4の中で一番画質の良いフォーマット」という指定をしています。
同じくbestaudio[ext=m4a]
を指定することで「m4aでダウンロードできる中で一番良いフォーマット」を指定しています。
その後ろの/best
は「1つ目の設定が存在しない時はbest
(デフォルト)を使用する」という意味です。
/
で区切ると先頭から実行して指定したフォーマットがない場合は次のフォーマットを試すという意味になります。
ffmpegで動画をカットする
ということで動画をダウンロードできたのでffmpegでダウンロードした動画を簡単に切り抜いてみようと思います。
ffmpeg -ss [切り抜き開始位置(秒)] -t [開始位置から切り取る秒数] -i [切り抜きたいファイル] -vcodec copy -acodec copy [出力ファイル名] -y
ffmpegを使ってシェルから簡単に切り抜く方法を見つけたのでPython上からシェルを実行してffmpegを実行します。
cmd = 'ffmpeg -ss ' + str(start) + ' -i ' + input_file + ' -t ' + \
str(end - start) + \
' -vcodec copy -acodec copy ' + outputfile + ' -y'
subprocess.check_call(cmd.split())
雑ですが、こんな感じでPythonからシェルコマンドを実行します。
オプションには変数を使うために文字列結合で無理やりつなげています。
もっとスマートな方法があると思いますが、調べるのがめんどいなので思考停止してこんな実装に・・・。
Pythonから簡単にWEB画面を作る
ということでだいたいの処理ができたのですが、この状態ではLinuxのシェルからしか起動できません。
ということで簡単にWEB画面を作ってみます。
Flaskというライブラリを使うことで簡単にWEBサーバーを立ち上げることができるみたいなので、それを利用します。
ただし、Flaskはプロトタイプ作成などに使う開発用ツールのようなので公開する予定のページには使わないようにしましょう。
セキュリティ的にあまり良くないとかどうとか・・・。
ということでできたのがこんな簡単な画面です。
URLと切り抜きたい時間の最初と最後を入れ、ダウンロードのみなんかもできるようにしてみました。
この辺は自分しか使わないのでわかればOKですね!
ソースコード
youtube.py
#!/usr/bin/env python3
import youtube_dl
import re
import sys
import os
import time
import subprocess
import lxml
from lxml import etree
from bs4 import BeautifulSoup
from urllib.request import urlopen, urlretrieve
from moviepy.editor import *
from flask import Flask, render_template, request, send_file, make_response, send_from_directory
from pydub import AudioSegment
import urllib.request
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/output', methods=['GET', 'POST'])
def output():
if request.method == 'POST':
download_only = 0
force = 0
if request.form['URL'] == "":
return render_template('index.html')
else:
URL = request.form['URL']
if request.form.get('download_only') == "download_only":
download_only = 1
if request.form.get('force') == "force":
force = 1
if request.form['start'] == "" and request.form['end'] == "":
download_only = 1
else:
if ':' in str(request.form['start']):
start = str(request.form['start']).split(":")
if len(start) == 2:
start = int(start[0]) * 60 + int(start[1])
elif len(start) == 3:
start = int(start[0]) * 3600 + \
int(start[1]) * 60 + int(start[2])
else:
start = int(request.form['start'])
if ":" in request.form['end']:
end = str(request.form['end']).split(":")
if len(end) == 2:
end = int(end[0]) * 60 + int(end[1])
elif len(end) == 3:
end = int(end[0]) * 3600 + \
int(end[1]) * 60 + int(end[2])
else:
end = int(request.form['end'])
if "https://www.youtube.com/" in URL and "list=" in URL:
th_list = URL.split("list=")[1]
else:
th_list = URL
if request.form['download_type'] == "mp4":
ydl_opts = {
'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best',
'outtmpl': 'download/'+'%(id)s.%(ext)s',
'merge_output_format': 'mp4',
'add-metadata': True
}
if request.form['download_type'] == "mp3":
ydl_opts = {
'format': 'bestaudio/best',
'outtmpl': 'download/' + '%(id)s.%(ext)s',
'postprocessors': [
{'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '320'},
{'key': 'FFmpegMetadata'},
],
}
ydl = youtube_dl.YoutubeDL(ydl_opts)
info_dict = ydl.extract_info(th_list, download=False)
# print(info_dict)
print(info_dict['id'])
print(info_dict['channel_id'])
print(info_dict['title'])
print(info_dict['thumbnail'])
if download_only == 1:
path = info_dict['id'] + "." + \
str(request.form['download_type'])
download_filename = info_dict['title'] + "_" + info_dict['id'] + "." + \
str(request.form['download_type'])
if download_only == 0:
path = info_dict['id'] + "_" + \
str(start) + "_" + str(end) + "." + \
str(request.form['download_type'])
download_filename = info_dict['title'] + "_" + \
info_dict['id'] + "_" + \
str(start) + "_" + str(end) + "." + \
str(request.form['download_type'])
if os.path.isfile("download/" + path) and force == 0:
return send_from_directory(os.getcwd() + "/download", path,
as_attachment=True, attachment_filename=download_filename)
else:
info_dict = ydl.extract_info(th_list, download=True)
urllib.request.urlretrieve(
info_dict['thumbnail'], "download/" + info_dict['id'] + ".jpg")
input_file = "download/" + info_dict['id'] + "." + \
str(request.form['download_type'])
if download_only == 1:
filename = info_dict['id'] + "." + \
str(request.form['download_type'])
download_filename = info_dict['title'] + \
"_" + info_dict['id'] + "." + \
str(request.form['download_type'])
elif download_only == 0 and request.form['download_type'] == "mp4":
filename = info_dict['id'] + "_" + \
str(start) + "_" + str(end) + ".mp4"
download_filename = info_dict['title'] + "_" + \
info_dict['id'] + "_" + \
str(start) + "_" + str(end) + ".mp4" # 編集後のファイル保存先とパス
outputfile = "download/" + \
info_dict['id'] + "_" + \
str(start) + "_" + str(end) + ".mp4"
cmd = 'ffmpeg -ss ' + str(start) + ' -i ' + input_file + ' -t ' + \
str(end - start) + \
' -vcodec copy -acodec copy ' + outputfile + ' -y'
subprocess.check_call(cmd.split())
elif download_only == 0 and request.form['download_type'] == "mp3":
filename = info_dict['id'] + "_" + \
str(start) + "_" + str(end) + ".mp3"
download_filename = info_dict['title'] + "_" + \
info_dict['id'] + "_" + \
str(start) + "_" + str(end) + ".mp3"
outputfile = "download/" + \
info_dict['id'] + "_" + \
str(start) + "_" + str(end) + ".mp3"
sound = AudioSegment.from_file(input_file, format="mp3")
sound1 = sound[start*1000:end*1000]
sound1.export(outputfile, format="mp3")
return send_from_directory(os.getcwd() + "/download", filename,
as_attachment=True, attachment_filename=download_filename)
else:
return render_template('index.html')
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8080, debug=True)
templates/index.html
{% extends "layout.html" %}
{% block content %}
<form action="/output" method="post">
<div>
<label>URL</label>
<input type="text" id="URL" name="URL">
</div>
<div>
<label>時間(秒数)</label>
開始<input type="tel" id="start" name="start"><br>
終了<input type="tel" id="end" name="end"><br>
ダウンロードのみ<input type="checkbox" id="download_only" name="download_only" value="download_only">
force<input type="checkbox" id="force" name="force" value="force">
<select name="download_type">
<option value="mp4" selected>mp4</option>
<option value="mp3">mp3</option>
</select>
</div>
<div>
<button>実行開始!</button>
</div>
</form>
{% endblock %}
templates/layout.html
<!doctype html>
<html>
<head>
<title>youtube切り抜きテスト</title>
</head>
<body>
{% block content %}
<!-- ここにメインコンテンツを書く -->
{% endblock %}
</body>
</html>
まとめ
思ったよりもハードルが低かった
作る前は結構時間かかるだろうなぁ・・・と思いながら重い腰を上げた感じでしたが、実際作ってみると1週間ぐらいで大体の機能は実装できました。
とりあえず動くようになるまでに掛かった時間は3日ぐらいなので、スピード感のある開発だったと思います。
世の中の偉い人たちがライブラリとかを作ってくれていることが多いので、先人の知恵を継ぎ接ぎしていけば意外と簡単に作れてしまいます。
それこそGithubとかでそれっぽいサンプルコードを探してパクって拝借すれば意外と簡単に作れてしまいます。
課題は高速化
今回のツールは、切り抜きたい動画をダウンロードしてから切り抜き処理を行うので、どうしても時間がかかってしまいます。
切り抜き処理自体は数秒程度で終わるほど高速なので、動画全体をダウンロードしなくても済むようにできれば高速化できそうですが・・・。
あとはエンコードしないで動画を出力するという特性からか、指定時間より動画が短くなってしまったり、動画の最後の方のデータが壊れてしまって映像だけ止まってしまうことがあったりと不具合はちょいちょいあります。
ですが、意外と簡単に作れたので満足しました。
完全に自己満足でしたが、意外とフォーマットの指定のあたりはネットにも情報がなかったので記事にしました。
誰かの役に立てれば幸いです。
コメント