mirror of https://github.com/Chocobozzz/PeerTube
				
				
				
			
		
			
				
	
	
		
			190 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			190 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
import { MutexInterface } from 'async-mutex'
 | 
						|
import { Job } from 'bullmq'
 | 
						|
import { ensureDir, move } from 'fs-extra/esm'
 | 
						|
import { stat } from 'fs/promises'
 | 
						|
import { basename, extname as extnameUtil, join } from 'path'
 | 
						|
import { pick } from '@peertube/peertube-core-utils'
 | 
						|
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
 | 
						|
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
 | 
						|
import { sequelizeTypescript } from '@server/initializers/database.js'
 | 
						|
import { MVideo, MVideoFile } from '@server/types/models/index.js'
 | 
						|
import { getVideoStreamDuration, getVideoStreamFPS } from '@peertube/peertube-ffmpeg'
 | 
						|
import { CONFIG } from '../../initializers/config.js'
 | 
						|
import { VideoFileModel } from '../../models/video/video-file.js'
 | 
						|
import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist.js'
 | 
						|
import { updatePlaylistAfterFileChange } from '../hls.js'
 | 
						|
import { generateHLSVideoFilename, getHlsResolutionPlaylistFilename } from '../paths.js'
 | 
						|
import { buildFileMetadata } from '../video-file.js'
 | 
						|
import { VideoPathManager } from '../video-path-manager.js'
 | 
						|
import { buildFFmpegVOD } from './shared/index.js'
 | 
						|
 | 
						|
// Concat TS segments from a live video to a fragmented mp4 HLS playlist
 | 
						|
export async function generateHlsPlaylistResolutionFromTS (options: {
 | 
						|
  video: MVideo
 | 
						|
  concatenatedTsFilePath: string
 | 
						|
  resolution: number
 | 
						|
  fps: number
 | 
						|
  isAAC: boolean
 | 
						|
  inputFileMutexReleaser: MutexInterface.Releaser
 | 
						|
}) {
 | 
						|
  return generateHlsPlaylistCommon({
 | 
						|
    type: 'hls-from-ts' as 'hls-from-ts',
 | 
						|
    inputPath: options.concatenatedTsFilePath,
 | 
						|
 | 
						|
    ...pick(options, [ 'video', 'resolution', 'fps', 'inputFileMutexReleaser', 'isAAC' ])
 | 
						|
  })
 | 
						|
}
 | 
						|
 | 
						|
// Generate an HLS playlist from an input file, and update the master playlist
 | 
						|
export function generateHlsPlaylistResolution (options: {
 | 
						|
  video: MVideo
 | 
						|
  videoInputPath: string
 | 
						|
  resolution: number
 | 
						|
  fps: number
 | 
						|
  copyCodecs: boolean
 | 
						|
  inputFileMutexReleaser: MutexInterface.Releaser
 | 
						|
  job?: Job
 | 
						|
}) {
 | 
						|
  return generateHlsPlaylistCommon({
 | 
						|
    type: 'hls' as 'hls',
 | 
						|
    inputPath: options.videoInputPath,
 | 
						|
 | 
						|
    ...pick(options, [ 'video', 'resolution', 'fps', 'copyCodecs', 'inputFileMutexReleaser', 'job' ])
 | 
						|
  })
 | 
						|
}
 | 
						|
 | 
						|
export async function onHLSVideoFileTranscoding (options: {
 | 
						|
  video: MVideo
 | 
						|
  videoFile: MVideoFile
 | 
						|
  videoOutputPath: string
 | 
						|
  m3u8OutputPath: string
 | 
						|
  filesLockedInParent?: boolean // default false
 | 
						|
}) {
 | 
						|
  const { video, videoFile, videoOutputPath, m3u8OutputPath, filesLockedInParent = false } = options
 | 
						|
 | 
						|
  // Create or update the playlist
 | 
						|
  const playlist = await retryTransactionWrapper(() => {
 | 
						|
    return sequelizeTypescript.transaction(async transaction => {
 | 
						|
      return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction)
 | 
						|
    })
 | 
						|
  })
 | 
						|
  videoFile.videoStreamingPlaylistId = playlist.id
 | 
						|
 | 
						|
  const mutexReleaser = !filesLockedInParent
 | 
						|
    ? await VideoPathManager.Instance.lockFiles(video.uuid)
 | 
						|
    : null
 | 
						|
 | 
						|
  try {
 | 
						|
    await video.reload()
 | 
						|
 | 
						|
    const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, videoFile)
 | 
						|
    await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
 | 
						|
 | 
						|
    // Move playlist file
 | 
						|
    const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, basename(m3u8OutputPath))
 | 
						|
    await move(m3u8OutputPath, resolutionPlaylistPath, { overwrite: true })
 | 
						|
    // Move video file
 | 
						|
    await move(videoOutputPath, videoFilePath, { overwrite: true })
 | 
						|
 | 
						|
    // Update video duration if it was not set (in case of a live for example)
 | 
						|
    if (!video.duration) {
 | 
						|
      video.duration = await getVideoStreamDuration(videoFilePath)
 | 
						|
      await video.save()
 | 
						|
    }
 | 
						|
 | 
						|
    const stats = await stat(videoFilePath)
 | 
						|
 | 
						|
    videoFile.size = stats.size
 | 
						|
    videoFile.fps = await getVideoStreamFPS(videoFilePath)
 | 
						|
    videoFile.metadata = await buildFileMetadata(videoFilePath)
 | 
						|
 | 
						|
    await createTorrentAndSetInfoHash(playlist, videoFile)
 | 
						|
 | 
						|
    const oldFile = await VideoFileModel.loadHLSFile({
 | 
						|
      playlistId: playlist.id,
 | 
						|
      fps: videoFile.fps,
 | 
						|
      resolution: videoFile.resolution
 | 
						|
    })
 | 
						|
 | 
						|
    if (oldFile) {
 | 
						|
      await video.removeStreamingPlaylistVideoFile(playlist, oldFile)
 | 
						|
      await oldFile.destroy()
 | 
						|
    }
 | 
						|
 | 
						|
    const savedVideoFile = await VideoFileModel.customUpsert(videoFile, 'streaming-playlist', undefined)
 | 
						|
 | 
						|
    await updatePlaylistAfterFileChange(video, playlist)
 | 
						|
 | 
						|
    return { resolutionPlaylistPath, videoFile: savedVideoFile }
 | 
						|
  } finally {
 | 
						|
    if (mutexReleaser) mutexReleaser()
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// ---------------------------------------------------------------------------
 | 
						|
 | 
						|
async function generateHlsPlaylistCommon (options: {
 | 
						|
  type: 'hls' | 'hls-from-ts'
 | 
						|
  video: MVideo
 | 
						|
  inputPath: string
 | 
						|
 | 
						|
  resolution: number
 | 
						|
  fps: number
 | 
						|
 | 
						|
  inputFileMutexReleaser: MutexInterface.Releaser
 | 
						|
 | 
						|
  copyCodecs?: boolean
 | 
						|
  isAAC?: boolean
 | 
						|
 | 
						|
  job?: Job
 | 
						|
}) {
 | 
						|
  const { type, video, inputPath, resolution, fps, copyCodecs, isAAC, job, inputFileMutexReleaser } = options
 | 
						|
  const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
 | 
						|
 | 
						|
  const videoTranscodedBasePath = join(transcodeDirectory, type)
 | 
						|
  await ensureDir(videoTranscodedBasePath)
 | 
						|
 | 
						|
  const videoFilename = generateHLSVideoFilename(resolution)
 | 
						|
  const videoOutputPath = join(videoTranscodedBasePath, videoFilename)
 | 
						|
 | 
						|
  const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
 | 
						|
  const m3u8OutputPath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
 | 
						|
 | 
						|
  const transcodeOptions = {
 | 
						|
    type,
 | 
						|
 | 
						|
    inputPath,
 | 
						|
    outputPath: m3u8OutputPath,
 | 
						|
 | 
						|
    resolution,
 | 
						|
    fps,
 | 
						|
    copyCodecs,
 | 
						|
 | 
						|
    isAAC,
 | 
						|
 | 
						|
    inputFileMutexReleaser,
 | 
						|
 | 
						|
    hlsPlaylist: {
 | 
						|
      videoFilename
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  await buildFFmpegVOD(job).transcode(transcodeOptions)
 | 
						|
 | 
						|
  const newVideoFile = new VideoFileModel({
 | 
						|
    resolution,
 | 
						|
    extname: extnameUtil(videoFilename),
 | 
						|
    size: 0,
 | 
						|
    filename: videoFilename,
 | 
						|
    fps: -1
 | 
						|
  })
 | 
						|
 | 
						|
  await onHLSVideoFileTranscoding({
 | 
						|
    video,
 | 
						|
    videoFile: newVideoFile,
 | 
						|
    videoOutputPath,
 | 
						|
    m3u8OutputPath,
 | 
						|
    filesLockedInParent: !inputFileMutexReleaser
 | 
						|
  })
 | 
						|
}
 |