mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-05 19:42:24 +02:00
Create and inject caption playlist in HLS master
This commit is contained in:
parent
a7be820abc
commit
6e44e7e29a
49 changed files with 1368 additions and 401 deletions
|
@ -20,7 +20,8 @@ import {
|
|||
CreatedAt,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
Is, Scopes,
|
||||
Is,
|
||||
Scopes,
|
||||
Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
|
@ -57,7 +58,6 @@ export enum ScopeNames {
|
|||
]
|
||||
}
|
||||
}))
|
||||
|
||||
@Table({
|
||||
tableName: 'videoRedundancy',
|
||||
indexes: [
|
||||
|
@ -77,7 +77,6 @@ export enum ScopeNames {
|
|||
]
|
||||
})
|
||||
export class VideoRedundancyModel extends SequelizeModel<VideoRedundancyModel> {
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date
|
||||
|
||||
|
@ -134,8 +133,8 @@ export class VideoRedundancyModel extends SequelizeModel<VideoRedundancyModel> {
|
|||
const videoUUID = videoStreamingPlaylist.Video.uuid
|
||||
logger.info('Removing duplicated video streaming playlist %s.', videoUUID)
|
||||
|
||||
videoStreamingPlaylist.Video.removeStreamingPlaylistFiles(videoStreamingPlaylist, true)
|
||||
.catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err }))
|
||||
videoStreamingPlaylist.Video.removeAllStreamingPlaylistFiles({ playlist: videoStreamingPlaylist, isRedundancy: true })
|
||||
.catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err }))
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
@ -295,7 +294,7 @@ export class VideoRedundancyModel extends SequelizeModel<VideoRedundancyModel> {
|
|||
}
|
||||
|
||||
return VideoRedundancyModel.findOne(query)
|
||||
.then(r => !!r)
|
||||
.then(r => !!r)
|
||||
}
|
||||
|
||||
static async getVideoSample (p: Promise<VideoModel[]>) {
|
||||
|
@ -503,7 +502,7 @@ export class VideoRedundancyModel extends SequelizeModel<VideoRedundancyModel> {
|
|||
'(' +
|
||||
'SELECT "videoId" FROM "videoStreamingPlaylist" ' +
|
||||
'INNER JOIN "videoRedundancy" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id' +
|
||||
')'
|
||||
')'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -516,12 +515,12 @@ export class VideoRedundancyModel extends SequelizeModel<VideoRedundancyModel> {
|
|||
|
||||
const sql = `WITH "tmp" AS ` +
|
||||
`(` +
|
||||
`SELECT "videoStreamingFile"."size" AS "videoStreamingFileSize", "videoStreamingPlaylist"."videoId" AS "videoStreamingVideoId"` +
|
||||
`FROM "videoRedundancy" AS "videoRedundancy" ` +
|
||||
`LEFT JOIN "videoStreamingPlaylist" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ` +
|
||||
`LEFT JOIN "videoFile" AS "videoStreamingFile" ` +
|
||||
`ON "videoStreamingPlaylist"."id" = "videoStreamingFile"."videoStreamingPlaylistId" ` +
|
||||
`WHERE "videoRedundancy"."strategy" = :strategy AND "videoRedundancy"."actorId" = :actorId` +
|
||||
`SELECT "videoStreamingFile"."size" AS "videoStreamingFileSize", "videoStreamingPlaylist"."videoId" AS "videoStreamingVideoId"` +
|
||||
`FROM "videoRedundancy" AS "videoRedundancy" ` +
|
||||
`LEFT JOIN "videoStreamingPlaylist" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ` +
|
||||
`LEFT JOIN "videoFile" AS "videoStreamingFile" ` +
|
||||
`ON "videoStreamingPlaylist"."id" = "videoStreamingFile"."videoStreamingPlaylistId" ` +
|
||||
`WHERE "videoRedundancy"."strategy" = :strategy AND "videoRedundancy"."actorId" = :actorId` +
|
||||
`) ` +
|
||||
`SELECT ` +
|
||||
`COALESCE(SUM("videoStreamingFileSize"), '0') AS "totalUsed", ` +
|
||||
|
@ -604,7 +603,7 @@ export class VideoRedundancyModel extends SequelizeModel<VideoRedundancyModel> {
|
|||
`SELECT "videoStreamingPlaylist"."videoId" AS "videoId" FROM "videoRedundancy" ` +
|
||||
`INNER JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoRedundancy"."videoStreamingPlaylistId" ` +
|
||||
`WHERE "videoRedundancy"."actorId" = ${peertubeActor.id} ` +
|
||||
')'
|
||||
')'
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
import { uuidToShort } from '@peertube/peertube-node-utils'
|
||||
import { generateMagnetUri } from '@server/helpers/webtorrent.js'
|
||||
import { tracer } from '@server/lib/opentelemetry/tracing.js'
|
||||
import { getHlsResolutionPlaylistFilename } from '@server/lib/paths.js'
|
||||
import { getHLSResolutionPlaylistFilename } from '@server/lib/paths.js'
|
||||
import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls.js'
|
||||
import { VideoViewsManager } from '@server/lib/views/video-views-manager.js'
|
||||
import { isArray } from '../../../helpers/custom-validators/misc.js'
|
||||
|
@ -277,7 +277,7 @@ export function videoFilesModelToFormattedJSON (
|
|||
hasVideo: videoFile.hasVideo(),
|
||||
|
||||
playlistUrl: includePlaylistUrl === true
|
||||
? getHlsResolutionPlaylistFilename(fileUrl)
|
||||
? getHLSResolutionPlaylistFilename(fileUrl)
|
||||
: undefined,
|
||||
|
||||
storage: video.remote
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import { removeVTTExt } from '@peertube/peertube-core-utils'
|
||||
import { FileStorage, type FileStorageType, VideoCaption, VideoCaptionObject } from '@peertube/peertube-models'
|
||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import { getObjectStoragePublicFileUrl } from '@server/lib/object-storage/urls.js'
|
||||
import { removeCaptionObjectStorage } from '@server/lib/object-storage/videos.js'
|
||||
import { removeCaptionObjectStorage, removeHLSFileObjectStorageByFilename } from '@server/lib/object-storage/videos.js'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||
import {
|
||||
MVideo,
|
||||
MVideoCaption,
|
||||
MVideoCaptionFilename,
|
||||
MVideoCaptionFormattable,
|
||||
MVideoCaptionLanguageUrl,
|
||||
MVideoCaptionUrl,
|
||||
MVideoCaptionVideo,
|
||||
MVideoOwned
|
||||
MVideoOwned,
|
||||
MVideoPrivacy
|
||||
} from '@server/types/models/index.js'
|
||||
import { remove } from 'fs-extra/esm'
|
||||
import { join } from 'path'
|
||||
|
@ -22,7 +27,8 @@ import {
|
|||
DataType,
|
||||
Default,
|
||||
ForeignKey,
|
||||
Is, Scopes,
|
||||
Is,
|
||||
Scopes,
|
||||
Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
|
@ -31,13 +37,14 @@ import { logger } from '../../helpers/logger.js'
|
|||
import { CONFIG } from '../../initializers/config.js'
|
||||
import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants.js'
|
||||
import { SequelizeModel, buildWhereIdOrUUID, doesExist, throwIfNotValid } from '../shared/index.js'
|
||||
import { VideoStreamingPlaylistModel } from './video-streaming-playlist.js'
|
||||
import { VideoModel } from './video.js'
|
||||
|
||||
export enum ScopeNames {
|
||||
CAPTION_WITH_VIDEO = 'CAPTION_WITH_VIDEO'
|
||||
}
|
||||
|
||||
const videoAttributes = [ 'id', 'name', 'remote', 'uuid', 'url', 'state' ]
|
||||
const videoAttributes = [ 'id', 'name', 'remote', 'uuid', 'url', 'state', 'privacy' ]
|
||||
|
||||
@Scopes(() => ({
|
||||
[ScopeNames.CAPTION_WITH_VIDEO]: {
|
||||
|
@ -50,7 +57,6 @@ const videoAttributes = [ 'id', 'name', 'remote', 'uuid', 'url', 'state' ]
|
|||
]
|
||||
}
|
||||
}))
|
||||
|
||||
@Table({
|
||||
tableName: 'videoCaption',
|
||||
indexes: [
|
||||
|
@ -83,6 +89,10 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
|
|||
@Column
|
||||
filename: string
|
||||
|
||||
@AllowNull(true)
|
||||
@Column
|
||||
m3u8Filename: string
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(FileStorage.FILE_SYSTEM)
|
||||
@Column
|
||||
|
@ -92,6 +102,10 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
|
|||
@Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max))
|
||||
fileUrl: string
|
||||
|
||||
@AllowNull(true)
|
||||
@Column
|
||||
m3u8Url: string
|
||||
|
||||
@AllowNull(false)
|
||||
@Column
|
||||
automaticallyGenerated: boolean
|
||||
|
@ -117,11 +131,8 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
|
|||
if (instance.isOwned()) {
|
||||
logger.info('Removing caption %s.', instance.filename)
|
||||
|
||||
try {
|
||||
await instance.removeCaptionFile()
|
||||
} catch (err) {
|
||||
logger.error('Cannot remove caption file %s.', instance.filename)
|
||||
}
|
||||
instance.removeAllCaptionFiles()
|
||||
.catch(err => logger.error('Cannot remove caption file ' + instance.filename, { err }))
|
||||
}
|
||||
|
||||
return undefined
|
||||
|
@ -230,7 +241,7 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
|
|||
}
|
||||
|
||||
const captions = await VideoCaptionModel.scope(ScopeNames.CAPTION_WITH_VIDEO).findAll<MVideoCaptionVideo>(query)
|
||||
const result: { [ id: number ]: MVideoCaptionVideo[] } = {}
|
||||
const result: { [id: number]: MVideoCaptionVideo[] } = {}
|
||||
|
||||
for (const id of videoIds) {
|
||||
result[id] = []
|
||||
|
@ -253,6 +264,10 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
|
|||
return `${buildUUID()}-${language}.vtt`
|
||||
}
|
||||
|
||||
static generateM3U8Filename (vttFilename: string) {
|
||||
return removeVTTExt(vttFilename) + '.m3u8'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
toFormattedJSON (this: MVideoCaptionFormattable): VideoCaption {
|
||||
|
@ -265,9 +280,10 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
|
|||
|
||||
captionPath: this.Video.isOwned() && this.fileUrl
|
||||
? null // On object storage
|
||||
: this.getCaptionStaticPath(),
|
||||
: this.getFileStaticPath(),
|
||||
|
||||
fileUrl: this.getFileUrl(this.Video),
|
||||
m3u8Url: this.getM3U8Url(this.Video),
|
||||
|
||||
updatedAt: this.updatedAt.toISOString()
|
||||
}
|
||||
|
@ -278,7 +294,22 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
|
|||
identifier: this.language,
|
||||
name: VideoCaptionModel.getLanguageLabel(this.language),
|
||||
automaticallyGenerated: this.automaticallyGenerated,
|
||||
url: this.getOriginFileUrl(video)
|
||||
|
||||
// TODO: Remove break flag in v8
|
||||
url: process.env.ENABLE_AP_BREAKING_CHANGES === 'true'
|
||||
? [
|
||||
{
|
||||
type: 'Link',
|
||||
mediaType: 'text/vtt',
|
||||
href: this.getOriginFileUrl(video)
|
||||
},
|
||||
{
|
||||
type: 'Link',
|
||||
mediaType: 'application/x-mpegURL',
|
||||
href: this.getOriginFileUrl(video)
|
||||
}
|
||||
]
|
||||
: this.getOriginFileUrl(video)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -288,33 +319,77 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
|
|||
return this.Video.remote === false
|
||||
}
|
||||
|
||||
getCaptionStaticPath (this: MVideoCaptionLanguageUrl) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getFileStaticPath (this: MVideoCaptionFilename) {
|
||||
return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.filename)
|
||||
}
|
||||
|
||||
getFSPath () {
|
||||
return join(CONFIG.STORAGE.CAPTIONS_DIR, this.filename)
|
||||
}
|
||||
getM3U8StaticPath (this: MVideoCaptionFilename, video: MVideoPrivacy) {
|
||||
if (!this.m3u8Filename) return null
|
||||
|
||||
removeCaptionFile (this: MVideoCaption) {
|
||||
if (this.storage === FileStorage.OBJECT_STORAGE) {
|
||||
return removeCaptionObjectStorage(this)
|
||||
}
|
||||
|
||||
return remove(this.getFSPath())
|
||||
return VideoStreamingPlaylistModel.getPlaylistFileStaticPath(video, this.m3u8Filename)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getFileUrl (this: MVideoCaptionLanguageUrl, video: MVideoOwned) {
|
||||
getFSFilePath () {
|
||||
return join(CONFIG.STORAGE.CAPTIONS_DIR, this.filename)
|
||||
}
|
||||
|
||||
getFSM3U8Path (video: MVideoPrivacy) {
|
||||
if (!this.m3u8Filename) return null
|
||||
|
||||
return VideoPathManager.Instance.getFSHLSOutputPath(video, this.m3u8Filename)
|
||||
}
|
||||
|
||||
async removeAllCaptionFiles (this: MVideoCaptionVideo) {
|
||||
await this.removeCaptionFile()
|
||||
await this.removeCaptionPlaylist()
|
||||
}
|
||||
|
||||
async removeCaptionFile (this: MVideoCaptionVideo) {
|
||||
if (this.storage === FileStorage.OBJECT_STORAGE) {
|
||||
if (this.fileUrl) {
|
||||
await removeCaptionObjectStorage(this)
|
||||
}
|
||||
} else {
|
||||
await remove(this.getFSFilePath())
|
||||
}
|
||||
|
||||
this.filename = null
|
||||
this.fileUrl = null
|
||||
}
|
||||
|
||||
async removeCaptionPlaylist (this: MVideoCaptionVideo) {
|
||||
if (!this.m3u8Filename) return
|
||||
|
||||
const hls = await VideoStreamingPlaylistModel.loadHLSByVideoWithVideo(this.videoId)
|
||||
if (!hls) return
|
||||
|
||||
if (this.storage === FileStorage.OBJECT_STORAGE) {
|
||||
if (this.m3u8Url) {
|
||||
await removeHLSFileObjectStorageByFilename(hls, this.m3u8Filename)
|
||||
}
|
||||
} else {
|
||||
await remove(this.getFSM3U8Path(this.Video))
|
||||
}
|
||||
|
||||
this.m3u8Filename = null
|
||||
this.m3u8Url = null
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getFileUrl (this: MVideoCaptionUrl, video: MVideoOwned) {
|
||||
if (video.isOwned() && this.storage === FileStorage.OBJECT_STORAGE) {
|
||||
return getObjectStoragePublicFileUrl(this.fileUrl, CONFIG.OBJECT_STORAGE.CAPTIONS)
|
||||
}
|
||||
|
||||
return WEBSERVER.URL + this.getCaptionStaticPath()
|
||||
return WEBSERVER.URL + this.getFileStaticPath()
|
||||
}
|
||||
|
||||
getOriginFileUrl (this: MVideoCaptionLanguageUrl, video: MVideoOwned) {
|
||||
getOriginFileUrl (this: MVideoCaptionUrl, video: MVideoOwned) {
|
||||
if (video.isOwned()) return this.getFileUrl(video)
|
||||
|
||||
return this.fileUrl
|
||||
|
@ -322,6 +397,22 @@ export class VideoCaptionModel extends SequelizeModel<VideoCaptionModel> {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getM3U8Url (this: MVideoCaptionUrl, video: MVideoOwned & MVideoPrivacy) {
|
||||
if (!this.m3u8Filename) return null
|
||||
|
||||
if (video.isOwned()) {
|
||||
if (this.storage === FileStorage.OBJECT_STORAGE) {
|
||||
return getObjectStoragePublicFileUrl(this.m3u8Url, CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
|
||||
}
|
||||
|
||||
return WEBSERVER.URL + this.getM3U8StaticPath(video)
|
||||
}
|
||||
|
||||
return this.m3u8Url
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
isEqual (this: MVideoCaption, other: MVideoCaption) {
|
||||
if (this.fileUrl) return this.fileUrl === other.fileUrl
|
||||
|
||||
|
|
|
@ -12,22 +12,18 @@ import { getHLSPrivateFileUrl, getObjectStoragePublicFileUrl } from '@server/lib
|
|||
import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '@server/lib/paths.js'
|
||||
import { isVideoInPrivateDirectory } from '@server/lib/video-privacy.js'
|
||||
import { VideoFileModel } from '@server/models/video/video-file.js'
|
||||
import { MStreamingPlaylist, MStreamingPlaylistFiles, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models/index.js'
|
||||
import {
|
||||
MStreamingPlaylist,
|
||||
MStreamingPlaylistFiles,
|
||||
MStreamingPlaylistFilesVideo,
|
||||
MStreamingPlaylistVideo,
|
||||
MVideo,
|
||||
MVideoPrivacy
|
||||
} from '@server/types/models/index.js'
|
||||
import memoizee from 'memoizee'
|
||||
import { join } from 'path'
|
||||
import { Op, Transaction } from 'sequelize'
|
||||
import {
|
||||
AllowNull,
|
||||
BelongsTo,
|
||||
Column,
|
||||
CreatedAt,
|
||||
DataType,
|
||||
Default,
|
||||
ForeignKey,
|
||||
HasMany,
|
||||
Is, Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, HasMany, Is, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { isArrayOf } from '../../helpers/custom-validators/misc.js'
|
||||
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos.js'
|
||||
import {
|
||||
|
@ -205,7 +201,7 @@ export class VideoStreamingPlaylistModel extends SequelizeModel<VideoStreamingPl
|
|||
return VideoStreamingPlaylistModel.findByPk(id, options)
|
||||
}
|
||||
|
||||
static loadHLSPlaylistByVideo (videoId: number, transaction?: Transaction): Promise<MStreamingPlaylist> {
|
||||
static loadHLSByVideo (videoId: number, transaction?: Transaction): Promise<MStreamingPlaylist> {
|
||||
const options = {
|
||||
where: {
|
||||
type: VideoStreamingPlaylistType.HLS,
|
||||
|
@ -217,10 +213,31 @@ export class VideoStreamingPlaylistModel extends SequelizeModel<VideoStreamingPl
|
|||
return VideoStreamingPlaylistModel.findOne(options)
|
||||
}
|
||||
|
||||
static loadHLSByVideoWithVideo (videoId: number, transaction?: Transaction): Promise<MStreamingPlaylistVideo> {
|
||||
const options = {
|
||||
where: {
|
||||
type: VideoStreamingPlaylistType.HLS,
|
||||
videoId
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: VideoModel.unscoped(),
|
||||
required: true
|
||||
}
|
||||
],
|
||||
transaction
|
||||
}
|
||||
|
||||
return VideoStreamingPlaylistModel.findOne(options)
|
||||
}
|
||||
|
||||
static async loadOrGenerate (video: MVideo, transaction?: Transaction) {
|
||||
let playlist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id, transaction)
|
||||
let playlist = await VideoStreamingPlaylistModel.loadHLSByVideo(video.id, transaction)
|
||||
let generated = false
|
||||
|
||||
if (!playlist) {
|
||||
generated = true
|
||||
|
||||
playlist = new VideoStreamingPlaylistModel({
|
||||
p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
|
||||
type: VideoStreamingPlaylistType.HLS,
|
||||
|
@ -234,7 +251,7 @@ export class VideoStreamingPlaylistModel extends SequelizeModel<VideoStreamingPl
|
|||
await playlist.save({ transaction })
|
||||
}
|
||||
|
||||
return Object.assign(playlist, { Video: video })
|
||||
return { generated, playlist: Object.assign(playlist, { Video: video }) }
|
||||
}
|
||||
|
||||
static doesOwnedVideoUUIDExist (videoUUID: string, storage: FileStorageType) {
|
||||
|
@ -339,19 +356,21 @@ export class VideoStreamingPlaylistModel extends SequelizeModel<VideoStreamingPl
|
|||
return Object.assign(this, { Video: video })
|
||||
}
|
||||
|
||||
private getMasterPlaylistStaticPath (video: MVideo) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static getPlaylistFileStaticPath (video: MVideoPrivacy, filename: string) {
|
||||
if (isVideoInPrivateDirectory(video.privacy)) {
|
||||
return join(STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, video.uuid, this.playlistFilename)
|
||||
return join(STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, video.uuid, filename)
|
||||
}
|
||||
|
||||
return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.playlistFilename)
|
||||
return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, filename)
|
||||
}
|
||||
|
||||
private getSha256SegmentsStaticPath (video: MVideo) {
|
||||
if (isVideoInPrivateDirectory(video.privacy)) {
|
||||
return join(STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, video.uuid, this.segmentsSha256Filename)
|
||||
}
|
||||
private getMasterPlaylistStaticPath (video: MVideoPrivacy) {
|
||||
return VideoStreamingPlaylistModel.getPlaylistFileStaticPath(video, this.playlistFilename)
|
||||
}
|
||||
|
||||
return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.segmentsSha256Filename)
|
||||
private getSha256SegmentsStaticPath (video: MVideoPrivacy) {
|
||||
return VideoStreamingPlaylistModel.getPlaylistFileStaticPath(video, this.segmentsSha256Filename)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ import {
|
|||
removeWebVideoObjectStorage
|
||||
} from '@server/lib/object-storage/index.js'
|
||||
import { tracer } from '@server/lib/opentelemetry/tracing.js'
|
||||
import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths.js'
|
||||
import { getHLSDirectory, getHLSRedundancyDirectory, getHLSResolutionPlaylistFilename } from '@server/lib/paths.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||
import { isVideoInPrivateDirectory } from '@server/lib/video-privacy.js'
|
||||
|
@ -640,7 +640,6 @@ export class VideoModel extends SequelizeModel<VideoModel> {
|
|||
name: 'videoId',
|
||||
allowNull: true
|
||||
},
|
||||
hooks: true,
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
VideoFiles: Awaited<VideoFileModel>[]
|
||||
|
@ -650,7 +649,6 @@ export class VideoModel extends SequelizeModel<VideoModel> {
|
|||
name: 'videoId',
|
||||
allowNull: false
|
||||
},
|
||||
hooks: true,
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
VideoStreamingPlaylists: Awaited<VideoStreamingPlaylistModel>[]
|
||||
|
@ -834,7 +832,7 @@ export class VideoModel extends SequelizeModel<VideoModel> {
|
|||
static async removeFiles (instance: VideoModel, options) {
|
||||
const tasks: Promise<any>[] = []
|
||||
|
||||
logger.info('Removing files of video %s.', instance.url)
|
||||
logger.info('Removing files of video %s.', instance.url, { toto: new Error().stack })
|
||||
|
||||
if (instance.isOwned()) {
|
||||
if (!Array.isArray(instance.VideoFiles)) {
|
||||
|
@ -852,7 +850,8 @@ export class VideoModel extends SequelizeModel<VideoModel> {
|
|||
}
|
||||
|
||||
for (const p of instance.VideoStreamingPlaylists) {
|
||||
tasks.push(instance.removeStreamingPlaylistFiles(p))
|
||||
// Captions will be automatically deleted
|
||||
tasks.push(instance.removeAllStreamingPlaylistFiles({ playlist: p, deleteCaptionPlaylists: false }))
|
||||
}
|
||||
|
||||
// Remove source files
|
||||
|
@ -1904,7 +1903,7 @@ export class VideoModel extends SequelizeModel<VideoModel> {
|
|||
if (isArray(videoAP.VideoCaptions)) return videoAP.VideoCaptions
|
||||
|
||||
return this.$get('VideoCaptions', {
|
||||
attributes: [ 'filename', 'language', 'fileUrl', 'storage', 'automaticallyGenerated' ],
|
||||
attributes: [ 'filename', 'language', 'fileUrl', 'storage', 'automaticallyGenerated', 'm3u8Filename', 'm3u8Url' ],
|
||||
transaction
|
||||
}) as Promise<MVideoCaptionLanguageUrl[]>
|
||||
}
|
||||
|
@ -1993,47 +1992,76 @@ export class VideoModel extends SequelizeModel<VideoModel> {
|
|||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) {
|
||||
async removeAllStreamingPlaylistFiles (options: {
|
||||
playlist: MStreamingPlaylist
|
||||
deleteCaptionPlaylists?: boolean // default true
|
||||
isRedundancy?: boolean // default false
|
||||
}) {
|
||||
const { playlist, deleteCaptionPlaylists = true, isRedundancy = false } = options
|
||||
|
||||
const directoryPath = isRedundancy
|
||||
? getHLSRedundancyDirectory(this)
|
||||
: getHLSDirectory(this)
|
||||
|
||||
try {
|
||||
await remove(directoryPath)
|
||||
} catch (err) {
|
||||
// If it's a live, ffmpeg may have added another file while fs-extra is removing the directory
|
||||
// So wait a little bit and retry
|
||||
if (err.code === 'ENOTEMPTY') {
|
||||
await wait(1000)
|
||||
const removeDirectory = async () => {
|
||||
try {
|
||||
await remove(directoryPath)
|
||||
} catch (err) {
|
||||
// If it's a live, ffmpeg may have added another file while fs-extra is removing the directory
|
||||
// So wait a little bit and retry
|
||||
if (err.code === 'ENOTEMPTY') {
|
||||
await wait(1000)
|
||||
await remove(directoryPath)
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
if (isRedundancy !== true) {
|
||||
const streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo
|
||||
streamingPlaylistWithFiles.Video = this
|
||||
if (isRedundancy) {
|
||||
await removeDirectory()
|
||||
} else {
|
||||
if (deleteCaptionPlaylists) {
|
||||
const captions = await VideoCaptionModel.listVideoCaptions(playlist.videoId)
|
||||
|
||||
if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) {
|
||||
streamingPlaylistWithFiles.VideoFiles = await streamingPlaylistWithFiles.$get('VideoFiles')
|
||||
// Remove playlist files associated to captions
|
||||
for (const caption of captions) {
|
||||
try {
|
||||
await caption.removeCaptionPlaylist()
|
||||
await caption.save()
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Cannot remove caption ${caption.filename} (${caption.language}) playlist files associated to video ${this.name}`,
|
||||
{ video: this, ...lTags(this.uuid) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await removeDirectory()
|
||||
|
||||
const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo
|
||||
playlistWithFiles.Video = this
|
||||
|
||||
if (!Array.isArray(playlistWithFiles.VideoFiles)) {
|
||||
playlistWithFiles.VideoFiles = await playlistWithFiles.$get('VideoFiles')
|
||||
}
|
||||
|
||||
// Remove physical files and torrents
|
||||
await Promise.all(
|
||||
streamingPlaylistWithFiles.VideoFiles.map(file => file.removeTorrent())
|
||||
playlistWithFiles.VideoFiles.map(file => file.removeTorrent())
|
||||
)
|
||||
|
||||
if (streamingPlaylist.storage === FileStorage.OBJECT_STORAGE) {
|
||||
await removeHLSObjectStorage(streamingPlaylist.withVideo(this))
|
||||
if (playlist.storage === FileStorage.OBJECT_STORAGE) {
|
||||
await removeHLSObjectStorage(playlist.withVideo(this))
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Removing files associated to streaming playlist of video ${this.url}`,
|
||||
{ streamingPlaylist, isRedundancy, ...lTags(this.uuid) }
|
||||
{ playlist, isRedundancy, ...lTags(this.uuid) }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2042,7 +2070,7 @@ export class VideoModel extends SequelizeModel<VideoModel> {
|
|||
await videoFile.removeTorrent()
|
||||
await remove(filePath)
|
||||
|
||||
const resolutionFilename = getHlsResolutionPlaylistFilename(videoFile.filename)
|
||||
const resolutionFilename = getHLSResolutionPlaylistFilename(videoFile.filename)
|
||||
await remove(VideoPathManager.Instance.getFSHLSOutputPath(this, resolutionFilename))
|
||||
|
||||
if (videoFile.storage === FileStorage.OBJECT_STORAGE) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue