// node import * as fs from 'fs'; import * as path from 'path'; import * as assert from 'assert'; // npm import { v4 as uuid } from 'uuid'; import * as _ from 'lodash'; // @ownzones import { Rrtq } from '@ownzones/rrtq'; // app import { Server } from 'http'; import { RedisClient } from 'redis'; import { config } from '../config'; import { Worker } from '../lib/workers/worker'; import { IImageSegment, ISegment, PlaylistBuilder, SegmentType, } from '../lib/playlist-builder'; import { CacheManager, DynamodbCacheManager, ICacheManager, RedisCacheManager, } from '../lib/cache'; import { ffprobe, getFrameHash } from './utils/ffmpeg'; import { SegmentLoader } from '../lib/segment-loader'; import { connectApiInit } from './utils/zypline-mock'; import { ensureTestDynamoDBTable, flushDynamoDBTable } from './utils/dynamodb'; import { IComposition } from '../lib/connect-api'; import { TaskResponse } from '../utils/types'; describe('Video Segments', () => { let cacheManagerInstance: ICacheManager; let connectApi: Server; let rrtq: Rrtq; let worker: Worker; let definition: IComposition; before(async () => { definition = JSON.parse( fs.readFileSync(path.join(__dirname, './composition-definitions/definition-dv.json')).toString(), ) as IComposition; rrtq = new Rrtq(config); SegmentLoader.SUBSEGMENTS_COUNT = 2; cacheManagerInstance = CacheManager.getInstance(); if (cacheManagerInstance instanceof RedisCacheManager) { await cacheManagerInstance.options.client.flushdbAsync(); } else if (cacheManagerInstance instanceof DynamodbCacheManager) { await ensureTestDynamoDBTable(); await flushDynamoDBTable(); } await (rrtq.redisClient as RedisClient).flushdbAsync(); worker = new Worker(config, config.rrtq.videoNamespace); connectApi = connectApiInit(); }); after(async () => { await worker.stop(); connectApi.close(); await rrtq.stop(); cacheManagerInstance.stopCacheGC(); CacheManager.destroyInstance(); }); it('should test segment cache', () => { /* ** Testing the segment cache ** Considering that the source file is 30 frames long and the segment size is 30 frames, if we offset it by 5 frames ** the playlist output should be 2 segments first with 25 frames and second with 5 frames. */ // set the MERGE_SMALL_SEGMENTS_THRESHOLD to a small value, so the segments are not merged together PlaylistBuilder.MERGE_SMALL_SEGMENTS_THRESHOLD = 0.0001; const cacheDefinition = _.cloneDeep(definition); const segmentSize = 5; cacheDefinition.segments[0].sequences[0].resources[0].entryPoint = 5; cacheDefinition.segments[0].sequences[0].resources[0].sourceDuration = 30; const sourceDuration = cacheDefinition.segments[0].sequences[0].resources[0].sourceDuration; const playlist = PlaylistBuilder.buildPlaylist( cacheDefinition, '70ad0ea2-5ff8-4c02-8f3d-16f985867151', segmentSize, 'cacheLocation', '1', undefined, 'fed8de6a-5890-4c36-a203-cfcca695f485', ); const segments = playlist.segments as ISegment[]; assert.equal(segments.length, 2, '# of segments'); assert.equal(segments[0].editUnits, 25, 'First segment edit units'); assert.equal(segments[1].editUnits, 5, 'Second segment edit units'); assert.equal(segments[0].editUnits + segments[1].editUnits, sourceDuration, 'Check total edit units to be total duration'); }); it('should generate a master playlist', () => { const playlist = PlaylistBuilder.buildPlaylist( definition, '3cb9ff0e-aa10-4cfb-8e36-1ac8a87bfa5e', 5, 'cacheLocation', '1', ); assert.equal( JSON.stringify(playlist), '{"type":"master","mediaPlayList":"#EXTM3U\\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\\"audio\\",LANGUAGE=\\"eng\\",NAME=\\"English\\",AUTOSELECT=YES, DEFAULT=YES,URI=\\"/media-playlist?fileId=3cb9ff0e-aa10-4cfb-8e36-1ac8a87bfa5e&virtualTrackId=1aa84f5c-e6fe-473a-9e2f-23659a27c3e1\\"\\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\\"audio\\",LANGUAGE=\\"eng\\",NAME=\\"English\\",AUTOSELECT=YES, DEFAULT=YES,URI=\\"/media-playlist?fileId=3cb9ff0e-aa10-4cfb-8e36-1ac8a87bfa5e&virtualTrackId=85fc2bf5-0d29-47f7-a21f-528d37c4d6cf\\"\\n#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=195023,CODECS=\\"avc1.4d0028,mp4a.40.2,stpp.TTML.im1t\\",AUDIO=\\"audio\\"\\n/media-playlist?fileId=3cb9ff0e-aa10-4cfb-8e36-1ac8a87bfa5e&virtualTrackId=fed8de6a-5890-4c36-a203-cfcca695f485","sourceFile":{"id":"e5ed93cc-963b-4059-88da-e55dc48a4ade","locatorUrl":"s3://oz-zl-test-files/mediaview/src_prores_demux.mov","title":{"id":"1234","name":"test-title"}}}', ); }); it('should generate an image playlist', () => { const playlist = PlaylistBuilder.buildPlaylist( definition, '70ad0ea2-5ff8-4c02-8f3d-16f985867151', 5, 'cacheLocation', '1', undefined, 'fed8de6a-5890-4c36-a203-cfcca695f485', ); assert.deepEqual( JSON.parse(JSON.stringify(playlist)), JSON.parse('{"segments":[{"cacheLocation":"cacheLocation","orgId":"1","fileId": "e5ed93cc-963b-4059-88da-e55dc48a4ade", "url":"s3://oz-zl-test-files/mediaview/connect_mer_shrt_23976_vdm_hdr_pqp3d65_3840x2160_30frames_v3.mxf","entryPoint":0,"editUnits":30,"duration":1.25125,"totalStartTime":0,"startTime":0,"pixFmt":"rgb48le","width":3840,"height":2160,"editRate":23.976023976023978,"streamIndex":0,"codec":"jpeg2000","bitDepth":"12","mxfIndexUrl":null,"type":"image"}],"type":"image","mediaPlayList":"#EXTM3U\\n#EXT-X-VERSION:3\\n#EXT-X-TARGETDURATION:6\\n#EXT-X-MEDIA-SEQUENCE:0\\n#EXTINF:1.25125\\n/segment?fileId=70ad0ea2-5ff8-4c02-8f3d-16f985867151&virtualTrackId=fed8de6a-5890-4c36-a203-cfcca695f485&index=0\\n#EXT-X-ENDLIST","sourceFile":{"id":"e5ed93cc-963b-4059-88da-e55dc48a4ade","locatorUrl":"s3://oz-zl-test-files/mediaview/src_prores_demux.mov","title":{"id":"1234","name":"test-title"}}}'), ); }); it('should generate a ts image segment', async () => { const queue = await rrtq.createQueue( uuid(), { queueTimeout: 3000, taskTimeout: 2000, uniqueTasks: false, namespace: config.rrtq.videoNamespace, }, ); const unitRate = (1001 / 24000); const segmentInfo: IImageSegment = { orgId: uuid(), url: 's3://oz-zl-test-files/mediaview/connect_mer_shrt_23976_vdm_hdr_pqp3d65_3840x2160_30frames_v3.mxf', entryPoint: 10, editUnits: 20, duration: 20 * unitRate, totalStartTime: 10 * unitRate, startTime: 10 * unitRate, pixFmt: 'rgb48le', width: 3840, height: 2160, editRate: 24000 / 1001, streamIndex: 0, codec: 'jpeg2000', bitDepth: '12', type: SegmentType.Image, cacheLocation: `s3://oz-zl-test-files/mediaview/run/${uuid()}/`, fileId: 'e5ed93cc-963b-4059-88da-e55dc48a4ade', }; segmentInfo.id = SegmentLoader.generateSegmentId(segmentInfo); const sourceFile = { file: { id: 'e5ed93cc-963b-4059-88da-e55dc48a4ade', locatorUrl: 's3://oz-zl-test-files/mediaview/src_prores_demux.mov', title: { id: '1234', name: 'test-title', }, }, }; const transcodingTask = { sourceFile, segment: segmentInfo, }; const task = await queue.addTask(transcodingTask, segmentInfo.id); await queue.waitTasks(); const response = await task.getResponse() as TaskResponse; if (response.error) { assert.fail(JSON.stringify(response.errorObject)); } const videoStream = await ffprobe(response.result.s3Path); assert.deepEqual(videoStream, { duration: 0.834156, fieldOrder: 'progressive', frameRate: '24000/1001', height: 540, width: 960, }); assert.equal(await getFrameHash(response.result.s3Path, 2 * unitRate), '17a6d765759cc6564e859f231cf51ccf'); assert.equal(response.error, false); }); it('should generate a ts image segment from an interlaced j2k mxf', async () => { const queue = await rrtq.createQueue( uuid(), { queueTimeout: 3000, taskTimeout: 2000, uniqueTasks: false, namespace: config.rrtq.videoNamespace, }, ); const segmentInfo: IImageSegment = { orgId: uuid(), url: 's3://oz-zl-test-files/mediaview/chloe-j2k-interlaced.mxf', entryPoint: 0, editUnits: 30, duration: 30 * (1001 / 24000), totalStartTime: 0, startTime: 0, pixFmt: 'yuv422p10le', width: 1920, height: 540, editRate: 24000 / 1001, streamIndex: 0, codec: 'jpeg2000', bitDepth: '10', type: SegmentType.Image, cacheLocation: `s3://oz-zl-test-files/mediaview/run/${uuid()}/`, fileId: 'e5ed93cc-963b-4059-88da-e55dc48a4ade', }; segmentInfo.id = SegmentLoader.generateSegmentId(segmentInfo); const sourceFile = { file: { id: 'e5ed93cc-963b-4059-88da-e55dc48a4ade', locatorUrl: 's3://oz-zl-test-files/mediaview/chloe-j2k-interlaced.mxf', title: { id: '1234', name: 'test-title', }, }, }; const transcodingTask: any = { sourceFile, segment: segmentInfo, }; const task = await queue.addTask(transcodingTask, segmentInfo.id); await queue.waitTasks(); const response = await task.getResponse() as TaskResponse; if (response.error) { assert.fail(JSON.stringify(response.errorObject)); } const videoStream = await ffprobe(response.result.s3Path); assert.deepEqual(videoStream, { duration: 1.251244, fieldOrder: 'tt', frameRate: '24000/1001', height: 540, width: 960, }); assert.equal(await getFrameHash(response.result.s3Path, 2 * (1001 / 24000)), 'f39cbc14237a8580b0805561da9236d7'); assert.equal(response.error, false); }); });