1
0
Fork 0

Clearer live session

Get the save replay setting when the session started to prevent
inconsistent behaviour when the setting changed before the session was
processed by the live ending job

Display more information about the potential session replay in live
modal information
This commit is contained in:
Chocobozzz 2022-07-22 15:22:21 +02:00
parent a77c5ff362
commit c8fa571f32
No known key found for this signature in database
GPG key ID: 583A612D890159BE
10 changed files with 134 additions and 36 deletions

View file

@ -42,6 +42,7 @@
<span i18n>Started on {{ session.startDate | date:'medium' }}</span> <span i18n>Started on {{ session.startDate | date:'medium' }}</span>
<span i18n *ngIf="session.endDate">Ended on {{ session.endDate | date:'medium' }}</span> <span i18n *ngIf="session.endDate">Ended on {{ session.endDate | date:'medium' }}</span>
<a i18n *ngIf="session.replayVideo" [routerLink]="getVideoUrl(session.replayVideo)" target="_blank">Go to replay</a> <a i18n *ngIf="session.replayVideo" [routerLink]="getVideoUrl(session.replayVideo)" target="_blank">Go to replay</a>
<span i18n *ngIf="isReplayBeingProcessed(session)">Replay is being processed...</span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -49,6 +49,13 @@ export class LiveStreamInformationComponent {
return errors[session.error] return errors[session.error]
} }
isReplayBeingProcessed (session: LiveVideoSession) {
// Running live
if (!session.endDate) return false
return session.saveReplay && !session.endingProcessed
}
private loadLiveInfo (video: Video) { private loadLiveInfo (video: Video) {
this.liveVideoService.getVideoLive(video.id) this.liveVideoService.getVideoLive(video.id)
.subscribe(live => this.live = live) .subscribe(live => this.live = live)

View file

@ -230,7 +230,7 @@ export class VideoActionsDropdownComponent implements OnChanges {
let message = $localize`Do you really want to delete ${this.video.name}?` let message = $localize`Do you really want to delete ${this.video.name}?`
if (this.video.isLive) { if (this.video.isLive) {
message += ' ' + $localize`The live stream will be automatically terminated.` message += ' ' + $localize`The live stream will be automatically terminated and replays won't be saved.`
} }
const res = await this.confirmService.confirm(message, $localize`Delete ${this.video.name}`) const res = await this.confirmService.confirm(message, $localize`Delete ${this.video.name}`)

View file

@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 715 const LAST_MIGRATION_VERSION = 720
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -0,0 +1,56 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
db: any
}): Promise<void> {
const { transaction } = utils
{
const data = {
type: Sequelize.BOOLEAN,
defaultValue: null,
allowNull: true
}
await utils.queryInterface.addColumn('videoLiveSession', 'endingProcessed', data, { transaction })
await utils.queryInterface.addColumn('videoLiveSession', 'saveReplay', data, { transaction })
}
{
const query = `UPDATE "videoLiveSession" SET "saveReplay" = (
SELECT "videoLive"."saveReplay" FROM "videoLive" WHERE "videoLive"."videoId" = "videoLiveSession"."liveVideoId"
) WHERE "videoLiveSession"."liveVideoId" IS NOT NULL`
await utils.sequelize.query(query, { transaction })
}
{
const query = `UPDATE "videoLiveSession" SET "saveReplay" = FALSE WHERE "saveReplay" IS NULL`
await utils.sequelize.query(query, { transaction })
}
{
const query = `UPDATE "videoLiveSession" SET "endingProcessed" = TRUE`
await utils.sequelize.query(query, { transaction })
}
{
const data = {
type: Sequelize.BOOLEAN,
defaultValue: null,
allowNull: false
}
await utils.queryInterface.changeColumn('videoLiveSession', 'endingProcessed', data, { transaction })
await utils.queryInterface.changeColumn('videoLiveSession', 'saveReplay', data, { transaction })
}
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View file

@ -30,26 +30,36 @@ async function processVideoLiveEnding (job: Job) {
logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId, lTags()) logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId, lTags())
} }
const liveVideo = await VideoModel.load(payload.videoId) const video = await VideoModel.load(payload.videoId)
const live = await VideoLiveModel.loadByVideoId(payload.videoId) const live = await VideoLiveModel.loadByVideoId(payload.videoId)
const liveSession = await VideoLiveSessionModel.load(payload.liveSessionId) const liveSession = await VideoLiveSessionModel.load(payload.liveSessionId)
if (!liveVideo || !live || !liveSession) { const permanentLive = live.permanentLive
if (!video || !live || !liveSession) {
logError() logError()
return return
} }
if (live.saveReplay !== true) { liveSession.endingProcessed = true
return cleanupLiveAndFederate({ live, video: liveVideo, streamingPlaylistId: payload.streamingPlaylistId }) await liveSession.save()
if (liveSession.saveReplay !== true) {
return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId })
} }
if (live.permanentLive) { if (permanentLive) {
await saveReplayToExternalVideo({ liveVideo, liveSession, publishedAt: payload.publishedAt, replayDirectory: payload.replayDirectory }) await saveReplayToExternalVideo({
liveVideo: video,
liveSession,
publishedAt: payload.publishedAt,
replayDirectory: payload.replayDirectory
})
return cleanupLiveAndFederate({ live, video: liveVideo, streamingPlaylistId: payload.streamingPlaylistId }) return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId })
} }
return replaceLiveByReplay({ liveVideo, live, liveSession, replayDirectory: payload.replayDirectory }) return replaceLiveByReplay({ video, liveSession, live, permanentLive, replayDirectory: payload.replayDirectory })
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -68,7 +78,7 @@ async function saveReplayToExternalVideo (options: {
}) { }) {
const { liveVideo, liveSession, publishedAt, replayDirectory } = options const { liveVideo, liveSession, publishedAt, replayDirectory } = options
const video = new VideoModel({ const replayVideo = new VideoModel({
name: `${liveVideo.name} - ${new Date(publishedAt).toLocaleString()}`, name: `${liveVideo.name} - ${new Date(publishedAt).toLocaleString()}`,
isLive: false, isLive: false,
state: VideoState.TO_TRANSCODE, state: VideoState.TO_TRANSCODE,
@ -88,63 +98,64 @@ async function saveReplayToExternalVideo (options: {
channelId: liveVideo.channelId channelId: liveVideo.channelId
}) as MVideoWithAllFiles }) as MVideoWithAllFiles
video.Thumbnails = [] replayVideo.Thumbnails = []
video.VideoFiles = [] replayVideo.VideoFiles = []
video.VideoStreamingPlaylists = [] replayVideo.VideoStreamingPlaylists = []
video.url = getLocalVideoActivityPubUrl(video) replayVideo.url = getLocalVideoActivityPubUrl(replayVideo)
await video.save() await replayVideo.save()
liveSession.replayVideoId = video.id liveSession.replayVideoId = replayVideo.id
await liveSession.save() await liveSession.save()
// If live is blacklisted, also blacklist the replay // If live is blacklisted, also blacklist the replay
const blacklist = await VideoBlacklistModel.loadByVideoId(liveVideo.id) const blacklist = await VideoBlacklistModel.loadByVideoId(liveVideo.id)
if (blacklist) { if (blacklist) {
await VideoBlacklistModel.create({ await VideoBlacklistModel.create({
videoId: video.id, videoId: replayVideo.id,
unfederated: blacklist.unfederated, unfederated: blacklist.unfederated,
reason: blacklist.reason, reason: blacklist.reason,
type: blacklist.type type: blacklist.type
}) })
} }
await assignReplayFilesToVideo({ video, replayDirectory }) await assignReplayFilesToVideo({ video: replayVideo, replayDirectory })
await remove(replayDirectory) await remove(replayDirectory)
for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) { for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) {
const image = await generateVideoMiniature({ video, videoFile: video.getMaxQualityFile(), type }) const image = await generateVideoMiniature({ video: replayVideo, videoFile: replayVideo.getMaxQualityFile(), type })
await video.addAndSaveThumbnail(image) await replayVideo.addAndSaveThumbnail(image)
} }
await moveToNextState({ video, isNewVideo: true }) await moveToNextState({ video: replayVideo, isNewVideo: true })
} }
async function replaceLiveByReplay (options: { async function replaceLiveByReplay (options: {
liveVideo: MVideo video: MVideo
liveSession: MVideoLiveSession liveSession: MVideoLiveSession
live: MVideoLive live: MVideoLive
permanentLive: boolean
replayDirectory: string replayDirectory: string
}) { }) {
const { liveVideo, liveSession, live, replayDirectory } = options const { video, liveSession, live, permanentLive, replayDirectory } = options
await cleanupTMPLiveFiles(liveVideo) await cleanupTMPLiveFiles(video)
await live.destroy() await live.destroy()
liveVideo.isLive = false video.isLive = false
liveVideo.waitTranscoding = true video.waitTranscoding = true
liveVideo.state = VideoState.TO_TRANSCODE video.state = VideoState.TO_TRANSCODE
await liveVideo.save() await video.save()
liveSession.replayVideoId = liveVideo.id liveSession.replayVideoId = video.id
await liveSession.save() await liveSession.save()
// Remove old HLS playlist video files // Remove old HLS playlist video files
const videoWithFiles = await VideoModel.loadFull(liveVideo.id) const videoWithFiles = await VideoModel.loadFull(video.id)
const hlsPlaylist = videoWithFiles.getHLSPlaylist() const hlsPlaylist = videoWithFiles.getHLSPlaylist()
await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id)
@ -157,7 +168,7 @@ async function replaceLiveByReplay (options: {
await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory }) await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory })
if (live.permanentLive) { // Remove session replay if (permanentLive) { // Remove session replay
await remove(replayDirectory) await remove(replayDirectory)
} else { // We won't stream again in this live, we can delete the base replay directory } else { // We won't stream again in this live, we can delete the base replay directory
await remove(getLiveReplayBaseDirectory(videoWithFiles)) await remove(getLiveReplayBaseDirectory(videoWithFiles))
@ -224,16 +235,16 @@ async function assignReplayFilesToVideo (options: {
} }
async function cleanupLiveAndFederate (options: { async function cleanupLiveAndFederate (options: {
live: MVideoLive
video: MVideo video: MVideo
permanentLive: boolean
streamingPlaylistId: number streamingPlaylistId: number
}) { }) {
const { live, video, streamingPlaylistId } = options const { permanentLive, video, streamingPlaylistId } = options
const streamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(streamingPlaylistId) const streamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(streamingPlaylistId)
if (streamingPlaylist) { if (streamingPlaylist) {
if (live.permanentLive) { if (permanentLive) {
await cleanupPermanentLive(video, streamingPlaylist) await cleanupPermanentLive(video, streamingPlaylist)
} else { } else {
await cleanupUnsavedNormalLive(video, streamingPlaylist) await cleanupUnsavedNormalLive(video, streamingPlaylist)

View file

@ -475,7 +475,9 @@ class LiveManager {
private saveStartingSession (videoLive: MVideoLiveVideo) { private saveStartingSession (videoLive: MVideoLiveVideo) {
const liveSession = new VideoLiveSessionModel({ const liveSession = new VideoLiveSessionModel({
startDate: new Date(), startDate: new Date(),
liveVideoId: videoLive.videoId liveVideoId: videoLive.videoId,
saveReplay: videoLive.saveReplay,
endingProcessed: false
}) })
return liveSession.save() return liveSession.save()

View file

@ -53,6 +53,14 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv
@Column @Column
error: LiveVideoError error: LiveVideoError
@AllowNull(false)
@Column
saveReplay: boolean
@AllowNull(false)
@Column
endingProcessed: boolean
@ForeignKey(() => VideoModel) @ForeignKey(() => VideoModel)
@Column @Column
replayVideoId: number replayVideoId: number
@ -144,6 +152,8 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv
endDate: this.endDate endDate: this.endDate
? this.endDate.toISOString() ? this.endDate.toISOString()
: null, : null,
endingProcessed: this.endingProcessed,
saveReplay: this.saveReplay,
replayVideo, replayVideo,
error: this.error error: this.error
} }

View file

@ -206,6 +206,7 @@ describe('Save replay setting', function () {
expect(session.endDate).to.exist expect(session.endDate).to.exist
expect(new Date(session.endDate)).to.be.above(sessionEndDateMin) expect(new Date(session.endDate)).to.be.above(sessionEndDateMin)
expect(session.saveReplay).to.be.false
expect(session.error).to.not.exist expect(session.error).to.not.exist
expect(session.replayVideo).to.not.exist expect(session.replayVideo).to.not.exist
}) })
@ -272,6 +273,11 @@ describe('Save replay setting', function () {
it('Should correctly have saved the live and federated it after the streaming', async function () { it('Should correctly have saved the live and federated it after the streaming', async function () {
this.timeout(30000) this.timeout(30000)
const session = await servers[0].live.findLatestSession({ videoId: liveVideoUUID })
expect(session.endDate).to.not.exist
expect(session.endingProcessed).to.be.false
expect(session.saveReplay).to.be.true
await stopFfmpeg(ffmpegCommand) await stopFfmpeg(ffmpegCommand)
await waitUntilLiveReplacedByReplayOnAllServers(servers, liveVideoUUID) await waitUntilLiveReplacedByReplayOnAllServers(servers, liveVideoUUID)
@ -291,6 +297,8 @@ describe('Save replay setting', function () {
expect(session.endDate).to.exist expect(session.endDate).to.exist
expect(session.error).to.not.exist expect(session.error).to.not.exist
expect(session.saveReplay).to.be.true
expect(session.endingProcessed).to.be.true
expect(session.replayVideo).to.exist expect(session.replayVideo).to.exist
expect(session.replayVideo.id).to.exist expect(session.replayVideo.id).to.exist

View file

@ -8,6 +8,9 @@ export interface LiveVideoSession {
error: LiveVideoError error: LiveVideoError
saveReplay: boolean
endingProcessed: boolean
replayVideo: { replayVideo: {
id: number id: number
uuid: string uuid: string