Usar Bitmovin encoding con Microsoft Azure

Si bien es cierto que la documentación de Bitmovin menciona poco de esta posibilidad, los servicios que ofrecen esta compañía también están preparados a día de hoy para integrarse con Microsoft Azure. En este artículo te cuento cómo usar el servicio de encoding de Bitmovin con este cloud.

Prerrequisitos

Para que esta prueba funcione correctamente hay preparar algunas cosas del lado de Azure. En primer lugar necesitas una cuenta de almacenamiento, que puedes crear con estos comandos si todavía no la tienes (necesitas tener Azure CLI instalado):

#Variables
RESOURCE_GROUP="Bitmovin-West-Europe"
LOCATION="westeurope"
STORAGE_NAME="bitmovinassets"
#Create resource group
az group create -n $RESOURCE_GROUP -l $LOCATION
#Create storage account
az storage account create --name $STORAGE_NAME --resource-group $RESOURCE_GROUP --location $LOCATION --sku Standard_LRS

Por otro lado tienes que crear dos contenedores: uno donde estarán los videos de entrada, que vas a transcodificar, y otro para la salida generada. En este ejemplo los he llamado inputs y outputs respectivamente:

az storage container create --name inputs --account-name $STORAGE_NAME
az storage container create --name outputs --account-name $STORAGE_NAME --public-access blob

En el caso del contenedor de outputs es necesario que el acceso público esté a nivel de blob, para poder probar con el player que todo funciona correctamente. Sube algunos videos que puedas utilizar para probar la transcodificación.

Videos de ejemplo en el contenedor inputs

Por último, habilita en la configuración de CORS de la cuenta de almacenamiento el dominio de bitmovin, ya que voy a utilizar el player hospedado en este para la prueba final.

az storage cors add --origins https://bitmovin.com --methods GET --services b --account-name $STORAGE_NAME

Ahora ya tienes todos los prerrequisitos para probar el encoder de Bitmovin con Microsoft Azure.

Código de ejemplo

Para poder hacer la prueba, he basado mi código en este artículo. En él se permite elegir entre varios proveedores cloud, como AWS o GCP, para ver el código necesario, pero no muestra la opción de Azure, aunque también está soportada. Todo el código del ejemplo lo tienes en mi GitHub, pero aquí vamos a ver paso a paso los diferentes puntos:

Instala el módulo de Bitmovin

Antes de comenzar con él código, lo primero que necesitas es crear un proyecto de Node.js, e instalar el SDK de Bitmovin y el módulo dotenv, que usaré para trabajar con las variables de entorno:

mkdir bitmovin-on-azure && cd bitmovin-on-azure
npm init -y
npm install --save @bitmovin/api-sdk dotenv

Crea un archivo .env con las siguientes variables:

YOUR_API_KEY="YOUR_BITMOVIN_API_KEY"
ACCOUNT_NAME="YOUR_AZURE_STORAGE_NAME"
CONTAINER_INPUTS="inputs"
CONTAINER_OUTPUTS="outputs"
ACCOUNT_KEY="YOUR_AZURE_STORAGE_ACCOUNT_KEU"
INPUT_FILE=YOUR_MOVIE_FILE.MP4

Por otro lado, crea un archivo llamado app.js, donde debes añadir estas dependencias:

//Step 1: Add the Bitmovin SDK to Your Project
require('dotenv').config();
const path = require('path');
const BitmovinApi = require('@bitmovin/api-sdk').default;
const {
    CloudRegion,
    AzureInput,
    AzureOutput,
    H264VideoConfiguration,
    AacAudioConfiguration,
    Encoding,
    StreamInput,
    Stream,
    AclEntry,
    AclPermission,
    TsMuxing,
    MuxingStream,
    EncodingOutput,
    VideoAdaptationSet,
    AudioAdaptationSet,
    DashFmp4Representation,
    Fmp4Muxing,
    Period,
    DashManifest,
    StreamSelectionMode,
    PresetConfiguration,
    HlsManifest,
    AudioMediaInfo,
    StreamInfo,
    DashRepresentationType
} = require('@bitmovin/api-sdk');

Ahora, dentro de una función asíncrona que he llamado main, iré incluyendo los siguientes pasos:

Configurar el cliente de Bitmovin

Para poder interactuar con la API de Bitmovin primero necesitas instanciar un cliente, donde lo único que necesitas es la API key que puedes recuperar en el Dashboard.

//Step 2: Setup a Bitmovin API Client Instance
const bitmovinApi = new BitmovinApi({
    apiKey: process.env.YOUR_API_KEY
});

Crear una referencia al contenedor inputs

El objetivo de este artículo es transcodificar un video de ejemplo. Para que esto sea posible necesito definir una entrada que representará al sitio donde estarán alojados los diferentes videos. En Bitmovin se conoce como Input y este puede ser de muchos tipos: Https, FTP, S3, GCS y también Azure Storage. Para generarlo como este último debemos hacerlo de la siguiente forma:

//Step 3: Create an Input
console.log(`Create an AzureInput for ${process.env.INPUT_FILE}...`);
const input = await bitmovinApi.encoding.inputs.azure.create(
        new AzureInput({
            name: `Inputs container in ${process.env.ACCOUNT_NAME}`,
            accountName: process.env.ACCOUNT_NAME,
            accountKey: process.env.ACCOUNT_KEY,
            container: process.env.CONTAINER_INPUTS
        })
);

Como puedes ver, estoy generando un objeto AzureInput, para el que he especificado un nombre, el nombre de la cuenta de almacenamiento en Azure, el access key y el contenedor donde se encontrarán los archivos. En este punto todavía no estoy definiendo el archivo en concreto sino dónde se encuentra, por lo que este input podría utilizarlo entre diferentes archivos/transcodificaciones, sin tener que crear un nuevo input cada vez. En el Dashboard de Bitmovin ahora deberías de ver uno nuevo del tipo Azure:

Bitmovin Dashboard – Inputs – nuevo input representando al contenedor inputs

Crear referencia a la salida

Del mismo modo, también necesitamos generar la referencia para la salida, en este caso utilizando la clase AzureOutput:

//Step 4: Create an OutputLink
console.log(`Create an AzureOutput in ${process.env.ACCOUNT_NAME} (${process.env.CONTAINER_OUTPUTS} container)`);
    const output = await bitmovinApi.encoding.outputs.azure.create(
        new AzureOutput({
            name: `Outputs container in ${process.env.ACCOUNT_NAME}`,
            accountName: process.env.ACCOUNT_NAME,
            accountKey: process.env.ACCOUNT_KEY,
            container: process.env.CONTAINER_OUTPUTS, //Public access level must be blob
        })
);

Al igual que en el apartado Inputs, en el apartado Outputs te aparecerá una nueva salida del tipo Azure:

Bitmovin Dashboard – Inputs – nuevo output representando al contenedor outpus

Crear las configuraciones para el codec que quieres utilizar

Siguiendo el ejemplo del artículo de Bitmovin, el siguiente paso es crear las diferentes configuraciones que quieres utilizar durante la transcodificación, aunque también podrías recuperar algunas de las que ya te dan por defecto:

//Step 5: Create Codec Configurations
console.log(`Create codec configuration for H264`);
// Video Codec Configurations
const videoCodecConfiguration1 = await bitmovinApi.encoding.configurations.video.h264.create(
        new H264VideoConfiguration({
            name: 'H264 Codec Config for 1024',
            bitrate: 1500000,
            width: 1024,
            presetConfiguration: PresetConfiguration.VOD_STANDARD
        })
);
const videoCodecConfiguration2 = await bitmovinApi.encoding.configurations.video.h264.create(
        new H264VideoConfiguration({
            name: 'Getting Started H264 Codec for 768',
            bitrate: 1000000,
            width: 768,
            presetConfiguration: PresetConfiguration.VOD_STANDARD
        })
);
const videoCodecConfiguration3 = await bitmovinApi.encoding.configurations.video.h264.create(
        new H264VideoConfiguration({
            name: 'Getting Started H264 Codec for 640',
            bitrate: 750000,
            width: 640,
            presetConfiguration: PresetConfiguration.VOD_STANDARD
        })
);
//Audio Codec Configurations
const audioCodecConfiguration = await bitmovinApi.encoding.configurations.audio.aac.create(
        new AacAudioConfiguration({
            name: 'Getting Started Audio Codec Config',
            bitrate: 128000
        })
);

El resultado de esto es el siguiente:

Bitmovin – Encoding – Codec Configs

Crear el encoding

Ahora que ya tienes las diferentes configuraciones a las que quieres transformar tu video, lo siguiente que necesitas es definir el trabajo que hará la tarea:

//Step 6: Create Encoding
console.log(`Create encoding in Azure West Europe`);
const encoding = await bitmovinApi.encoding.encodings.create(
        new Encoding({
            name: `Encoding for ${process.env.INPUT_FILE} in ${CloudRegion.AZURE_EUROPE_WEST}`,
            cloudRegion: CloudRegion.AZURE_EUROPE_WEST
        })
);
//Video Stream  (Encodes the input file to a bitstream using the codec configuration)
const videoStreamInput1 = new StreamInput({
        inputId: input.id,
        inputPath: process.env.INPUT_FILE,
        selectionMode: StreamSelectionMode.AUTO
});
const videoStream1 = await bitmovinApi.encoding.encodings.streams.create(
        encoding.id,
        new Stream({
            codecConfigId: videoCodecConfiguration1.id,
            inputStreams: [videoStreamInput1]
        })
);
const videoStreamInput2 = new StreamInput({
        inputId: input.id,
        inputPath: process.env.INPUT_FILE,
        selectionMode: StreamSelectionMode.AUTO
});
const videoStream2 = await bitmovinApi.encoding.encodings.streams.create(
        encoding.id,
        new Stream({
            codecConfigId: videoCodecConfiguration2.id,
            inputStreams: [videoStreamInput2]
        })
);
const videoStreamInput3 = new StreamInput({
        inputId: input.id,
        inputPath: process.env.INPUT_FILE,
        selectionMode: StreamSelectionMode.AUTO
});
const videoStream3 = await bitmovinApi.encoding.encodings.streams.create(
        encoding.id,
        new Stream({
            codecConfigId: videoCodecConfiguration3.id,
            inputStreams: [videoStreamInput3]
        })
);
//Audio Stream
const audioStreamInput = new StreamInput({
        inputId: input.id,
        inputPath: process.env.INPUT_FILE,
        selectionMode: StreamSelectionMode.AUTO
});
const audioStream = await bitmovinApi.encoding.encodings.streams.create(
        encoding.id,
        new Stream({
            codecConfigId: audioCodecConfiguration.id,
            inputStreams: [audioStreamInput]
        })
);

En este ejemplo he utilizado como cloudRegion el valor de la constante CloudRegion.AZURE_EUROPE_WEST para que pueda utilizar los encoders que Bitmovin proporciona como servicio ya dentro de la nube de Azure.

Cuando el encoding esté generado podrás verlos en este apartado:

Bitmovin Dashboard – Encoding – Encodings

Los streams que generará este encoding los puedes ver aquí:

Bitmovin Dashboard – Encoding – Encodings > Streams

Crear los muxings

El paso final es crear los muxings para el video, para poder servirlos a través de DASH o HLS en este ejemplo.

//Step 7: Create a Muxing
//Muxings: Contenerizes the Stream to segments in formats like fmp4 (DASH), ts (HLS), webm
//  FMP4 (DASH)
// Video Muxings
const aclEntry = new AclEntry({
        permission: AclPermission.PUBLIC_READ
});
const segmentLength = 4;
const dashOutputPath = path.basename(process.env.INPUT_FILE, path.extname(process.env.INPUT_FILE));
const dashSegmentNaming = 'seg_%number%.m4s';
const initSegmentName = 'init.mp4';
const fmp4VideoMuxing1 = await bitmovinApi.encoding.encodings.muxings.fmp4.create(
        encoding.id,
        new Fmp4Muxing({
            segmentLength: segmentLength,
            segmentNaming: dashSegmentNaming,
            initSegmentName: initSegmentName,
            streams: [new MuxingStream({ streamId: videoStream1.id })],
            outputs: [
                new EncodingOutput({
                    outputId: outputId,
                    outputPath: dashOutputPath + '/video/1024_1500000/fmp4/',
                    acl: [aclEntry]
                })
            ]
        })
);
const fmp4VideoMuxing2 = await bitmovinApi.encoding.encodings.muxings.fmp4.create(
        encoding.id,
        new Fmp4Muxing({
            segmentLength: segmentLength,
            segmentNaming: dashSegmentNaming,
            initSegmentName: initSegmentName,
            streams: [new MuxingStream({ streamId: videoStream2.id })],
            outputs: [
                new EncodingOutput({
                    outputId: outputId,
                    outputPath: dashOutputPath + '/video/768_1000000/fmp4/',
                    acl: [aclEntry]
                })
            ]
        })
);
const videoMuxing3 = await bitmovinApi.encoding.encodings.muxings.fmp4.create(
        encoding.id,
        new Fmp4Muxing({
            segmentLength: segmentLength,
            segmentNaming: dashSegmentNaming,
            initSegmentName: initSegmentName,
            streams: [new MuxingStream({ streamId: videoStream3.id })],
            outputs: [
                new EncodingOutput({
                    outputId: outputId,
                    outputPath: dashOutputPath + '/video/640_750000/fmp4/',
                    acl: [aclEntry]
                })
            ]
        })
);
//Audio Muxings
const fmp4AudioMuxing = await bitmovinApi.encoding.encodings.muxings.fmp4.create(
        encoding.id,
        new Fmp4Muxing({
            segmentLength: segmentLength,
            segmentNaming: dashSegmentNaming,
            initSegmentName: initSegmentName,
            streams: [new MuxingStream({ streamId: audioStream.id })],
            outputs: [
                new EncodingOutput({
                    outputId: outputId,
                    outputPath: dashOutputPath + '/audio/128000/fmp4/',
                    acl: [aclEntry]
                })
            ]
        })
);
//TS for HLS
const hlsAclEntry = new AclEntry({
        permission: AclPermission.PUBLIC_READ
});
// const segmentLength = 4;
const hlsOutputPath = path.basename(process.env.INPUT_FILE, path.extname(process.env.INPUT_FILE));
const hlsSegmentNaming = 'seg_%number%.ts';
const tsVideoMuxing1 = await bitmovinApi.encoding.encodings.muxings.ts.create(
        encoding.id,
        new TsMuxing({
            segmentLength: segmentLength,
            segmentNaming: hlsSegmentNaming,
            streams: [new MuxingStream({ streamId: videoStream1.id })],
            outputs: [
                new EncodingOutput({
                    outputId: outputId,
                    outputPath: hlsOutputPath + '/video/1024_1500000/ts/',
                    acl: [hlsAclEntry]
                })
            ]
        })
);
const tsVideoMuxing2 = await bitmovinApi.encoding.encodings.muxings.ts.create(
        encoding.id,
        new TsMuxing({
            segmentLength: segmentLength,
            segmentNaming: hlsSegmentNaming,
            streams: [new MuxingStream({ streamId: videoStream2.id })],
            outputs: [
                new EncodingOutput({
                    outputId: outputId,
                    outputPath: hlsOutputPath + '/video/768_1000000/ts/',
                    acl: [aclEntry]
                })
            ]
        })
);
const tsVideoMuxing3 = await bitmovinApi.encoding.encodings.muxings.ts.create(
        encoding.id,
        new TsMuxing({
            segmentLength: segmentLength,
            segmentNaming: hlsSegmentNaming,
            streams: [new MuxingStream({ streamId: videoStream3.id })],
            outputs: [
                new EncodingOutput({
                    outputId: outputId,
                    outputPath: hlsOutputPath + '/video/640_750000/ts/',
                    acl: [aclEntry]
                })
            ]
        })
);
// Audio Muxings
const tsAudioMuxing = await bitmovinApi.encoding.encodings.muxings.ts.create(
        encoding.id,
        new TsMuxing({
            segmentLength: segmentLength,
            segmentNaming: hlsSegmentNaming,
            streams: [new MuxingStream({ streamId: audioStream.id })],
            outputs: [
                new EncodingOutput({
                    outputId: outputId,
                    outputPath: hlsOutputPath + '/audio/128000/ts/',
                    acl: [aclEntry]
                })
            ]
        })
);

Comenzar la transcodificación

Ahora que ya tienes todo lo necesario para comenzar la transcodifcación, lo único que necesitas es iniciarla:

//Step 8: Start Encoding
await bitmovinApi.encoding.encodings.start(encoding.id);

Este proceso llevará un tiempo, aunque una vez lanzada la demo continuará el proceso, sin esperar a su finalización.

Crear los manifiestos

Como en este ejemplo estamos trabajando con DASH y HLS necesitamos crear dos manifiestos:

//Step 9: Create a manifests
//  Create a DASH Manifest
const dashManifest = await bitmovinApi.encoding.manifests.dash.create(
        new DashManifest({
            name: `${process.env.INPUT_FILE} manifest for DASH`,
            manifestName: 'manifest.mpd',
            outputs: [
                new EncodingOutput({
                    outputId: outputId,
                    outputPath: dashOutputPath,
                    acl: [aclEntry]
                })
            ]
        })
);
const period = await bitmovinApi.encoding.manifests.dash.periods.create(
        dashManifest.id,
        new Period()
);
//Add Adaptation Sets
const videoAdaptationSet = await bitmovinApi.encoding.manifests.dash.periods.adaptationsets.video.create(
        dashManifest.id,
        period.id,
        new VideoAdaptationSet()
);
const audioAdaptationSet = await bitmovinApi.encoding.manifests.dash.periods.adaptationsets.audio.create(
        dashManifest.id,
        period.id,
        new AudioAdaptationSet({
            lang: 'en'
        })
    );
//Add Representations
// Adding Audio Representation
const audioRepresentation = await bitmovinApi.encoding.manifests.dash.periods.adaptationsets.representations.fmp4.create(
        dashManifest.id,
        period.id,
        audioAdaptationSet.id,
        new DashFmp4Representation({
            type: DashRepresentationType.TEMPLATE,
            encodingId: encoding.id,
            muxingId: fmp4AudioMuxing.id,
            segmentPath: 'audio/128000/fmp4'
        })
    );
// Adding Video Representation
const videoRepresentation1 = await bitmovinApi.encoding.manifests.dash.periods.adaptationsets.representations.fmp4.create(
        dashManifest.id,
        period.id,
        videoAdaptationSet.id,
        new DashFmp4Representation({
            type: DashRepresentationType.TEMPLATE,
            encodingId: encoding.id,
            muxingId: fmp4VideoMuxing1.id,
            segmentPath: 'video/1024_1500000/fmp4'
        })
    );
// Adding Video Representation
const videoRepresentation2 = await bitmovinApi.encoding.manifests.dash.periods.adaptationsets.representations.fmp4.create(
        dashManifest.id,
        period.id,
        videoAdaptationSet.id,
        new DashFmp4Representation({
            type: DashRepresentationType.TEMPLATE,
            encodingId: encoding.id,
            muxingId: fmp4VideoMuxing2.id,
            segmentPath: 'video/768_1000000/fmp4'
        })
    );
// Adding Video Representation
const videoRepresentation3 = await bitmovinApi.encoding.manifests.dash.periods.adaptationsets.representations.fmp4.create(
        dashManifest.id,
        period.id,
        videoAdaptationSet.id,
        new DashFmp4Representation({
            type: DashRepresentationType.TEMPLATE,
            encodingId: encoding.id,
            muxingId: videoMuxing3.id,
            segmentPath: 'video/640_750000/fmp4'
        })
    );
//Start Manifest Generation
console.log(`Start DASH manifest generation`);
await bitmovinApi.encoding.manifests.dash.start(dashManifest.id);
//HLS Manifest
//Create a HLS manifest
console.log(`Create HLS manifest`);
const hlsManifest = await bitmovinApi.encoding.manifests.hls.create(
        new HlsManifest({
            name: `${process.env.INPUT_FILE} manifest for HLS`,
            manifestName: 'manifest.m3u8',
            outputs: [
                new EncodingOutput({
                    outputId: outputId,
                    outputPath: hlsOutputPath,
                    acl: [aclEntry]
                })
            ]
        })
    );
// Create Audio Media Info and Video playlist
const audioMediaInfo = new AudioMediaInfo({
        name: 'my-audio-media',
        groupId: 'audio_group',
        segmentPath: 'audio/128000/ts',
        uri: 'audiomedia.m3u8',
        encodingId: encoding.id,
        streamId: audioStream.id,
        muxingId: tsAudioMuxing.id,
        language: 'en'
    });
await bitmovinApi.encoding.manifests.hls.media.audio.create(hlsManifest.id, audioMediaInfo);
const streamInfo1 = await bitmovinApi.encoding.manifests.hls.streams.create(
        hlsManifest.id,
        new StreamInfo({
            audio: 'audio_group',
            closedCaptions: 'NONE',
            segmentPath: 'video/1024_1500000/ts',
            uri: 'video1.m3u8',
            encodingId: encoding.id,
            streamId: videoStream1.id,
            muxingId: tsVideoMuxing1.id
        })
);
const streamInfo2 = await bitmovinApi.encoding.manifests.hls.streams.create(
        hlsManifest.id,
        new StreamInfo({
            audio: 'audio_group',
            closedCaptions: 'NONE',
            segmentPath: 'video/768_1000000/ts',
            uri: 'video2.m3u8',
            encodingId: encoding.id,
            streamId: videoStream2.id,
            muxingId: tsVideoMuxing2.id
        })
);
const streamInfo3 = await bitmovinApi.encoding.manifests.hls.streams.create(
        hlsManifest.id,
        new StreamInfo({
            audio: 'audio_group',
            closedCaptions: 'NONE',
            segmentPath: 'video/640_750000/ts',
            uri: 'video3.m3u8',
            encodingId: encoding.id,
            streamId: videoStream3.id,
            muxingId: tsVideoMuxing3.id
        })
);
//Start Manifest Generation
console.log(`Start HLS manifest generation`);
await bitmovinApi.encoding.manifests.hls.start(hlsManifest.id);

Estos y su configuración podrás verlos en el apartado Manifests:

Bitmovin Dashboard – Encoding – Manifests

Por último, para hacer más fácil la prueba final, he generado las URLs que espero para la reproducción a través de DASH o HLS con el reproductor hospedado en Bitmovin:

console.log(`Test DASH:`);
console.log(`https://bitmovin.com/demos/stream-test?format=dash&manifest=https%3A%2F%2F${process.env.ACCOUNT_NAME}.blob.core.windows.net%2F${process.env.CONTAINER_OUTPUTS}%2F${path.basename(process.env.INPUT_FILE, path.extname(process.env.INPUT_FILE))}%2Fmanifest.mpd`)
console.log(`Test HLS:`);
console.log(`https://bitmovin.com/demos/stream-test?format=hls&manifest=https%3A%2F%2F${process.env.ACCOUNT_NAME}.blob.core.windows.net%2F${process.env.CONTAINER_OUTPUTS}%2F${path.basename(process.env.INPUT_FILE, path.extname(process.env.INPUT_FILE))}%2Fmanifest.m3u8`);

El resultado debería de ser algo así:

Resultado de la ejecución del código de ejemplo

Con este ejemplo ya puedes probar la integración de Bitmovin con Microsoft Azure. El código de ejemplo completo lo tienes en mi GitHub.

¡Saludos!