【Unity】Unity上でYoutubeの指定動画のサムネイルをとってくる方法【Youtube】

# TL;DR
- Youtubeにはサムネ取得用URLがあるのでそこにパラメータを入れれば入手可能
- UnityはデフォルトではHTTPリクエストは禁止しているのでHTTPSに変更するかセキュリティリスクを背負ってHTTPリクエストを有効化しよう


# 概要
ひょんなことから仕事で `UnityでYoutube の動画を再生したいけど、再生リストを作れないか?` と言われたのでまずはサムネイルを取得できないと話にならんと思い調査した記録になります。

# 制作環境

| 項目 | 説明 |
|---| ---|
| Unity | 2022LTS |
| UniTask | 2.3.3 |

# 手順

1. Youtubeの対象の動画のURLを取得
1. URLから動画IDを抽出
1. サムネイルの品質を選択
1. サムネイル取得用URLの生成
1. UnityWebRequest経由でサムネイルをとりにいく
1. [Optional] UIで表示

という手順になります。

## 1. Youtubeの対象の動画のURLを取得

`https://www.youtube.com/watch?v=動画 ID` というフォーマットで取得しましょう。
こちらは手入力でもいいし、コピペでも良いです。

ただし、再生リストの情報などは不必要なのでカットしましょう。

## 2. URLから動画IDを抽出

先ほどのフォーマットであれば `=` より後ろがあれば十分なので、以下のようにすればとってこれるはずです。

```GetID.cs

public string GetId(string url)
{
    string[] urls = url.Split("=");
    if (urls.Length != 2)
    {
       return null;
    }
    return urls[urls.Length -1];
}
```

## 3. サムネイルの品質を選択

品質は `標準`, `中`, `高`, `HQ`, FullHD` の5択です。
また、それぞれに紐づくファイル名も決まっています。

| 品質 | ファイル名 |
| --- | --- |
|標準|default.jpg|
|中|mqdefault.jpg|
|高|hqdefault.jpg|
|HQ|sddefault.jpg|
|FullHD|maxresdefault.jpg|

~~HQなのにhqdefault.jpg じゃないのかよ！っていうツッコミはなしです~~

とりまとめると、以下のように定義しておくと良いでしょう。

```quality.cs

    private static readonly string[] FILENAMES = new string[]
    {
        "default.jpg",
        "mqdefault.jpg",
        "hqdefault.jpg",
        "sddefault.jpg",
        "maxresdefault.jpg",
    };
    
    public enum Quality
    {
        Default,
        Mid,
        High,
        HQ,
        FHD
    }

```
## 4. サムネイル取得用URLの生成

https://blog.piece-web.jp/php/1878.html

こちらのブログでも紹介されている通りで、 `http://img.youtube.com/vi/動画ID/画像サイズ` , `https://img.youtube.com/vi/動画ID/画像サイズ` いずれかのURLで取得することが出来ます。

## 5. UnityWebRequest経由でサムネイルをとりにいく

普通に `UnityWebRequest.GetTexture` を利用しましょう。

また、今回は最近の時流にのっかり Async/Await で書いてみるので [UniTask](https://github.com/Cysharp/UniTask) を利用しましょう。

``` GetThumbnail.cs

    public void OnSubmit()
    {
        if (cts != null)
        {
            cts.Cancel();
            cts.Dispose();
        }

        cts = new CancellationTokenSource();
        GetThumbnailAsync(m_url.text, currentQuality, cts.Token).Forget(e=> Debug.LogError(e));
        
    }

    private async UniTask GetThumbnailAsync(string url, Quality quality, CancellationToken token)
    {
        string thumbnail_url = string.Empty;
        try
        {
            string youtubeId = string.Empty;
            if (url.Contains("watch?v="))
            {
                string[] urls = url.Split("=");
                if (urls.Length != 2)
                {
                    throw new Exception("Invalid URL:" + url);
                }

                youtubeId = urls[1];
            }
            else
            {
                string[] urls = url.Split("/");
                youtubeId = urls[urls.Length - 1];
            }
            if(string.IsNullOrEmpty(youtubeId))throw new Exception("Invalid youtubeId:" + youtubeId);

            thumbnail_url = $"https://img.youtube.com/vi/{youtubeId}/{FILENAMES[(int)quality]}";

            Debug.Log("Try Download Thumbnail:"+thumbnail_url);
            
            var www = UnityWebRequestTexture.GetTexture(thumbnail_url);

            await www.SendWebRequest();

            if (www.result == UnityWebRequest.Result.Success)
            {
                Texture2D texture = DownloadHandlerTexture.GetContent(www);
                if (m_thumbnail[(int)quality].texture != null)
                {
                    var prev = m_thumbnail[(int)quality].texture;
                    m_thumbnail[(int)quality].texture = null;
                    Destroy(prev);
                }
                m_thumbnail[(int)quality].texture = texture;
            }
            else
            {
                Debug.LogError("Failed to download texture: " + www.error);
            }
        }
        catch (OperationCanceledException e)
        {
            Debug.LogWarning($"Operation was canceled. (URL:{thumbnail_url})");
        }
        catch (Exception e)
        {
            Debug.LogError(e);
        }
    }
```

### つまづいたところ
素直に技術ブログ通りに `http` リクエストを行ったところ以下のエラーがおきました。

```
InvalidOperationException: Insecure connection not allowed
```

調べたところ以下の記事が大変参考になりました。

https://zenn.dev/keisuke114/scraps/5a0792aea13bdc

ざっくり言うと、HTTPはセキュリティ的に危ないからデフォルトでNGにしてるので、PlayerSettingsから有効化するときは自前でリスクを負ってね！　というものです。

## 6. [Optional] UIで表示

今回はUI上にRawImage を用意して、それぞれのサイズで表示できるようにしました。
なお、RawImage で差し替える時は先ほどのコードにあるように、前のTextureはDestroyで破棄するようにしておかないとメモリリークを引き起こすためご注意ください。

``` NotMemoryLeak.cs

                if (m_thumbnail[(int)quality].texture != null)
                {
                    var prev = m_thumbnail[(int)quality].texture;
                    m_thumbnail[(int)quality].texture = null;
                    Destroy(prev);
                }
```

# 動作チェック
動画は以前、私が登壇した時の録画を利用します。

https://www.youtube.com/watch?v=Qea4ziAg5jk

なお、上記動画の文字起こし版の記事もあるのでぜひよろしくお願いします。

https://qiita.com/Cova8bitdot/items/8ea26245748856cfa5af

![GetThumbnail.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/5def739b-df20-acfa-8960-e68f906ab7fe.gif)


# リポジトリ
今回作ったUnityプロジェクトはこちらで公開しています。

https://github.com/Cova8bitdots/GetYoutubeThumbnail