YouTubeにHDR動画を上げるときに起こる問題を解決(しきれない)

YouTubeにゲーム画面をキャプチャしてHDR動画を投稿するまでに躓きポイントがあったので
どうにか解決する。

#環境
OS:Window11 Home 21H2
CPU:Intel Core i9-9900K
GPU:NVIDIA GeForce RTX 2090 (Driver]497.29)/GeForce Experience 3.24.0.126
エンコーダー:TMPGEnc Video Mastering Works 7

#キャプチャで躓く
グラフィックカードはGeForceなので、GeForce Experienceのキャプチャー機能を使えばいいと思っていた。
しかし、インスタントリプレイは暫くすると勝手に終了しちゃうし
手動録画も一瞬で録画終了してしまう。一時ファイルや録画ファイルの場所を変えても同じだった。
色々調べた結果、公式に載っている不具合だった。
[NVIDIAI 公式フォーラム：ShadowPlay starts and instantly stops recording](
https://www.nvidia.com/en-us/geforce/forums/geforce-experience/14/331889/shadowplay-starts-and-instantly-stops-recording/?topicPage=21)
>When I start my recording it starts and instantly stops, Does anyone know how to fix this? I tried changing the location for the temp files but that didn't work.

録画を開始すると、瞬時に停止してしまいます。一時ファイルの場所を変えてみましたが、うまくいきませんでした。

これに関してNVIDIA STAFFの返信として
>There is a bug when using multi-monitors if you are mixing HDR and non-HDR. To workaround the issue, disable HDR on all monitors or enable HDR on all monitors.

HDRと非HDRを混在させている場合、マルチモニターを使用すると不具合が発生します。この問題を回避するには、すべてのモニターでHDRを無効にするか、すべてのモニターでHDRを有効にしてください。

と、確かに2台のモニターで片方だけHDR対応でWindowsの設定でもHDRを有効にしている。
片方はHDRを有効に出来ないので、解決方法はHDRを無効にすることとなる。
しかし、Windows11の自動HDRで遊びたい。

##解決策
色々弄っている間に解決方法を発見した(あらためてみると、フォーラムのスレッドにも載っていた)
「GeForce Experienceの設定＞プライバシー管理＞デスクトップキャプチャ」を無効にする。
これは以前に対応していなかったゲームを録画してた時に有効にしておいたけど
今回ターゲットにしてるゲームは、これが無効でも録画できるので無効にする。

そうするとゲーム自体はHDR非対応だがWindows11の自動HDRによりHDR化された物が録画できるようになる。

#YouTubeにアップロードしてもHDRにならない
TMPGEnc Video Mastering Works 7でのエンコード設定は
MP4(H.264/AVC) 解像度=4K(4096x2160)でHDR用にカラープロファイル＞テンプレート(HDR(BT.2100 PQ))を選択
エンコードしたファイルがVLC media playerでHDR再生できることを確認していざアップロード。

しかしHDRでの再生が出来ない。
色々調べてみたが、まずYouTubeの仕様として低画質用データから作成が始めるのだけど
動画詳細画面でSD/HD/4Kの進行状況が表示される
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1667610/fd91dc69-10b3-bcae-6f35-deed4f86a5c4.png)
で、4Kで作ったので4Kまで完了した時点で確認してみると
画質がHDRになって欲しいのにならない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1667610/f58494a7-4c69-a902-44ce-b6eb1a8c3749.png)
こんな風に4Kにはなっているが、HDR動画の場合は
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1667610/f1f710bd-0679-ab57-e05c-67b11279cb2a.png)
HDRと表示されるはず。

##解決策
結論から言えば、待てばいい。
YouTubeは低品質データから作っていく仕様だが、動作を見る限り
HDRデータをアップロードした場合は
最大解像度までのSDRデータを作った後にHDRデータを作るようになっているらしい。
4K-HDRをアップロードトした場合には、4K-SDRデータが準備できた時点で4K完了となり、
管理画面からはこれ以上の変化は確認できないが裏ではHDRの準備が進んでいて
その準備が終わり次第に動画再生画面でHDRが有効化される。

HDRが有効になったかは再生してみないと分からない、
4K完了後2・3時間たってもまだ有効にならない場合もあるのでとにかく待つしか無い。
しかも、設定をミスってそもそもHDRが不可能なデータであっても場合によっては
SDRではあるけど普通に見られる状態になってしまうのでミスがあっても分かりづらい。

まずは短い動画とかで出力設定に問題が無いかを確認した後はその設定を使い、
アップロード後は公開制限をしておいて、気長に待ち
たまに再生してみてHDRで再生できることを確認してから公開設定するのがいいと思う。
10分ちょい(3.8G位)の動画でも4・5時間待ったりしたので半日とか1日待つ気がいいかもしれない。

#さらになにかできないか
YouTubeの管理画面からはわからないけどAPIならわかるのではないか？
と思い立ったのでYouTube APIで情報取得してみよう。
APIの情報は
https://developers.google.com/youtube/v3/docs


##準備
YouTube DATA API V3を使えるようにする。
OAuth 2.0を有効ににしてクライアントIDとシークレットを取得する。

##コード
適当に作った、node.js用

```ts:youtubecheck.ts
//設定
const keyJsonPath = "./keys.json";//tokenとか保存するファイル
const callbackListenPort = 8000;//OAtuh2認証待ち受けポート
const interval = 1*60*1000;//処理中の再取得時間

import {google} from 'googleapis';
import {readFileSync,writeFile,existsSync} from 'fs';
import { Credentials, OAuth2Client } from 'google-auth-library';
import {createServer} from 'http';
const youtube = google.youtube('v3');
/*
API Refrence
https://developers.google.com/youtube/v3/docs
*/
interface keyFile {client:{id:string,secret:string},usertoken?:string,tokens?:Credentials};
let keys = null as null|keyFile;
const loadKeys = ():keyFile|null => {
  if(!existsSync(keyJsonPath)){
    console.log(`keyファイル(${keyJsonPath})が見つかりません`);
    return null;
  }
  try{
    return JSON.parse(readFileSync(keyJsonPath).toString());
  }catch(err){
    console.error(err);
    return null;
  }
}

const saveKeys = (exit=false) => {
  if(!keys){
    return false;
  }
  writeFile(keyJsonPath,JSON.stringify(keys),(err)=>{
    if(err){
      console.error("keysの保存に失敗しました");
      console.log("----keys data---");
      console.log(JSON.stringify(keys))
      console.log("----------------");
    }else{
      console.log(`${keyJsonPath} saved.`);
    }
    if(exit){
      process.exit();
    }
  });
}

const getAuth = async (oauthClient:OAuth2Client) =>{
  //認証URL取得
  const authUrl = oauthClient.generateAuthUrl({
    scope:"https://www.googleapis.com/auth/youtube",
    access_type:"offline"
  });
  console.log(`authorizeUrl:${authUrl}`);
  console.log(`Prot:${callbackListenPort} 待機中`);
  createServer((req,res) => {
    const querys = new URLSearchParams(new URL(`http://127.0.0.1${req.url}`).search);
    res.writeHead(200,{'Content-Type': 'text/plain'});
    if(querys.has("code")){
      if(keys){
        keys.usertoken = querys.get("code") as string;
      }
      console.log("認証コードを取得しました");
      res.write('OK');
      res.end();
      saveKeys(true);
    }else{
      res.write('NG');
      res.end();
    }
  }).listen(callbackListenPort);
}

const getToken = async (oauthClient:OAuth2Client):Promise<Credentials> =>{
  const tokenData = await (async ()=>{
    try{
      return JSON.parse(readFileSync(keyJsonPath).toString()) as keyFile;
    }catch(err){
      return null;
    }
  })();
  if(tokenData){
    if(tokenData.tokens){
      return tokenData.tokens;
    }
  }
  if(keys&&keys.usertoken){
    const {tokens} = await oauthClient.getToken(keys.usertoken);
    return tokens;
  }
  throw new Error('Credentials error.');
}

//引数(URL)からvideoidを取り出す
const youtubeUrl = [
  RegExp('https://youtu.be/([0-9A-Za-z]+)'),
  RegExp('https://www.youtube.com/watch\\?v=([0-9A-Za-z]+)')
]
const getVideoId = (url:string) =>{
  for(const idReg of youtubeUrl){
    const id = idReg.exec(url);
    if(id){
      return id[1];
    }
  }
  return null;
}

const getVideoDetail = async (videoId:string) => {
  const videoList = await youtube.videos.list({
    id:[videoId],
    part:["snippet","contentDetails","status","statistics","player","topicDetails","recordingDetails","fileDetails","suggestions","processingDetails"]
  });
  return videoList.data.items?videoList.data.items[0]:null;
}

const videoProccesingCheck = async (videoId:string) => {
  const videoData = await getVideoDetail(videoId);
  const videoFlags = {
    hdr:false,
    definition:"",
  }
  if(!videoData){
    console.error("データの取得に失敗しました");
    return false;
  }
  if(videoData.processingDetails?.processingStatus === "failed"){
    console.log('動画の処理に失敗しました');
    console.log(videoData.processingDetails.processingFailureReason);
    return false;
  }
  videoFlags.definition = videoData.contentDetails?.definition || "";
  if(videoData.suggestions?.processingHints){
    for(const hints of videoData.suggestions.processingHints){
      if(hints === "hdrVideo"){
        videoFlags.hdr = true;
      }
    }
  }
  console.log('snippet:');
  console.log(videoData.snippet);
  console.log('status:');
  console.log(videoData.status);
  console.log('processing:');
  console.log(videoData.processingDetails);
  console.log('contentDetails:');
  console.log(videoData.contentDetails);
  console.log('suggestions:');
  if(videoData.suggestions?.processingHints){
    console.log('suggestions Hints');
    console.log(videoData.suggestions?.processingHints);
  }
  if(videoData.suggestions?.processingErrors){
    console.log('suggestions Errors');
    console.log(videoData.suggestions?.processingErrors);
  }
  if(videoData.suggestions?.processingWarnings){
    console.log('suggestions Warnings');
    console.log(videoData.suggestions?.processingWarnings);
  }
  if(videoData.suggestions?.tagSuggestions){
    console.log('suggestions Tag');
    console.log(videoData.suggestions?.tagSuggestions);
  }
  if(videoData.processingDetails?.processingStatus === "processing"){
    console.log(`処理中です`);
    const progs = videoData.processingDetails.processingProgress;
    if(progs){
      console.log(`${progs.partsProcessed}/${progs.partsTotal} (残り：${progs.timeLeftMs} ms)`);
    }
    setTimeout(videoProccesingCheck,interval,videoId);
  }
  
  if(videoData.processingDetails?.processingStatus === "succeeded"){
    console.log('動画の処理が完了しました。');
    console.log(`解像度は${videoFlags.definition}です`);
    console.log(`HDRが${videoFlags.hdr?'有効':'無効'}です`);
    return true;
  }
}

(async () =>{
  keys = loadKeys();
  if(!keys){
    console.log('Key error');
    return false;
  }
  const oauthClient = new google.auth.OAuth2({
    "clientId":keys.client.id,
    "clientSecret":keys.client.secret,
    redirectUri:`http://127.0.0.1:${callbackListenPort.toString()}`,
  });
    oauthClient.on("tokens",(tokens)=>{
    if(keys){
      keys.tokens = tokens;
    }
    saveKeys();
  });
  if(process.argv[2] === "auth"){
    getAuth(oauthClient);
    return true;
  }
  if(!keys){
    return false;
  }
  const videoId = getVideoId(process.argv[2]);
  if(!videoId){
    console.log("VideoIdが抽出できません");
    return false;
  }
  console.log(`VideoId:${videoId}`);
  const accesstoken = await getToken(oauthClient);
  oauthClient.setCredentials(accesstoken);
  google.options({
    "auth":oauthClient
  });
  videoProccesingCheck(videoId);
})();

```

```json:keys.json
{
    "client":{
        "id":"クライアント ID",
        "secret":"クライアント シークレット"
    }
}
```

動画情報取得前に一度認証コードを取得しておく必要がある。

```
PS > node .\yutubecheck.js auth
authorizeUrl:https://accounts.google.com/o/oauth2/v2/auth?scope=*************************************************************
Port:8000 待機中
認証コードを取得しました
./keys.json saved.
PS >
```
こんな風に引数"auth"入れて実行し、表示されたurlをブラウザに入れれば出来る。


実行してみる、引数にYouTubeの視聴URLを指定する。

```
PS > node .\yutubecheck.js https://youtu.be/*******
```


##結果
processingDetails.processingStatusはSDの処理完了(つまりは最低画質で視聴可能)で"succeeded"になった。
processingDetails.processingProgressもろくな結果を返さない。
YouTube DATA APIを使っても分からない(HD以降の結果が分からないので余計酷い)結果だった。

ただリファレンスには載っていないけど、HDRが有効な場合
suggestions.processingHints[]に'hdrVideo'が入るみたいなのでHDRが有効な動画データであるかは判断できる。

やはり、待って実際に視聴ページで確認するしか無いようだ。