AMS v3でCMAFが突然追加されて困った話

# 概要

Azureメディアサービス v3ではStreamingPathsというエンティティからストリーミングURLのパスが取得できます。この戻り値の仕様が2020年6月に突然変更になり、問題が起きたのでその概要を記載しておきます。

6月に問題が起きてすぐ書こうと思っていたのですが、気付いた年末になってしまいました。

# ストリーミング URL の形式

Azureメディアサービスで配信プロトコルやDRM、あるいはフィルタを指定するときは下記のように指定します。

```
/manifest(format=xxx,encryption=yyy,filter=zzz1;zzz2)
```
|パラメータ|用途|値の例|
|:---|:---|:---|
|format|配信プロトコルを指定|m3u8-aapl, m3u8-aapl-v3, mpd-time-csf|
|encryption|DRMを指定|cenc, cbc-aapl|
|filter|フィルタを指定(AND条件)|自分で定義した名前|

これ以上は記事の本題から外れるため省きます。AMS v2についての記載ですが、下記などが参考になると思います。
> 顧客へのコンテンツ配信 > ストリーミング URL の形式
> https://docs.microsoft.com/ja-jp/azure/media-services/previous/media-services-deliver-content-overview#streaming-url-formats

v3についてはあまりいい記事が見つからず、この辺りがストリーミングURLの形式について触れられているところでしょうか・・・

> Media Services v3 のダイナミック パッケージ > ソース ファイルをデリバリー用に準備するには 
> https://docs.microsoft.com/ja-jp/azure/media-services/latest/dynamic-packaging-overview#to-prepare-your-source-files-for-delivery

# AMS v2で配信ファイルのURLを取得する

* AMS v2では主にILocatorを利用してURLを求めます
* manifest以下のformatやencryption, filterの値は分かっている前提で自分で組み立てる必要があります。


## サンプルコード

CENC(CTR)のMPEG-DASHのURLを取得する場合

* SDK: [windowsazure.mediaservices](https://www.nuget.org/packages/windowsazure.mediaservices/)

```csharp:AMSv2サンプル
using Microsoft.WindowsAzure.MediaServices.Client;

public class Sample
{
    public string GetDashUrl(IProgram program, ILocator locator)
    {
        var uriBuilder = new UriBuilder(
            $"{locator.Path}{program.ManifestName}.ism/manifest(format=mpd-time-csf,encryption=cenc)");
        uriBuilder.HostName = "{サービスで利用するホスト名}";
        uriBuilder.Scheme = "https";
        uriBuilder.Port = -1;
        return uriBuilder.Uri.AbsoluteUri;
    }
}
```

# AMS v3で配信ファイルのURLを取得する

* AMS v3ではListPathsAsync()メソッド（同期メソッドもあり）を利用して、StreamingPathクラスのリストを取得することができます。
* StreamingPathクラスはStreamingProtocol, EncryptionSchemeというプロパティを持っており配信プロトコルとDRM形式で絞り込むことができます。
* StreamingPathクラスはPathsというIList\<string\>型のプロパティが返ってきます。 

## サンプルコード

CENC(CTR)のMPEG-DASHのURLを取得する場合

* SDK: [Microsoft.Azure.Management.Media](https://www.nuget.org/packages/Microsoft.Azure.Management.Media)

```csharp:AMSv3サンプル(現行仕様では問題あり)
using System.Threading.Task;
using Microsoft.Azure.Management.Media;
using Microsoft.Azure.Management.Media.Models;

public class Sample
{
    public async Task<string> GetUrlAsync(
        AzureMediaServicesClient mediaClient, 
        string resourceGroupName, 
        string accountName,
        string locatorName)
    {
        ListPathsResponse response = await mediaClient.StreamingLocators
            .ListPathsAsync(resourceGroupName, accountName, locatorName);

        // DASHでCENC(CTR)のStreaingPathを選択
        var streamingPath = response.StreamingPaths.SingleOrDefault(x=>
            x.StreamingProtocol == StreamingPolicyStreamingProtocol.Dash 
            && x.EncryptionScheme == EncryptionScheme.CommonEncryptionCenc);

        // 1つしかない前提でSigle ※ここが問題を生みました。詳細は後述※ 
        string localPath = streamingPath.Paths.Single();
        var uri = new Uri("https://{サービスで利用するホスト名}", localPath);
        return uril.AbsolutePath;
    }
}
```

# 問題は Pathsプロパティの戻り値の仕様が突如変わったこと

PathsはIList\<string\>なので当然、複数のパスが返ってくる可能性があります。
ただ、この戻り値の仕様が東日本リージョンでいうと2020年6月25日以降、予告なく変わったため大きな問題が起きました。[^1]

[^1]: Azureでは東日本リージョンと西日本リージョンのアップデートは数日空けて行われるのが通常です。

## 従来の仕様

PathsはIList\<string\>なので当然、複数のパスが返ってくる可能性があります。従来の仕様では

* Pathsの要素数はAsset内にある.ismファイルの数と一致している
* StreamingProtocolがDash, EncryptionSchemeがCommonEncryptionCencであれば、 __末尾がmanifest(format=mpd-time-csf,encryption=cenc)のパスのみが返ってくる__ 

という状況でした。

```
├ 001.ism
├ 001.ismc
├ 001_v_500.mp4 (500kbpsの映像)
├ 001_v_1500.mp4 (1500kbpsの映像)
├ 001_v_4000.mp4 (4000kbpsの映像)
└ 001_a_128.mp4 (128kbpsの音声)
```
たとえば、アセット内のファイル構成が上記のような値だった場合、以下1件が返ってきます

1. /0f03ceba-7988-4547-ad70-64af981b3831/001.ism/manifest(format=mpd-time-csf,encryption=cenc)

```
├ 001-1.ism
├ 001-1.ismc
├ 001-2.ism
├ 001-2.ismc
├ 001_v_500.mp4 (500kbpsの映像)
├ 001_v_1500.mp4 (1500kbpsの映像)
├ 001_v_4000.mp4 (4000kbpsの映像)
└ 001_a_128.mp4 (128kbpsの音声)
```
もし、この例のように2つのismファイルを置いてあった場合は、以下の様に2件のパスが返ってきます。

1. /0f03ceba-7988-4547-ad70-64af981b3831/001-1.ism/manifest(format=mpd-time-csf,encryption=cenc)
2. /0f03ceba-7988-4547-ad70-64af981b3831/001-2.ism/manifest(format=mpd-time-csf,encryption=cenc)

ismファイルを複数置くということはあまり推奨方法ではなく、アセットフィルタを利用してほしいようですが、こういうパターンがあり得るから、PathsはIList型なのかなくらいの認識でした。

## 新仕様

```
├ 001.ism
├ 001.ismc
├ 001_v_500.mp4 (500kbpsの映像)
├ 001_v_1500.mp4 (1500kbpsの映像)
├ 001_v_4000.mp4 (4000kbpsの映像)
└ 001_a_128.mp4 (128kbpsの音声)
```
先ほどと同様にアセット内のファイル構成が上記のような値だった場合、今まで1件だったPathsが2件返ってくるようになりました。

1. /0f03ceba-7988-4547-ad70-64af981b3831/001.ism/manifest(format=mpd-time-csf,encryption=cenc)
1. /0f03ceba-7988-4547-ad70-64af981b3831/001.ism/manifest(format=**mpd-time-cmaf**,encryption=cenc)

同じことがHLSでも発生しています。
このformat=mpd-time-cmafというのはCMAF(Common Media Application Format)対応を意味しています。私もCMAFについての理解が足りないところがありますが、下記の記事など参考になるかもしれません。

> CMAF と CMAFを使用した低遅延 LIVE 配信の実現
> https://blogs.akamai.com/jp/2018/04/cmaf-cmaf-low-lalatency-live.html

## CMAF形式のURLが返ってくるようになった上、SDKのプロパティでは絞り込み出来ない

CMAFとは何かというのは置いておいて、__これはCMAF対応のHLS, DASHはその形式の再生に対応したプレイヤーでないと再生エラーを起こす場合がありますので、本来は違う配信プロトコルとして明確分けられるべきものです。__
また、前述のAMS v3のサンプルコードだと、Pathsプロパティに1つ要素がない前提でSingle()をしていますので、要素が2つになれば前述のサンプルコードはエラーとなってしまいます。

SDKやその裏のRES APIの仕様としては、StreamingPolicyStreamingProtocolにDashCmafなど新しい形式として追加するか、StreamingPathクラスに新しいプロパティを追加してCMAFかどうかを判定できるようにするのが良い設計と私は思います。事実、ストリーミングURLのformatの値は違うんですから！

これはAzureのサポートに問い合わせて要望は出しましたが、結局のところ実装の改善は行われませんでした。

せめて、このレベルの仕様変更は事前に告知されるべきと思いますが、そのアナウンスもなし。
うーん、残念仕様・・・

# 対応

SaaSなので仕様が変わらないとなるとこちらで対応するしかありません。
今の仕様が改善されないとなると、今後もPathsに未知のformatが追加される危険を考慮する必要があります。もしかしてencryptionもサイレントに追加されて複数パターンまとめて返ってきちゃうかもしれないと
しれないと疑心暗鬼にもなります。
結果として、安全を考慮すると、こんなコードになってしまいます。

```csharp:AMSv3サンプル(現行仕様への対応版)
using System.Threading.Task;
using Microsoft.Azure.Management.Media;
using Microsoft.Azure.Management.Media.Models;

public class Sample
{
    public async Task<string> GetUrlAsync(
        AzureMediaServicesClient mediaClient, 
        string resourceGroupName, 
        string accountName,
        string locatorName)
    {
        ListPathsResponse response = await mediaClient.StreamingLocators
            .ListPathsAsync(resourceGroupName, accountName, locatorName);

        // DASHでCENC(CTR)のStreaingPathを選択
        var streamingPath = response.StreamingPaths.SingleOrDefault(x=>
            x.StreamingProtocol == StreamingPolicyStreamingProtocol.Dash 
            && x.EncryptionScheme == EncryptionScheme.CommonEncryptionCenc);

        // 1つ手前でStreamingProtocolとEncryptionSchemeを絞ってるのにもう1回絞り込み・・・
        string localPath = streamingPath.Paths.Single(x=>
            x.Contains("format=mpd-time-csf") && x.Contains("encryption=cenc"));
        var uri = new Uri("https://{サービスで利用するホスト名}", localPath);
        return uril.AbsolutePath;
    }
}
```
うーん・・・改善求む。