import { ChildProcess , exec } from 'child_process' ; import * as fs from 'fs' ; import { inject , injectable } from 'inversify' ; import internal , { Readable } from 'stream' ; import ID3MetadataTransform from 'arib-subtitle-timedmetadater' ; import * as apid from '../../../../../api' ; import * as fst from '../../../../lib/TailStream' ; import ProcessUtil from '../../../../util/ProcessUtil' ; import IVideoUtil from '../../../api/video/IVideoUtil' ; import IRecordedDB from '../../../db/IRecordedDB' ; import IVideoFileDB from '../../../db/IVideoFileDB' ; import IConfiguration from '../../../IConfiguration' ; import ILoggerModel from '../../../ILoggerModel' ; import IEncodeProcessManageModel , { CreateProcessOption } from '../../encode/IEncodeProcessManageModel' ; import ISocketIOManageModel from '../../socketio/ISocketIOManageModel' ; import IHLSFileDeleterModel from '../util/IHLSFileDeleterModel' ; import IRecordedStreamBaseModel , { RecordedStreamOption , VideoFileInfo } from './IRecordedStreamBaseModel' ; import { RecordedStreamInfo } from './IStreamBaseModel' ; import StreamBaseModel from './StreamBaseModel' ; @ injectable ( ) export default abstract class RecordedStreamBaseModel extends StreamBaseModel < RecordedStreamOption > implements IRecordedStreamBaseModel { private videoFileDB : IVideoFileDB ; private recordedDB : IRecordedDB ; private videoUtil : IVideoUtil ; private fileStream : Readable | null = null ; private id3MetadataTransoform : ID3MetadataTransform | null = null ; private streamProcess : ChildProcess | null = null ; private videoFilePath : string | null = null ; private videoFileInfo : VideoFileInfo | null = null ; private videoFileType : apid . VideoFileType = 'encoded' ; private isRecording : boolean = false ; constructor ( @ inject ( 'IConfiguration' ) configure , @ inject ( 'ILoggerModel' ) logger , @ inject ( 'IEncodeProcessManageModel' ) processManager , @ inject ( 'IHLSFileDeleterModel' ) fileDeleter , @ inject ( 'ISocketIOManageModel' ) socketIO , @ inject ( 'IVideoFileDB' ) videoFileDB , @ inject ( 'IRecordedDB' ) recordedDB , @ inject ( 'IVideoUtil' ) videoUtil ) { super ( configure , logger , processManager , fileDeleter , socketIO ) ; this . videoFileDB = videoFileDB ; this . recordedDB = recordedDB ; this . videoUtil = videoUtil ; } param streamId return return public async start ( streamId ) { if ( this . getStreamType ( ) === 'RecordedHLS' ) { await this . prepStreamDir ( streamId ) ; } if ( this . processOption === null ) { throw new Error ( 'ProcessOptionIsNull' ) ; } await this . setVideFileInfo ( ) ; if ( this . videoFilePath === null || this . videoFileInfo === null ) { throw new Error ( 'SetVideoFileInfoError' ) ; } if ( this . processOption . playPosition > this . videoFileInfo . duration ) { throw new Error ( 'OutOfRange' ) ; } try { this . setFileStream ( ) ; } catch ( err ) { this . log . stream . error ( 'create file stream error' ) ; this . log . stream . error ( err ) ; await this . stop ( ) ; throw new Error ( 'FileStreamSetError' ) ; } const poption = await this . createProcessOption ( streamId ) ; this . log . stream . info ( `create encode process: ${ poption . cmd }` ) ; try { this . streamProcess = await this . processManager . create ( poption ) ; } catch ( err ) { this . log . stream . error ( `create encode process failed: ${ poption . cmd }` ) ; await this . stop ( ) ; } if ( this . streamProcess === null ) { throw new Error ( 'CreateStreamProcessError' ) ; } if ( this . getStreamType ( ) !== 'RecordedHLS' ) { this . streamProcess . on ( 'exit' , ( ) => { this . emitExitStream ( ) ; } ) ; this . streamProcess . on ( 'error' , ( ) => { this . emitExitStream ( ) ; } ) ; } else { this . startCheckStreamEnable ( streamId ) ; } this . setStopTimer ( ) ; if ( this . streamProcess . stderr !== null ) { this . streamProcess . stderr . on ( 'data' , data => { this . log . stream . debug ( String ( data ) ) ; } ) ; } if ( this . streamProcess . stdin !== null && this . fileStream !== null ) { if ( this . videoFileType === 'ts' && this . getStreamType ( ) === 'RecordedHLS' ) { this . log . stream . info ( 'use arib-subtitle-timedmetadater' ) ; this . id3MetadataTransoform = new ID3MetadataTransform ( ) ; this . fileStream . pipe ( this . id3MetadataTransoform ) ; this . id3MetadataTransoform . pipe ( this . streamProcess . stdin ) ; } else { this . fileStream . pipe ( this . streamProcess . stdin ) ; } } if ( ProcessUtil . isExited ( this . streamProcess ) === true ) { this . streamProcess . removeAllListeners ( ) ; this . emitExitStream ( ) ; } } return private async setVideFileInfo ( ) { if ( this . processOption === null ) { throw new Error ( 'ProcessOptionIsNull' ) ; } const video = await this . videoFileDB . findId ( this . processOption . videoFileId ) ; if ( video === null ) { throw new Error ( 'VideoIsNull' ) ; } const recorded = await this . recordedDB . findId ( video . recordedId ) ; if ( recorded === null ) { throw new Error ( 'RecordedIsNull' ) ; } this . isRecording = recorded . isRecording ; this . videoFilePath = await this . videoUtil . getFullFilePathFromId ( video . id ) ; if ( this . videoFilePath === null ) { throw new Error ( 'GetVideoFilePathError' ) ; } this . videoFileInfo = await this . getVideoInfo ( this . videoFilePath ) ; this . videoFileType = video . type as apid . VideoFileType ; } param filePath return private getVideoInfo ( filePath ) { return new Promise < VideoFileInfo > ( ( resolve , reject ) => { exec ( `${ this . config . ffprobe } -v 0 -show_format -of json "${ filePath }"` , ( err , std ) => { if ( err ) { reject ( err ) ; return ; } const result = < any > JSON . parse ( std ) ; resolve ( { duration : parseFloat ( result . format . duration ) , size : parseInt ( result . format . size , 10 ) , bitRate : parseFloat ( result . format . bit_rate ) , } ) ; } ) ; } ) ; } return private async createProcessOption ( streamId ) { if ( this . processOption === null ) { throw new Error ( 'ProcessOptionIsNull' ) ; } if ( this . videoFilePath === null || this . videoFileInfo === null ) { throw new Error ( 'SetVideoFileInfoError' ) ; } let cmd = this . processOption . cmd . replace ( /%FFMPEG%/g , this . config . ffmpeg ) . replace ( /%SS%/g , this . videoFileType === 'ts' ? '' : this . processOption . playPosition . toString ( 10 ) ) ; if ( this . getStreamType ( ) === 'RecordedHLS' ) { cmd = cmd . replace ( /%streamFileDir%/g , this . config . streamFilePath ) . replace ( /%streamNum%/g , streamId . toString ( 10 ) ) ; } const option = { input : this . isRecording === true ? null : this . videoFilePath , output : this . getStreamType ( ) === 'RecordedHLS' ? `${ this . config . streamFilePath }\/stream${ streamId . toString ( 10 ) }.m3u8` : null , cmd : cmd , priority : RecordedStreamBaseModel . ENCODE_PROCESS_PRIORITY , } ; return option ; } /**
     * fileStream をセットする
     */ private setFileStream ( ) { if ( this . processOption === null || this . videoFilePath === null || this . videoFileInfo === null ) { throw new Error ( 'VideoFileError' ) ; } if ( this . videoFileType === 'encoded' ) { return ; } this . log . stream . info ( `create file stream: ${ this . videoFilePath }` ) ; const start = Math . floor ( ( this . videoFileInfo . bitRate / 8 ) * this . processOption . playPosition ) ; if ( this . isRecording === true ) { this . fileStream = fst . createReadStream ( this . videoFilePath , { start : start , } ) ; } else { this . fileStream = fs . createReadStream ( this . videoFilePath , { start : start , } ) ; } } return public async stop ( ) { await super . stop ( ) ; if ( this . fileStream !== null ) { this . fileStream . unpipe ( ) ; this . fileStream . destroy ( ) ; } if ( this . id3MetadataTransoform !== null ) { this . id3MetadataTransoform . unpipe ( ) ; this . id3MetadataTransoform . destroy ( ) ; } if ( this . streamProcess !== null ) { await ProcessUtil . kill ( this . streamProcess ) ; } if ( this . getStreamType ( ) === 'RecordedHLS' ) { await this . fileDeleter . deleteAllFiles ( ) ; } } return public getStream ( ) { if ( this . streamProcess !== null && this . streamProcess . stdout !== null ) { return this . streamProcess . stdout ; } else { throw new Error ( 'StreamIsNull' ) ; } } return public getInfo ( ) { if ( this . processOption === null ) { throw new Error ( 'ProcessOptionIsNull' ) ; } if ( this . configMode === null ) { throw new Error ( 'ConfigModeIsNull' ) ; } return { type : this . getStreamType ( ) , mode : this . configMode , videoFileId : this . processOption . videoFileId , isEnable : this . isEnable ( ) , } ; } protected abstract getStreamType ( ) ; }