import { ChildProcess } from 'child_process' ; import * as events from 'events' ; import { inject , injectable } from 'inversify' ; import * as path from 'path' ; import * as apid from '../../../../api' ; import FileUtil from '../../../util/FileUtil' ; import ProcessUtil from '../../../util/ProcessUtil' ; import Util from '../../../util/Util' ; import IVideoUtil , { VideoInfo } from '../../api/video/IVideoUtil' ; import IChannelDB from '../../db/IChannelDB' ; import IRecordedDB from '../../db/IRecordedDB' ; import IVideoFileDB from '../../db/IVideoFileDB' ; import IEncodeEvent from '../../event/IEncodeEvent' ; import IConfiguration from '../../IConfiguration' ; import ILogger from '../../ILogger' ; import ILoggerModel from '../../ILoggerModel' ; import IEncodeFileManageModel from './IEncodeFileManageModel' ; import IEncodeProcessManageModel from './IEncodeProcessManageModel' ; import { EncodeOption , EncodeProgressInfo , IEncoderModel } from './IEncoderModel' ; @ injectable ( ) class EncoderModel implements IEncoderModel { private log : ILogger ; private configure : IConfiguration ; private processManager : IEncodeProcessManageModel ; private fileManager : IEncodeFileManageModel ; private videoFileDB : IVideoFileDB ; private recordedDB : IRecordedDB ; private channelDB : IChannelDB ; private videoUtil : IVideoUtil ; private encodeEvent : IEncodeEvent ; private listener : events . EventEmitter = new events . EventEmitter ( ) ; private encodeOption : EncodeOption | null = null ; private childProcess : ChildProcess | null = null ; private timerId : NodeJS . Timer | null = null ; private isCanceld : boolean = false ; private progressInfo : EncodeProgressInfo | null = null ; constructor ( @ inject ( 'ILoggerModel' ) logger , @ inject ( 'IConfiguration' ) configure , @ inject ( 'IEncodeProcessManageModel' ) processManager , @ inject ( 'IEncodeFileManageModel' ) fileManager , @ inject ( 'IVideoFileDB' ) videoFileDB , @ inject ( 'IRecordedDB' ) recordedDB , @ inject ( 'IChannelDB' ) channelDB , @ inject ( 'IVideoUtil' ) videoUtil , @ inject ( 'IEncodeEvent' ) encodeEvent ) { this . log = logger . getLogger ( ) ; this . configure = configure ; this . processManager = processManager ; this . fileManager = fileManager ; this . videoFileDB = videoFileDB ; this . recordedDB = recordedDB ; this . channelDB = channelDB ; this . videoUtil = videoUtil ; this . encodeEvent = encodeEvent ; } param encodeOption public setOption ( encodeOption ) { if ( this . encodeOption !== null ) { this . log . encode . error ( 'encodeOption is not null' ) ; throw new Error ( 'EncodeSetOptionError' ) ; } this . encodeOption = encodeOption ; } param callback public setOnFinish ( callback ) { this . listener . once ( EncoderModel . ENCODE_FINISH_EVENT , ( isError , outputFilePath ) => { callback ( isError , outputFilePath ) ; } ) ; } /**
     * エンコード開始
     */ public async start ( ) { if ( this . encodeOption === null ) { this . log . encode . error ( 'encodeOption is null' ) ; throw new Error ( 'EncodeOptionIsNull' ) ; } const video = await this . videoFileDB . findId ( this . encodeOption . sourceVideoFileId ) ; if ( video === null ) { throw new Error ( 'VideoFileIdIsNotFound' ) ; } const recorded = await this . recordedDB . findId ( this . encodeOption . recordedId ) ; if ( recorded === null ) { throw new Error ( 'RecordedIsNotFound' ) ; } const channel = await this . channelDB . findId ( recorded . channelId ) ; if ( channel === null ) { throw new Error ( 'CannelIsNotFound' ) ; } const inputFilePath = await this . videoUtil . getFullFilePathFromId ( this . encodeOption . sourceVideoFileId ) ; if ( inputFilePath === null ) { throw new Error ( 'VideoPathIsNotFound' ) ; } try { await FileUtil . stat ( inputFilePath ) ; } catch ( err ) { this . log . encode . error ( `video file is not found: ${ inputFilePath }` ) ; throw err ; } const encodeCmd = this . configure . getConfig ( ) . encode . find ( enc => { return enc . name === this . encodeOption ?. mode ; } ) ; if ( typeof encodeCmd === 'undefined' ) { throw new Error ( 'EncodeCommandIsNotFound' ) ; } const outputDirPath = typeof encodeCmd . suffix === 'undefined' ? null : this . getDirPath ( this . encodeOption ) ; if ( outputDirPath !== null ) { try { await FileUtil . stat ( outputDirPath ) ; } catch ( e ) { this . log . encode . info ( `mkdirp: ${ outputDirPath }` ) ; await FileUtil . mkdir ( outputDirPath ) ; } } const outputFilePath = outputDirPath === null || typeof encodeCmd . suffix === 'undefined' ? null : await this . fileManager . getFilePath ( outputDirPath , inputFilePath , encodeCmd . suffix ) ; const config = this . configure . getConfig ( ) ; let dir = '' ; if ( typeof encodeCmd . suffix === 'undefined' && typeof this . encodeOption . directory !== 'undefined' ) { dir = this . encodeOption . directory ; } else if ( outputFilePath !== null ) { dir = outputFilePath ; } this . log . encode . info ( `encode start. mode: ${ this . encodeOption . mode } name: ${ recorded . name } file: ${ inputFilePath } -> ${ outputFilePath }` ) ; this . log . encode . info ( `encodeId: ${ this . encodeOption . encodeId }` ) ; this . log . encode . info ( `encodeCmd.suffix: ${ encodeCmd . suffix }` ) ; this . log . encode . info ( `queueItem.directory: ${ this . encodeOption . directory }` ) ; this . log . encode . info ( `outputFilePath: ${ outputFilePath }` ) ; this . childProcess = await this . processManager . create ( { input : inputFilePath , output : outputFilePath , cmd : encodeCmd . cmd , priority : EncoderModel . ENCODE_PRIPORITY , spawnOption : { env : { ... process . env , RECORDEDID : recorded . id . toString ( 10 ) , INPUT : inputFilePath , OUTPUT : outputFilePath === null ? '' : outputFilePath , DIR : dir , SUBDIR : this . encodeOption . directory || '' , FFMPEG : config . ffmpeg , FFPROBE : config . ffprobe , NAME : recorded . name , HALF_WIDTH_NAME : recorded . halfWidthName , DESCRIPTION : recorded . description || '' , HALF_WIDTH_DESCRIPTION : recorded . halfWidthDescription || '' , EXTENDED : recorded . extended || '' , HALF_WIDTH_EXTENDED : recorded . halfWidthExtended || '' , VIDEOTYPE : recorded . videoType || '' , VIDEORESOLUTION : recorded . videoResolution || '' , VIDEOSTREAMCONTENT : typeof recorded . videoStreamContent === 'number' ? recorded . videoStreamContent . toString ( 10 ) : '' , VIDEOCOMPONENTTYPE : typeof recorded . videoComponentType === 'number' ? recorded . videoComponentType . toString ( 10 ) : '' , AUDIOSAMPLINGRATE : typeof recorded . audioSamplingRate === 'number' ? recorded . audioSamplingRate . toString ( 10 ) : '' , AUDIOCOMPONENTTYPE : typeof recorded . audioComponentType === 'number' ? recorded . audioComponentType . toString ( 10 ) : '' , CHANNELID : typeof recorded . channelId === 'number' ? recorded . channelId . toString ( 10 ) : '' , CHANNELNAME : typeof channel . name === 'string' ? channel . name : '' , HALF_WIDTH_CHANNELNAME : typeof channel . halfWidthName === 'string' ? channel . halfWidthName : '' , GENRE1 : typeof recorded . genre1 === 'number' ? recorded . genre1 . toString ( 10 ) : '' , SUBGENRE1 : typeof recorded . subGenre1 === 'number' ? recorded . subGenre1 . toString ( 10 ) : '' , GENRE2 : typeof recorded . genre2 === 'number' ? recorded . genre2 . toString ( 10 ) : '' , SUBGENRE2 : typeof recorded . subGenre2 === 'number' ? recorded . subGenre2 . toString ( 10 ) : '' , GENRE3 : typeof recorded . genre3 === 'number' ? recorded . genre3 . toString ( 10 ) : '' , SUBGENRE3 : typeof recorded . subGenre3 === 'number' ? recorded . subGenre3 . toString ( 10 ) : '' , START_AT : recorded . startAt . toString ( 10 ) , END_AT : recorded . endAt . toString ( 10 ) , DROPLOG_ID : recorded . dropLogFile ?. id . toString ( 10 ) || '' , DROPLOG_PATH : recorded . dropLogFile ?. filePath || '' , ERROR_CNT : recorded . dropLogFile ?. errorCnt . toString ( 10 ) || '' , DROP_CNT : recorded . dropLogFile ?. dropCnt . toString ( 10 ) || '' , SCRAMBLING_CNT : recorded . dropLogFile ?. scramblingCnt . toString ( 10 ) || '' , } , } , } ) ; this . timerId = setTimeout ( async ( ) => { if ( this . encodeOption === null ) { return ; } this . log . encode . error ( `encode process is time out: ${ this . encodeOption . encodeId } ${ outputFilePath }` ) ; await this . cancel ( ) ; } , recorded . duration * ( typeof encodeCmd . rate === 'undefined' ? EncoderModel . DEFAULT_TIMEOUT_RATE : encodeCmd . rate ) ) ; /**
         * プロセスの設定
         */ if ( this . childProcess . stderr !== null ) { this . childProcess . stderr . on ( 'data' , data => { this . log . encode . debug ( String ( data ) ) ; } ) ; } if ( this . childProcess . stdout !== null ) { let videoInfo = null ; try { videoInfo = await this . videoUtil . getInfo ( inputFilePath ) ; } catch ( err ) { this . log . encode . error ( `get encode vidoe file info: ${ inputFilePath }` ) ; this . log . encode . error ( err ) ; } if ( videoInfo !== null ) { this . childProcess . stdout . on ( 'data' , data => { try { this . updateEncodingProgressInfo ( data ) ; } catch ( err ) { } } ) ; } } this . childProcess . on ( 'exit' , async ( code , signal ) => { this . childEndProcessing ( code , signal , outputFilePath ) ; } ) ; if ( ProcessUtil . isExited ( this . childProcess ) === true ) { this . childEndProcessing ( this . childProcess . exitCode , this . childProcess . signalCode , outputFilePath ) ; this . childProcess . removeAllListeners ( ) ; } } param queueItem return private getDirPath ( queueItem ) { const parentDir = this . videoUtil . getParentDirPath ( queueItem . parentDir ) ; if ( parentDir === null ) { this . log . encode . error ( `parent dir config is not found: ${ queueItem . parentDir }` ) ; throw new Error ( 'parentDirIsNotFound' ) ; } return typeof queueItem . directory === 'undefined' ? parentDir : path . join ( parentDir , queueItem . directory ) ; } param data param encodeId private updateEncodingProgressInfo ( data ) { if ( this . encodeOption === null ) { return ; } const logs = String ( data ) . split ( '\n' ) ; for ( let j = 0 ; j < logs . length ; j ++ ) { if ( logs [ j ] != '' ) { const log = JSON . parse ( String ( logs [ j ] ) ) ; this . log . encode . debug ( log ) ; if ( log . type === 'progress' && typeof log . percent === 'number' && typeof log . log === 'string' ) { this . progressInfo = { percent : log . percent , log : log . log , } ; this . encodeEvent . emitUpdateEncodeProgress ( ) ; } } } } param code param signal param outputFilePath param queueItem private async childEndProcessing ( code , signal , outputFilePath ) { this . log . encode . info ( `exit code: ${ code }, signal: ${ signal }` ) ; if ( this . timerId !== null ) { clearTimeout ( this . timerId ) ; } if ( outputFilePath !== null ) { this . fileManager . release ( outputFilePath ) ; } if ( this . encodeOption === null ) { this . log . encode . error ( 'encodeOptionIsNull' ) ; return ; } let isError = true ; if ( this . isCanceld === true ) { this . log . encode . info ( `canceld encode: ${ this . encodeOption . encodeId }` ) ; } else if ( code !== 0 ) { this . log . encode . error ( `encode failed: ${ this . encodeOption . encodeId } ${ outputFilePath }` ) ; } else { this . log . encode . info ( `Successfully encod: ${ this . encodeOption . encodeId } ${ outputFilePath }` ) ; isError = false ; } if ( isError === true ) { if ( outputFilePath !== null ) { this . log . encode . info ( `delete encode output file: ${ outputFilePath }` ) ; await Util . sleep ( 1000 ) ; await FileUtil . unlink ( outputFilePath ) . catch ( err => { this . log . encode . error ( `delete encode output file failed: ${ outputFilePath }` ) ; this . log . encode . error ( err ) ; } ) ; } } this . listener . emit ( EncoderModel . ENCODE_FINISH_EVENT , isError , outputFilePath ) ; this . listener . removeAllListeners ( ) ; } /**
     * キャンセル処理
     */ public async cancel ( ) { if ( this . encodeOption === null ) { return ; } this . log . encode . info ( `cancel encode: ${ this . encodeOption . encodeId }` ) ; if ( this . childProcess !== null ) { this . log . encode . info ( `kill encode process encodeId: ${ this . encodeOption . encodeId }, pid: ${ this . childProcess . pid }` ) ; this . isCanceld = true ; await ProcessUtil . kill ( this . childProcess ) . catch ( err => { this . log . encode . error ( `kill encode process failed: ${ this . encodeOption ?. encodeId }` ) ; this . log . encode . error ( err ) ; } ) ; } } returns public getEncodeOption ( ) { return this . encodeOption ; } returns public getProgressInfo ( ) { return this . progressInfo ; } returns public getEncodeId ( ) { return this . encodeOption === null ? null : this . encodeOption . encodeId ; } } namespace EncoderModel { export const ENCODE_FINISH_EVENT = 'encodeFinishEvent' ; export const ENCODE_PRIPORITY = 10 ; export const DEFAULT_TIMEOUT_RATE = 4.0 ; } export default EncoderModel ;