Azure Media ServicesでVoD(Video on Demand)のストリーミングを試してみる

nazology.net

地球規模の飲料水不足を解決するには、海水の淡水化をもっと容易にしなければいけません。

水を高速で通すのに”塩は通さない”「フッ素化ナノチューブ」を開発! - ナゾロジー

しかし現時点で利用できる水処理膜(海水から塩分を除去するフィルター)には限界があります。

水を高速で通すのに”塩は通さない”「フッ素化ナノチューブ」を開発! - ナゾロジー

そこで東京大学・大学院工学系研究科に所属する伊藤 喜光(いとう よしみつ)氏ら研究チームは、水を超高速で通しつつ塩は通さない「フッ素化ナノチューブを開発しました。

水を高速で通すのに”塩は通さない”「フッ素化ナノチューブ」を開発! - ナゾロジー

⇧ 水は大事ですからね~。ありがたいことです。

waterstand.jp

水の性質は、地域による土壌の成分や土壌内での滞留時間が違うため、硬度には大きな差があります。たとえば、日本は河川の長さが短く海までの傾斜が大きいので、比較的短時間で地下水が地層を流れます。そのためミネラルをあまり含まない軟水になります。
一方、欧米は河川の長さが長く、海までの傾斜が緩やかなので、ゆっくりと時間をかけて地層を浸透して通り抜けます。そのためミネラルたっぷりの硬水になります。 

「軟水」「硬水」の違いと使い分け方 | お水の知識 | 水道直結ウォーターサーバー ウォータースタンド 

www.shinsousui.com

 国内で市販されているミネラルウォーターは、山を産地としたものが多く、そのほとんどは軟水。ミネラル分の中ではカルシウムを多く含みます。

山の水と海の水の違い - 深層水ってすごい!!【海洋深層水研究会】

これに対して、海の水の特徴は硬度が高いこと。特に今、現代人に最も不足しているといわれるマグネシウムの含有量が多いことでも注目を集めています。もちろん、マグネシウムだけでなくカルシウム量も多いので、ミネラル補給を目的にするなら、海の水由来のミネラルウォーターがおすすめです。

山の水と海の水の違い - 深層水ってすごい!!【海洋深層水研究会】

⇧ へぇ~、海水は硬水になるんですね、

daiwafoods.com

日本では、軟水がほとんどで日本料理にはあってます。出汁・出し・だしは、軟水ではでますが、硬水では出ないのです。

出汁(だし)を考える 出汁(だし)を考える 軟水・硬水 | 大和食品株式会社

⇧ 軟水が生んだ出汁文化。

例の如く冒頭から関係ない話でしたが、今回も、Azure Media Servicesについてです。

レッツトライ~。

ダウンロード配信とストリーミング配信

ストリーミング配信に必要な要素が整理できてなかったので、改めて確認。

www.image-house.co.jp

⇧ 上記サイト様の説明にあるように、「ストリーミング配信」では「メタデータ」が必要ということらしい。

Wikipediaさんの説明によると、

ストリーミング英語Streaming)とは、主に音声動画などのマルチメディアファイルを転送・再生するダウンロード方式の一種である。

通常、ファイルはダウンロード完了後に開く動作が行われるが、動画のようなサイズの大きいファイルを再生する際にはダウンロードに非常に時間がかかってしまい、特にライブ配信では大きな支障が出る。そこで、ファイルをダウンロードしながら、同時に再生をすることにより、ユーザーの待ち時間が大幅に短縮される。この方式を大まかに「ストリーミング」と称することが多い。

ストリーミング - Wikipedia

⇧ ストリーミングはダウンロード方式の一種とあり、混乱してしまうのだが...

video-b.com

⇧ 上記サイト様によりますと、動画配信の方法としては、

  • ダウンロード
  • プログレッシブ・ダウンロード
  • ストリーミング
    • リアルタイム配信
    • オンデマンド配信

ということで、ストリーミングはネットワークに繋がってることが前提ということらしい。

Azure Media Servicesでは「マニフェスト」というファイルが「ストリーミング配信」に必要な「メタデータ」が含まれるらしい

Azure Media Servicesの場合は、

docs.microsoft.com

マニフェスト

Media Services の "ダイナミック パッケージ" では、HLS、MPEG-DASH、スムーズ ストリーミングのストリーミング クライアント マニフェストが、URL 内の format クエリに基づいて動的に生成されます。

https://docs.microsoft.com/ja-jp/azure/media-services/latest/encode-dynamic-packaging-concept#manifests

マニフェスト ファイルには、トラックの種類 (オーディオ、ビデオ、またはテキスト)、トラック名、開始時刻と終了時刻、ビットレート (品質)、トラック言語、プレゼンテーション ウィンドウ (固定時間のスライディング ウィンドウ)、ビデオ コーデック (FourCC) などの、ストリーミング メタデータが含まれます。 また、次に再生可能なビデオ フラグメントとその場所の情報を通知して、次のフラグメントを取得するようにプレイヤーに指示します。 フラグメント (またはセグメント) とは、ビデオ コンテンツの実際の "チャンク" です。

https://docs.microsoft.com/ja-jp/azure/media-services/latest/encode-dynamic-packaging-concept#manifests

⇧ とあるように、「マニフェスト」ファイルに「ストリーミング メタデータ」が含まれるとあるので、「マニフェスト」ファイルが無いとストリーミング配信は実現できないということになるのではないかと。

teratail.com

 

Azure Media Services のストリーミングURLを再生するには

公式のドキュメントによると、

docs.microsoft.com

Media Services では、複数のメディア プレーヤーを使用できます。

https://docs.microsoft.com/ja-jp/azure/media-services/latest/player-media-players-concept

⇧ とあって、

  • Azure Media Player
  • Shaka
  • Video.js

の3つが紹介されてる。

Azure Media ServicesのストリーミングURLに対応してるのかが分からないのですが、

mat0401.info

zenn.dev

⇧ hls.jsってものもあるらしい。

Video.jsをTypeScriptで使えるようにするのに必要なライブラリを確認

公式のドキュメントを見る限り、

videojs.com

www.npmjs.com

⇧ TypeScriptで使えるのかが分からない...

何か、TypeScriptで使う場合は、

www.npmjs.com

⇧ 追加で必要らしい。Video.jsのドキュメントでこのあたりの情報が見当たらないところを見ると、TypeScriptの市民権が無いに等しいんだが...

というわけで、インストール可能なバージョンを確認してみる。

う~ん、例の如く、「video.js」と「@types/video.js」のバージョンの対応が分からんですな...

仕方ない、最新をインストールしてみます。

Vue.jsとTypeScriptでVideo.jsを使ってみようとして泥沼にハマる...

で、公式のVideo.jsのドキュメントで、

videojs.com

⇧ Vueの場合の実装サンプルが掲載されていて、試したけど、動いてくれないという...というか無茶苦茶泥沼にハマった...

まぁ、今回、動画アップロード、エンコード、ストリーミングを同時に試そうとした結果、フロントエンドの実装が宜しくない実装になっているというのが原因なんだが...

で、いろいろ調べた結果、

github.com

updated hook means the component did a re-render. If the prop is never used during render, why would you expect the component to re-render when it changes?

Put it another way: you are misunderstanding what updated should be used for. 

https://github.com/vuejs/vue/issues/5325#issuecomment-290297915

⇧ Vue.jsの生みの親と言われる、Evan Youさんが質問に答えていて、どうも、propの変数の値がリアクティブに変わったところで、Vue.jsのライフサイクルによる再レンダリングはされないってことなんかな?

stackoverflow.com

It's because tricky behavior in Vue Parent and Child lifecycle hooks.

Usually parent component fire created() hook and then mount() hook, but when there are child components it's not exactly that way: Parent fires created() and then his childs fire created(), then mount() and only after child's mount() hooks are loaded, parent loads his mount() as explained here. And that's why the prop in child component isn't loaded.

Use mounted() hook instead created()

like that https://jsfiddle.net/stanimirsp5/xnwcvL59/1/

https://stackoverflow.com/questions/39697334/vuejs-child-component-props-not-updating-instantly

⇧う~ん、よく分からんけども、propとした変数の値は動的に連動してくれるんだけど、propの変数の中身が変わったからといって、created()、mounted()、updated()、などのようなVue.jsのライフサイクルのメソッドが発火するわけではないということなんかな?

Azure Media ServicesでVoD(Video on Demand)のストリーミングを試してみる

というわけで、試行錯誤の果てにストリーミング再生の確認ができたので、ソースコードを掲載。

基本的には、

ts0818.hatenablog.com

⇧ 上記の時のコードに修正を加えていっているので、変更、追加した部分以外は、上記をご参考ください。

◇フロントエンド

■vue-router-work\my-project-vue\src\views\video\VideoPlayer.vue

<template>
  <div class="video-content">
    <div class="container row">
      <div class="column">
        <video
          ref="videoPlayer"
          class="video-js vjs-default-skin vjs-big-play-centered vjs-16-9"
          muted
          playsinline
        ></video>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from "video.js";
/* eslint-disable no-console */
import { Component, Prop, Vue, Watch } from "vue-property-decorator";

// type PlayerPropsType = {
//   id: string;
//   mediaType?: string;
//   src: string;
//   title: string;
//   createdAt: string;
// };

@Component
export default class VideoPlayerView extends Vue {
  private name = "videojsPlayer";
  @Prop({
    type: Object,
    default: () => {
      /**/
    },
  })
  private options?: VideoJsPlayerOptions;

  private videoJsPlayerInstance = {} as VideoJsPlayer;

  mounted() {
    console.log("■child component mounted()");
  }

  beforeDestroy() {
    if (this.videoJsPlayerInstance) {
      this.videoJsPlayerInstance.dispose();
    }
  }

  private createVideoJsInstance() {
    this.videoJsPlayerInstance = videojs(
      this.$refs.videoPlayer as any,
      this.options as any,
      () => {
        console.log("onPlayerReady");
      }
    );
  }

  @Watch("videoJSPlayerInstance")
  private infoVideoJSPlayerInstance() {
    console.log(this.videoJsPlayerInstance);
  }

  @Watch("options")
  private infoPropOptions() {
    console.log(this.options);
    this.createVideoJsInstance();
  }
}
</script>

<style lang="scss" scoped>
.video-content {
  max-width: 640px;
  margin: 0 auto;
}
::v-deep {
  .vjs-big-play-button {
    display: block;
    background: orange;
  }
}
</style>
    

■vue_work\vue-router-work\my-project-vue\src\views\AboutView.vue

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <video-player :options="videoOptions" />
    <label
      >画像を選択
      <input type="file" @change="onFileChange" />
    </label>
    <button @click="removeFile">削除</button>
    <button @click="uploadFile">アップロード</button>
    <div>
      <video v-if="isMovie" :src="selectedFile" controls />
      <img v-else :src="selectedFile" />
    </div>

    <!--<img :src="require('@/assets/images/SVGアイコン.svg')" />-->
  </div>
</template>

<script lang="ts">
/* eslint-disable no-console */
import { Component, Vue, Watch } from "vue-property-decorator";
import UserService from "../api/userService";
import UserDto from "../dto/user/UserDto";
import VideoPlayer from "@/views/video/VideoPlayer.vue";

@Component({
  components: {
    VideoPlayer,
  },
})
export default class AboutView extends Vue {
  private temp = "";
  private selectedFile = "";
  private tempFile = {};
  private userDto?: any;
  private isMovie = false;

  private mimeType = "";
  private videoOptions = {};

  created() {
    //this.test();
  }

  private onFileChange(event: any) {
    const files = event.target.files || event.dataTransfer.files;
    this.tempFile = files[0];
    this.createDisplayImage(files[0], event);
  }

  private createDisplayImage(file: any, event: any) {
    const fileReader = new FileReader();
    fileReader.onload = (event) => {
      console.log("createDisplayImage");
      console.dir(event);
      if (event && event.target) {
        this.selectedFile = event.target.result as string;
        this.isMovie = this.isMimeTypeOfMovie(file.type);
      }
    };
    fileReader.readAsDataURL(file);
  }

  private removeFile() {
    this.selectedFile = "";
  }

  private uploadFile() {
    console.log("■created()");
    //console.dir(this.selectedFile);
    //console.dir(this.tempFile);
    let data = "テスト";
    this.userDto = Object.entries({
      id: data,
      file: this.tempFile as Blob,
    });
    let formData = new FormData();
    formData.append("id", data);
    formData.append("file", this.tempFile as File);

    //formData.append("userFormDto", JSON.stringify(userDto as any));
    formData.append("userFormDto", this.userDto);
    var postdata = new URLSearchParams();
    postdata.append("userFormDto", this.userDto);
    //postdata.append("id", data);
    //postdata.append("file", JSON.stringify(this.tempFile as File));

    // https://tech.chakapoko.com/nodejs/http/axios.html
    console.log("■送信前");
    UserService.save(formData)
      //UserService.save(postdata)
      .then((result) => {
        console.log("■レスポンス受信");
        console.log("■バックエンドからの返却の値");
        console.dir(result);
        let data = result.data.split("data:");
        let url = "";
        for (let temp of data) {
          if (temp.match("https")) {
            url = temp.replace(/\r?\n/g, "");
          }
        }
        console.log(url);
        this.videoOptions = Object.assign({}, this.videoOptions, {
          sourceOrder: true,
          autoplay: true,
          controls: true,
          liveui: true,
          muted: true,
          preload: "auto",
          html5: {
            vhs: {
              allowSeeksWithinUnsafeLiveWindow: true,
              enableLowInitialPlaylist: true,
              overrideNative: false,
              smoothQualityChange: true,
              withCredentials: false,
            },
          },
          sources: [
            {
              src: url,
              type: "application/x-mpegURL",
            },
          ],
        });
        //this.$forceUpdate();
      })
      .catch((error) => {
        console.log(error);
      });
    console.log("■処理中...");
    console.dir(UserService.chunkedArr);
    //const numArr = [...Array(100000).keys()].map((i: number) => ++i);
    const numArr = Array.from(Array(1000000).keys()).map((x: number) => x + 1);
    let response = "[";
    for (let val of numArr) {
      let tmp = val === numArr.length ? "" : ",";
      response = response.concat(val.toString(), tmp);
    }
    response += "]";
    console.log("■フロントエンド");
    console.log(response);
  }

  private test() {
    console.log("■created()");
    let data = "テスト";
    let formData = new FormData();
    formData.append("id", data);
    // https://tech.chakapoko.com/nodejs/http/axios.html
    console.log("■送信前");
    UserService.save(formData)
      .then((result) => {
        console.log("■レスポンス受信");
        console.log("■バックエンドからの返却の値");
        console.dir(result);
      })
      .catch((error) => {
        console.log(error);
      });
    console.log("■処理中...");
    console.dir(UserService.chunkedArr);
    //const numArr = [...Array(100000).keys()].map((i: number) => ++i);
    const numArr = Array.from(Array(1000000).keys()).map((x: number) => x + 1);
    let response = "[";
    for (let val of numArr) {
      let tmp = val === numArr.length ? "" : ",";
      response = response.concat(val.toString(), tmp);
    }
    response += "]";
    console.log("■フロントエンド");
    console.log(response);
  }

  @Watch("temp")
  private setInfo() {
    console.dir(this.temp);
  }

  private isMimeTypeOfMovie(mimeType: string): boolean {
    if (mimeType) {
      this.mimeType = mimeType;
      switch (mimeType) {
        case "video/mp4":
        case "video/mov":
          return true;
        default:
          return false;
      }
    }
    return false;
  }

  @Watch("videoOptions")
  private infoVideoOptions() {
    console.log(this.videoOptions);
  }
}
</script>

⇧ サーバーサイドでJavaSpring Frameworkのorg.springframework.web.servlet.mvc.method.annotation.SseEmitterを使っていてる影響で、フロントエンドのaxiosのリクエストの戻り値が特殊な感じになってるので、axiosのリクエストの戻り値からAzure Media Services のAPIで作成されたストリーミングURLを取得するのに、ちょっと面倒くさいことしてます。

■vue_work\vue-router-work\my-project-vue\src\main.ts

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import Element from "element-ui";
// ElementUIでの言語設定、datePickerとかで適用される
import locale from "element-ui/lib/locale/lang/ja";
import "element-ui/lib/theme-chalk/index.css";

import "video.js/dist/video-js.css";

Vue.config.productionTip = false;
Vue.use(Element, { locale });

new Vue({
  router,
  render: (h) => h(App),
}).$mount("#app");

続いて、サーバーサイド側。

◇サーバーサイド

■test-uml/src/main/java/com/example/demo/service/azure/media/EncodingWithMESPredefinedPreset.java

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.example.demo.service.azure.media;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import javax.naming.AuthenticationException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.azure.core.http.policy.HttpLogDetailLevel;
import com.azure.core.http.policy.HttpLogOptions;
import com.azure.core.management.exception.ManagementException;
import com.azure.core.management.profile.AzureProfile;
import com.azure.identity.DefaultAzureCredential;
import com.azure.identity.DefaultAzureCredentialBuilder;
import com.azure.resourcemanager.mediaservices.MediaServicesManager;
import com.azure.resourcemanager.mediaservices.models.Asset;
import com.azure.resourcemanager.mediaservices.models.AssetContainerPermission;
import com.azure.resourcemanager.mediaservices.models.AssetContainerSas;
import com.azure.resourcemanager.mediaservices.models.BuiltInStandardEncoderPreset;
import com.azure.resourcemanager.mediaservices.models.EncoderNamedPreset;
import com.azure.resourcemanager.mediaservices.models.Job;
import com.azure.resourcemanager.mediaservices.models.JobInput;
import com.azure.resourcemanager.mediaservices.models.JobInputAsset;
import com.azure.resourcemanager.mediaservices.models.JobOutput;
import com.azure.resourcemanager.mediaservices.models.JobOutputAsset;
import com.azure.resourcemanager.mediaservices.models.JobState;
import com.azure.resourcemanager.mediaservices.models.ListContainerSasInput;
import com.azure.resourcemanager.mediaservices.models.ListPathsResponse;
import com.azure.resourcemanager.mediaservices.models.StreamingEndpoint;
import com.azure.resourcemanager.mediaservices.models.StreamingEndpointResourceState;
import com.azure.resourcemanager.mediaservices.models.StreamingLocator;
import com.azure.resourcemanager.mediaservices.models.StreamingPath;
import com.azure.resourcemanager.mediaservices.models.Transform;
import com.azure.resourcemanager.mediaservices.models.TransformOutput;
import com.azure.security.keyvault.secrets.SecretClient;
import com.azure.security.keyvault.secrets.SecretClientBuilder;
import com.azure.storage.blob.BlobClient;
import com.azure.storage.blob.BlobContainerClient;
import com.azure.storage.blob.BlobContainerClientBuilder;

@Service
public class EncodingWithMESPredefinedPreset {
	
	@Autowired
	private ConfigWrapper configWrapper;
	
    private static final String TRANSFORM_NAME = "AdaptiveBitrate";
    private static final String OUTPUT_FOLDER = "Output";
    //private static final String BASE_URI = "https://nimbuscdn-nimbuspm.streaming.mediaservices.windows.net/2b533311-b215-4409-80af-529c3e853622/";
    //private static final String INPUT_LABEL = "input1";

    // Please change this to your endpoint name
    private static final String STREAMING_ENDPOINT_NAME = "default";

    @Value("${azure.key.vault.url}")
    private String azureKeyVaultUrl;
    
    @Value("${azure.media.managed.id}")
    private String azureMediaManagedId;
    
    private MediaServicesManager createMediaServiceManagerByKeyVault () {
        DefaultAzureCredential defaultCredential = new DefaultAzureCredentialBuilder()
                .managedIdentityClientId(azureMediaManagedId)
                .build();

            // Azure SDK client builders accept the credential as a parameter
            SecretClient client = new SecretClientBuilder()
                .vaultUrl(azureKeyVaultUrl)
                .credential(defaultCredential)
                .buildClient();
            
            AzureProfile profile = new AzureProfile(configWrapper.getAadTenantId(), configWrapper.getAzureSubscriptionId(),
                    com.azure.core.management.AzureEnvironment.AZURE);

            MediaServicesManager manager = MediaServicesManager.configure()
                    .withLogOptions(new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS))
                    .authenticate(defaultCredential, profile);
            return manager; 
    }
    
//    private MediaServicesManager createMediaServicesManager () {
//        // Connect to media services, please see https://docs.microsoft.com/en-us/azure/media-services/latest/configure-connect-java-howto
//        // for details.
//        TokenCredential credential = new ClientSecretCredentialBuilder()
//                .clientId(configWrapper.getAzureClientId())
//                .clientSecret(configWrapper.getAzureClientSecret())
//                .tenantId(configWrapper.getAadTenantId())
//                .build();
//        AzureProfile profile = new AzureProfile(configWrapper.getAadTenantId(), configWrapper.getAzureSubscriptionId(),
//                com.azure.core.management.AzureEnvironment.AZURE);
//
//        // MediaServiceManager is the entry point to Azure Media resource management.
//        MediaServicesManager manager = MediaServicesManager.configure()
//                .withLogOptions(new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS))
//                .authenticate(credential, profile);
//        return manager;
//    }

    /**
     * Run the sample.
     *
     * @param config This param is of type ConfigWrapper. This class reads values from local configuration file.
     */
    public String runEncodingWithMESPredefinedPreset(MultipartFile uploadFile) {
        // MediaServiceManager is the entry point to Azure Media resource management.
        //MediaServicesManager manager = createMediaServicesManager();
        MediaServicesManager manager = createMediaServiceManagerByKeyVault();
        // Creating a unique suffix so that we don't have name collisions if you run the
        // sample
        UUID uuid = UUID.randomUUID();
        String uniqueness = uuid.toString();
        String jobName = "job-" + uniqueness.substring(0, 13);
        String locatorName = "locator-" + uniqueness;
        String outputAssetName = "output-" + uniqueness;
        String inputAssetName = "input-" + uniqueness;
        boolean stopEndpoint = false;

        String streamingUrl = null;
        try {
            List<TransformOutput> outputs = new ArrayList<>();
            outputs.add(new TransformOutput().withPreset(
                    new BuiltInStandardEncoderPreset().withPresetName(EncoderNamedPreset.CONTENT_AWARE_ENCODING)));

            // Create the transform.
            System.out.println("Creating a transform...");
            Transform transform = manager.transforms()
                    .define(TRANSFORM_NAME)
                    .withExistingMediaService(configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName())
                    .withOutputs(outputs)
                    .create();
            System.out.println("Transform created");

            // Create a JobInputHttp. The input to the Job is a HTTPS URL pointing to an MP4 file.
//            List<String> files = new ArrayList<>();
//            files.add(uploadFile.getOriginalFilename());
//            JobInputHttp input = new JobInputHttp().withBaseUri(BASE_URI);
//            input.withFiles(files);
//            input.withLabel(INPUT_LABEL);

            Asset inputAsset = createInputAsset(manager, configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), inputAssetName,
            		uploadFile);
            
            // Output from the encoding Job must be written to an Asset, so let's create one. Note that we
            // are using a unique asset name, there should not be a name collision.
            System.out.println("Creating an output asset...");
            Asset outputAsset = manager.assets()
                    .define(outputAssetName)
                    .withExistingMediaService(configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName())
                    .create();

            Job job = submitJob(manager, configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), transform.name(), jobName,
            		inputAsset.name(), outputAsset.name());

            long startedTime = System.currentTimeMillis();

            // In this demo code, we will poll for Job status. Polling is not a recommended best practice for production
            // applications because of the latency it introduces. Overuse of this API may trigger throttling. Developers
            // should instead use Event Grid. To see how to implement the event grid, see the sample
            // https://github.com/Azure-Samples/media-services-v3-java/tree/master/ContentProtection/BasicAESClearKey.
            job = waitForJobToFinish(manager, configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), transform.name(),
                    jobName);

            long elapsed = (System.currentTimeMillis() - startedTime) / 1000; // Elapsed time in seconds
            System.out.println("Job elapsed time: " + elapsed + " second(s).");            


            if (job.state() == JobState.FINISHED) {
                System.out.println("Job finished.");
                System.out.println();

                // Now that the content has been encoded, publish it for Streaming by creating
                // a StreamingLocator. 
                StreamingLocator locator = getStreamingLocator(manager, configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(),
                        outputAsset.name(), locatorName);

                StreamingEndpoint streamingEndpoint = manager.streamingEndpoints()
                        .get(configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), STREAMING_ENDPOINT_NAME);

                if (streamingEndpoint != null) {
                    // Start The Streaming Endpoint if it is not running.
                    if (streamingEndpoint.resourceState() != StreamingEndpointResourceState.RUNNING) {
                        System.out.println("Streaming endpoint was stopped, restarting it...");
                        manager.streamingEndpoints().start(configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), STREAMING_ENDPOINT_NAME);

                        // We started the endpoint, we should stop it in cleanup.
                        stopEndpoint = true;
                    }

                    System.out.println();
                    System.out.println("Streaming urls:");
                    List<String> urls = getStreamingUrls(manager, configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), locator.name(), streamingEndpoint);

                    for (String url : urls) {
                        System.out.println("\t" + url);                        
                    }
                    streamingUrl = urls.get(0);

                    System.out.println();
                    System.out.println("To stream, copy and paste the Streaming URL into the Azure Media Player at 'http://aka.ms/azuremediaplayer'.");
                    System.out.println("When finished, press ENTER to continue.");
                    System.out.println();
                    System.out.flush();
                    

                    // Download output asset for verification.
                    System.out.println("Downloading output asset...");
                    System.out.println();
                    File outputFolder = new File(OUTPUT_FOLDER);
                    if (outputFolder.exists() && !outputFolder.isDirectory()) {
                        outputFolder = new File(OUTPUT_FOLDER + uniqueness);
                    }
                    if (!outputFolder.exists()) {
                        outputFolder.mkdir();
                    }

                    downloadResults(manager, configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), outputAsset.name(),
                            outputFolder);
                    
                    uploadResults(manager, configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), outputAsset.name(),
                            outputFolder);

                    System.out.println("Done downloading. Please check the files at " + outputFolder.getAbsolutePath());
                } else {
                    System.out.println("Could not find streaming endpoint: " + STREAMING_ENDPOINT_NAME);
                }

                System.out.println("When finished, press ENTER to cleanup.");
                System.out.println();
                System.out.flush();
               
            } else if (job.state() == JobState.ERROR) {
                System.out.println("ERROR: Job finished with error message: " + job.outputs().get(0).error().message());
                System.out.println("ERROR:                   error details: "
                        + job.outputs().get(0).error().details().get(0).message());
            }
        } catch (Exception e) {
            Throwable cause = e;
            while (cause != null) {
                if (cause instanceof AuthenticationException) {
                    System.out.println("ERROR: Authentication error, please check your account settings in appsettings.json.");
                    break;
                } else if (cause instanceof ManagementException) {
                    ManagementException apiException = (ManagementException) cause;
                    System.out.println("ERROR: " + apiException.getValue().getMessage());
                    break;
                }
                cause = cause.getCause();
            }
            System.out.println();
            e.printStackTrace();
            System.out.println();
        } finally {
            System.out.println("Cleaning up...");
            cleanup(manager, configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), TRANSFORM_NAME, jobName,
                    outputAssetName, locatorName, stopEndpoint, STREAMING_ENDPOINT_NAME);
            System.out.println("Done.");
        }
        return streamingUrl;
    }

    /**
     * Create and submit a job.
     *
     * @param manager         The entry point of Azure Media resource management.
     * @param resourceGroup   The name of the resource group within the Azure subscription.
     * @param accountName     The Media Services account name.
     * @param transformName   The name of the transform.
     * @param jobName         The name of the job.
     * @param jobInput        The input to the job.
     * @param outputAssetName The name of the asset that the job writes to.
     * @return The job created.
     */
    private static Job submitJob(MediaServicesManager manager, String resourceGroup, String accountName, String transformName,
                                 String jobName, String inputAssetName,  String outputAssetName) {
        System.out.println("Creating a job...");
        // Use the name of the created input asset to create the job input.
        JobInput jobInput = new JobInputAsset().withAssetName(inputAssetName);

        // First specify where the output(s) of the Job need to be written to
        List<JobOutput> jobOutputs = new ArrayList<>();
        jobOutputs.add(new JobOutputAsset().withAssetName(outputAssetName));

        Job job = manager.jobs().define(jobName)
                .withExistingTransform(resourceGroup, accountName, transformName)
                .withInput(jobInput)
                .withOutputs(jobOutputs)
                .create();

        return job;
    }

    /**
     * Polls Media Services for the status of the Job.
     *
     * @param manager       This is the entry point of Azure Media resource
     *                      management
     * @param resourceGroup The name of the resource group within the Azure
     *                      subscription
     * @param accountName   The Media Services account name
     * @param transformName The name of the transform
     * @param jobName       The name of the job submitted
     * @return The job
     */
    private static Job waitForJobToFinish(MediaServicesManager manager, String resourceGroup, String accountName,
                                          String transformName, String jobName) {
        final int SLEEP_INTERVAL = 10 * 1000;

        Job job = null;
        boolean exit = false;

        do {
            job = manager.jobs().get(resourceGroup, accountName, transformName, jobName);

            if (job.state() == JobState.FINISHED || job.state() == JobState.ERROR || job.state() == JobState.CANCELED) {
                exit = true;
            } else {
                System.out.println("Job is " + job.state());

                int i = 0;
                for (JobOutput output : job.outputs()) {
                    System.out.print("\tJobOutput[" + i++ + "] is " + output.state() + ".");
                    if (output.state() == JobState.PROCESSING) {
                        System.out.print("  Progress: " + output.progress());
                    }
                    System.out.println();
                }

                try {
                    Thread.sleep(SLEEP_INTERVAL);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } while (!exit);

        return job;
    }

    /**
     * Use Media Service and Storage APIs to download the output files to a local folder
     *
     * @param manager       The entry point of Azure Media resource management
     * @param resourceGroup The name of the resource group within the Azure subscription
     * @param accountName   The Media Services account name
     * @param assetName     The asset name
     * @param outputFolder  The output folder for downloaded files.
     * @throws Exception
     * @throws URISyntaxException
     * @throws IOException
     */
    private static void downloadResults(MediaServicesManager manager, String resourceGroup, String accountName,
                                        String assetName, File outputFolder) throws URISyntaxException, IOException {
        ListContainerSasInput parameters = new ListContainerSasInput()
                .withPermissions(AssetContainerPermission.READ)
                .withExpiryTime(OffsetDateTime.now().plusHours(1));
        AssetContainerSas assetContainerSas = manager.assets()
                .listContainerSas(resourceGroup, accountName, assetName, parameters);

        BlobContainerClient container =
                new BlobContainerClientBuilder()
                        .endpoint(assetContainerSas.assetContainerSasUrls().get(0))
                        .buildClient();

        File directory = new File(outputFolder, assetName);
        directory.mkdir();

        container.listBlobs().forEach(blobItem -> {
            BlobClient blob = container.getBlobClient(blobItem.getName());
            File downloadTo = new File(directory, blobItem.getName());
            blob.downloadToFile(downloadTo.getAbsolutePath());
        });

        System.out.println("Download complete.");
    }

    /**
     * Creates a StreamingLocator for the specified asset and with the specified streaming policy name.
     * Once the StreamingLocator is created the output asset is available to clients for playback.
     *
     * @param manager       The entry point of Azure Media resource management
     * @param resourceGroup The name of the resource group within the Azure subscription
     * @param accountName   The Media Services account name
     * @param assetName     The name of the output asset
     * @param locatorName   The StreamingLocator name (unique in this case)
     * @return The locator created
     */
    private static StreamingLocator getStreamingLocator(MediaServicesManager manager, String resourceGroup, String accountName,
                                                        String assetName, String locatorName) {
        // Note that we are using one of the PredefinedStreamingPolicies which tell the Origin component
        // of Azure Media Services how to publish the content for streaming.
        System.out.println("Creating a streaming locator...");
        StreamingLocator locator = manager
                .streamingLocators().define(locatorName)
                .withExistingMediaService(resourceGroup, accountName)
                .withAssetName(assetName)
                .withStreamingPolicyName("Predefined_ClearStreamingOnly")
                .create();

        return locator;
    }

    /**
     * Checks if the streaming endpoint is in the running state, if not, starts it.
     *
     * @param manager           The entry point of Azure Media resource management
     * @param resourceGroup     The name of the resource group within the Azure subscription
     * @param accountName       The Media Services account name
     * @param locatorName       The name of the StreamingLocator that was created
     * @param streamingEndpoint The streaming endpoint.
     * @return List of streaming urls
     */
    private static List<String> getStreamingUrls(MediaServicesManager manager, String resourceGroup, String accountName,
                                                 String locatorName, StreamingEndpoint streamingEndpoint) {
        List<String> streamingUrls = new ArrayList<>();

        ListPathsResponse paths = manager.streamingLocators().listPaths(resourceGroup, accountName, locatorName);

        for (StreamingPath path : paths.streamingPaths()) {
            StringBuilder uriBuilder = new StringBuilder();
            uriBuilder.append("https://")
                    .append(streamingEndpoint.hostname())
                    //.append("/")
                    .append(path.paths().get(0));

            streamingUrls.add(uriBuilder.toString());
        }
        return streamingUrls;
    }

    /**
     * Cleanup
     *
     * @param manager               The entry point of Azure Media resource management.
     * @param resourceGroupName     The name of the resource group within the Azure subscription.
     * @param accountName           The Media Services account name.
     * @param transformName         The transform name.
     * @param jobName               The job name.
     * @param assetName             The asset name.
     * @param streamingLocatorName  The streaming locator name.
     * @param stopEndpoint          Stop endpoint if true, otherwise keep endpoint running.
     * @param streamingEndpointName The endpoint name.
     */
    private static void cleanup(MediaServicesManager manager, String resourceGroupName, String accountName, String transformName, String jobName,
                                String assetName, String streamingLocatorName, boolean stopEndpoint, String streamingEndpointName) {
        if (manager == null) {
            return;
        }

        manager.jobs().delete(resourceGroupName, accountName, transformName, jobName);
        //manager.assets().delete(resourceGroupName, accountName, assetName);

        //manager.streamingLocators().delete(resourceGroupName, accountName, streamingLocatorName);

        if (stopEndpoint) {
            // Because we started the endpoint, we'll stop it.
            manager.streamingEndpoints().stop(resourceGroupName, accountName, streamingEndpointName);
        } else {
            // We will keep the endpoint running because it was not started by this sample. Please note, There are costs to keep it running.
            // Please refer https://azure.microsoft.com/en-us/pricing/details/media-services/ for pricing.
            System.out.println("The endpoint '" + streamingEndpointName + "' is running. To halt further billing on the endpoint, please stop it in azure portal or AMS Explorer.");
        }
    }
    /**
     * Creates a new input Asset and uploads the specified local video file into it.
     *
     * @param manager           This is the entry point of Azure Media resource
     *                          management.
     * @param resourceGroupName The name of the resource group within the Azure
     *                          subscription.
     * @param accountName       The Media Services account name.
     * @param assetName         The name of the asset where the media file to
     *                          uploaded to.
     * @param mediaFile         The path of a media file to be uploaded into the
     *                          asset.
     * @return The asset.
     */
    private static Asset createInputAsset(MediaServicesManager manager, String resourceGroupName, String accountName,
            String assetName, MultipartFile mediaFile) throws Exception {

        System.out.println("Creating an input asset...");
        // Call Media Services API to create an Asset.
        // This method creates a container in storage for the Asset.
        // The files (blobs) associated with the asset will be stored in this container.
        Asset asset = manager.assets().define(assetName).withExistingMediaService(resourceGroupName, accountName)
                .create();

        // Use Media Services API to get back a response that contains
        // SAS URL for the Asset container into which to upload blobs.
        // That is where you would specify read-write permissions
        // and the expiration time for the SAS URL.
        ListContainerSasInput parameters = new ListContainerSasInput()
                .withPermissions(AssetContainerPermission.READ_WRITE).withExpiryTime(OffsetDateTime.now().plusHours(4));
        AssetContainerSas response = manager.assets()
                .listContainerSas(resourceGroupName, accountName, assetName, parameters);

        // Use Storage API to get a reference to the Asset container
        // that was created by calling Asset's create method.
        BlobContainerClient container = new BlobContainerClientBuilder()
                .endpoint(response.assetContainerSasUrls().get(0))
                .buildClient();

        // Uploading from a local file:
       // URI fileToUpload = StreamHLSAndDASH.class.getClassLoader().getResource(mediaFile).toURI(); // The file is a
        // resource in
        // CLASSPATH.
        //File file = new File(fileToUpload);
        BlobClient blob = container.getBlobClient(mediaFile.getOriginalFilename());

        // Use Storage API to upload the file into the container in storage.
        System.out.println("Uploading a media file to the asset...");
        blob.upload(mediaFile.getInputStream(), mediaFile.getBytes().length);
        //blob.uploadFromFile(mediaFile.getOriginalFilename());

        return asset;
    }
    
	private static void uploadResults(MediaServicesManager manager, String resourceGroup, String accountName,
			String assetName, File outputFolder) throws URISyntaxException, IOException {
		ListContainerSasInput parameters = new ListContainerSasInput().withPermissions(AssetContainerPermission.READ)
				.withExpiryTime(OffsetDateTime.now().plusHours(1));
		AssetContainerSas assetContainerSas = manager.assets().listContainerSas(resourceGroup, accountName, assetName,
				parameters);

		BlobContainerClient container = new BlobContainerClientBuilder()
				.endpoint(assetContainerSas.assetContainerSasUrls().get(0))
				.buildClient();

		container.listBlobs().forEach(blobItem -> {
			BlobClient blob = container.getBlobClient(blobItem.getName());
			//blob.copyFromUrl(blob.getBlobUrl());
			//blob.uploadFromFile(blob.getBlobUrl());
		});

		System.out.println("Upload complete.");
	}
}

■test-uml/src/main/java/com/example/demo/controller/UserController.java

package com.example.demo.controller;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import com.example.demo.dto.user.UserFormDto;
import com.example.demo.service.azure.media.EncodingWithMESPredefinedPreset;

import lombok.extern.log4j.Log4j2;

@Log4j2
@RestController
@RequestMapping("/user")
public class UserController {
	
	@Autowired
	private EncodingWithMESPredefinedPreset encodingWithMESPredefinedPreset;
	
	@PostMapping("/save")
	public SseEmitter save(@ModelAttribute  @Validated UserFormDto userFormDto) throws IOException {
		log.info("■■■INPUT チェック");
		log.info(userFormDto);
		log.info(userFormDto.getId());
		log.info(userFormDto.getFile());
		
		long timeOutForSseEmitter = 2 * 60 * 60 * 1000L;
		SseEmitter sseEmitter = new SseEmitter(timeOutForSseEmitter);
//		UserAsyncHelper userAsyncHelper = UserAsyncHelper.builder()
//		.sseEmitter(sseEmitter)
//		.userList(null)
//		.build();
//		Thread th = new Thread(userAsyncHelper);
		log.info("■■■処理開始前");
		sseEmitter.send("処理開始");
//		th.start();
		//userAsyncHelper.run();
		log.info("■■■処理開始後");
		String streamingUrl = encodingWithMESPredefinedPreset.runEncodingWithMESPredefinedPreset(userFormDto.getFile());
		sseEmitter.send(streamingUrl);
		sseEmitter.complete();
		return sseEmitter;
	}
}

で、Azure PortalでAzure Media Services のストリーミングエンドポイントを起動しておきます。

そしたら、フロントエンドとサーバーサイドのサーバーを起動してアプリケーションを稼働させます。

ブラウザで、Vue.jsのアプリケーションにアクセスし、Aboutのページに移動し、

「ファイルを選択」で動画ファイルをアップロードします。

「アップロード」ボタンを押下。

アップロード処理が完了し、ストリーミング再生が始まればOKです。

ストリーミング再生中にボタンを非表示にするのとかできてないですが、Azure Media Services のストリーミングURLを再生できることが確認できました。

課金されると怖いので、Azure Media Services のストリーミングエンドポイントは停止しておきます。

毎度、モヤモヤ感が半端ない...

今回はこのへんで。