🖼️

Misskeyメディアプロキシサーバを作ってみた

2021/12/12に公開

これは Misskey Advent Calendar 2021 12日目の記事です。

Misskeyはメディアプロキシをサポートしており、画像の圧縮やキャッシュもできます。
画像がよく流れるタイムラインを外で見るとモバイルデータをそこそこ食います。
そこで、画像を超圧縮するプロキシサーバーをPythonで作ってみることにしました。

Webサーバーフレームワークとして、Flaskを使用しています。
全体のコードは記事の最後に書きました。

仕様

Misskeyのメディアプロキシの設定は、.ymlファイルにあります。

mediaProxy: https://host-to.proxy

このようにアドレスを設定すると、https://host-to.proxy/proxy?url=[URL] という形で使われます。

サーバーでは /proxy へのリクエストを受け付けるようにします。

仕組み

プロキシしたいURLは、クエリとしてurlに指定されます。
これをサーバーが取得して、JPEGで圧縮して返してやります。

Pythonで書くなら、本当であればPillowを使うべきですが、慣れているFFmpegを使いました。
サーバーとして使う環境にFFmpegがインストールされている前提で進めていきます。

FFmpegのオプション

圧縮する際のオプションはこんな感じです。
ffmpeg -hide_banner -http_proxy PROXY_URL -i URL -vcodec mjpeg -qmax 20 -q 20 -vf "scale=360*dar:360" -f image2 -

オプション 意味
-http_proxy PROXY_URL 指定したプロキシサーバー経由でダウンロードする
-i URL オリジナルURL
-vcodec mjpeg コーデック選択(JPEG)
-qmax 20 品質最大20に限定 ※1
-q 20 品質20に指定 ※1
-vf "scale=360*dar:360" 画像サイズで、縦横比を維持したまま縦幅が360pxになるようにリサイズ
-f image2 - 標準出力に出力
※1: 品質の値は、デフォルトで2が最高、96が最低。

実装

このパラメータをPythonで使えるように変えて、subprocessで実行します。

data = subprocess.check_output(['ffmpeg', '-hide_banner', '-http_proxy', proxy_url, '-i', url, '-vcodec', 'mjpeg', '-qmax', '20', '-q', '20', '-vf', 'scale=360*dar:360', '-f', 'image2', '-'], stderr=subprocess.DEVNULL)

標準エラー出力を/dev/nullに流しているのは、FFmpegの変換ログを抑制するためです。(本当ならログレベル-loglevel で抑制すればよかったかも)

画像データは標準出力に流れてくるので、そのまま送信します。

return send_file(io.BytesIO(data), environ=request.environ, mimetype='image/jpeg', as_attachment=False)

試す

試しにブラウザで直接アクセスして確かめてみます。

https://host-to.proxy/proxy?url=...

まず、元画像です。自分が撮影した写真です。
解像度は4Kで、ファイルサイズは8MBもあります。

これを画像圧縮プロキシに通すと、こうなります。
解像度は640x360まで縮小され、ファイルサイズは15KBになりました。

nginxなどでキャッシュすれば、サーバー側の負担が減るかと思います。

サンプルコード

この画像圧縮プロキシサーバーのソースコードです。
Python3.8/3.9で動作確認済みです。pipでFlaskをインストールする必要があります。

from sys import stderr, stdout
from flask import Flask, request
from flask.helpers import make_response

import subprocess, io

from werkzeug.utils import send_file

app = Flask(__name__)

@app.route('/proxy')
def do_proxy():
    url = request.args.get('url')
    if not url:
        return make_response('', 400)
    try:
        data = subprocess.check_output(['ffmpeg', '-hide_banner', '-http_proxy', proxy_url, '-i', url, '-vcodec', 'mjpeg', '-qmax', '20', '-q', '20', '-vf', 'scale=360*dar:360', '-f', 'image2', '-'], stderr=subprocess.DEVNULL)
    except Exception as e:
        return make_response('', 500)
    return send_file(io.BytesIO(data), environ=request.environ, mimetype='image/jpeg', as_attachment=False)

app.run(host='127.0.0.1', port=8080, threaded=True)

さいごに

少ないコードで簡単に画像圧縮プロキシを作ることが出来ました。
クライアントがWebPに対応している場合に、サーバー側もWebPでエンコードすれば、画質を保ちながらさらにサイズ削減できるかもしれません。
Misskeyは細かいところも設定できるという強みがありますので、ぜひおひとり様インスタンスに使ってみてください。

Misskeyをはじめよう (Misskey公式サイト)
https://join.misskey.page/

Discussion