diff options
author | Chocobozzz <me@florianbigard.com> | 2022-05-25 15:22:28 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-05-25 15:22:28 +0200 |
commit | ab623c0e0b4815bd69a94887241a69aaa857ed26 (patch) | |
tree | 9140675d0aa501d9ddbbcf14bbdb5c28b6ef0712 | |
parent | c501cdef27e6769b7d2a13ac7b6deeb2e61eee46 (diff) | |
parent | c8fdfab0e36cc7324c61710009bf334e836485d9 (diff) | |
download | PeerTube-ab623c0e0b4815bd69a94887241a69aaa857ed26.tar.gz PeerTube-ab623c0e0b4815bd69a94887241a69aaa857ed26.tar.zst PeerTube-ab623c0e0b4815bd69a94887241a69aaa857ed26.zip |
Merge branch 'release/4.2.0' into develop
-rw-r--r-- | client/src/app/+admin/system/jobs/jobs.component.html | 2 | ||||
-rw-r--r-- | client/src/app/shared/shared-actor-image-edit/actor-image-edit.scss | 8 | ||||
-rw-r--r-- | client/src/app/shared/shared-main/account/actor.model.ts | 6 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-live-ending.ts | 53 | ||||
-rw-r--r-- | server/lib/live/live-manager.ts | 6 | ||||
-rw-r--r-- | server/lib/live/live-utils.ts | 40 | ||||
-rw-r--r-- | server/lib/live/shared/muxing-session.ts | 41 | ||||
-rw-r--r-- | server/models/actor/actor-image.ts | 3 | ||||
-rw-r--r-- | server/models/user/sql/user-notitication-list-query-builder.ts | 5 | ||||
-rw-r--r-- | server/tests/api/live/live-save-replay.ts | 34 | ||||
-rw-r--r-- | server/tests/api/live/live-socket-messages.ts | 2 | ||||
-rw-r--r-- | server/tests/shared/live.ts | 24 | ||||
-rw-r--r-- | server/tests/shared/notifications.ts | 20 |
13 files changed, 173 insertions, 71 deletions
diff --git a/client/src/app/+admin/system/jobs/jobs.component.html b/client/src/app/+admin/system/jobs/jobs.component.html index 638d2380a..301591786 100644 --- a/client/src/app/+admin/system/jobs/jobs.component.html +++ b/client/src/app/+admin/system/jobs/jobs.component.html | |||
@@ -65,7 +65,7 @@ | |||
65 | <span class="badge" [ngClass]="getJobStateClass(job.state)">{{ job.state }}</span> | 65 | <span class="badge" [ngClass]="getJobStateClass(job.state)">{{ job.state }}</span> |
66 | </td> | 66 | </td> |
67 | 67 | ||
68 | <td class="job-progress c-hand" [pRowToggler]="job"> | 68 | <td *ngIf="hasGlobalProgress()" class="job-progress c-hand" [pRowToggler]="job"> |
69 | <ng-container *ngIf="hasProgress(job)">{{ getProgress(job) }}</ng-container> | 69 | <ng-container *ngIf="hasProgress(job)">{{ getProgress(job) }}</ng-container> |
70 | </td> | 70 | </td> |
71 | 71 | ||
diff --git a/client/src/app/shared/shared-actor-image-edit/actor-image-edit.scss b/client/src/app/shared/shared-actor-image-edit/actor-image-edit.scss index c4fe5a59e..93bdaba57 100644 --- a/client/src/app/shared/shared-actor-image-edit/actor-image-edit.scss +++ b/client/src/app/shared/shared-actor-image-edit/actor-image-edit.scss | |||
@@ -16,10 +16,12 @@ | |||
16 | } | 16 | } |
17 | 17 | ||
18 | .actor-img-edit-button { | 18 | .actor-img-edit-button { |
19 | @include peertube-button-file(21px); | 19 | @include peertube-button-file(30px); |
20 | @include button-with-icon(19px); | ||
21 | @include orange-button; | 20 | @include orange-button; |
22 | 21 | ||
22 | display: flex; | ||
23 | justify-content: center; | ||
24 | padding: 0; | ||
23 | margin-top: 10px; | 25 | margin-top: 10px; |
24 | margin-bottom: 5px; | 26 | margin-bottom: 5px; |
25 | cursor: pointer; | 27 | cursor: pointer; |
@@ -30,6 +32,6 @@ | |||
30 | } | 32 | } |
31 | 33 | ||
32 | my-global-icon { | 34 | my-global-icon { |
33 | right: 7px; | 35 | width: 19px; |
34 | } | 36 | } |
35 | } | 37 | } |
diff --git a/client/src/app/shared/shared-main/account/actor.model.ts b/client/src/app/shared/shared-main/account/actor.model.ts index 6be6b75d0..6e45ba588 100644 --- a/client/src/app/shared/shared-main/account/actor.model.ts +++ b/client/src/app/shared/shared-main/account/actor.model.ts | |||
@@ -21,11 +21,11 @@ export abstract class Actor implements ServerActor { | |||
21 | isLocal: boolean | 21 | isLocal: boolean |
22 | 22 | ||
23 | static GET_ACTOR_AVATAR_URL (actor: { avatars: { width: number, url?: string, path: string }[] }, size?: number) { | 23 | static GET_ACTOR_AVATAR_URL (actor: { avatars: { width: number, url?: string, path: string }[] }, size?: number) { |
24 | const avatars = actor.avatars.sort((a, b) => b.width - a.width) | 24 | const avatarsAscWidth = actor.avatars.sort((a, b) => a.width - b.width) |
25 | 25 | ||
26 | const avatar = size | 26 | const avatar = size |
27 | ? avatars.find(a => a.width >= size) | 27 | ? avatarsAscWidth.find(a => a.width >= size) |
28 | : avatars[0] | 28 | : avatarsAscWidth[avatarsAscWidth.length - 1] // Bigger one |
29 | 29 | ||
30 | if (!avatar) return '' | 30 | if (!avatar) return '' |
31 | if (avatar.url) return avatar.url | 31 | if (avatar.url) return avatar.url |
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 55fd09344..7607267f8 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | import { Job } from 'bull' | 1 | import { Job } from 'bull' |
2 | import { pathExists, readdir, remove } from 'fs-extra' | 2 | import { readdir, remove } from 'fs-extra' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamDuration } from '@server/helpers/ffmpeg' | 4 | import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamDuration } from '@server/helpers/ffmpeg' |
5 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | 5 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' |
6 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | 6 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' |
7 | import { cleanupLive, LiveSegmentShaStore } from '@server/lib/live' | 7 | import { cleanupNormalLive, cleanupPermanentLive, cleanupTMPLiveFiles, LiveSegmentShaStore } from '@server/lib/live' |
8 | import { | 8 | import { |
9 | generateHLSMasterPlaylistFilename, | 9 | generateHLSMasterPlaylistFilename, |
10 | generateHlsSha256SegmentsFilename, | 10 | generateHlsSha256SegmentsFilename, |
@@ -45,13 +45,13 @@ async function processVideoLiveEnding (job: Job) { | |||
45 | LiveSegmentShaStore.Instance.cleanupShaSegments(liveVideo.uuid) | 45 | LiveSegmentShaStore.Instance.cleanupShaSegments(liveVideo.uuid) |
46 | 46 | ||
47 | if (live.saveReplay !== true) { | 47 | if (live.saveReplay !== true) { |
48 | return cleanupLiveAndFederate({ liveVideo }) | 48 | return cleanupLiveAndFederate({ live, video: liveVideo }) |
49 | } | 49 | } |
50 | 50 | ||
51 | if (live.permanentLive) { | 51 | if (live.permanentLive) { |
52 | await saveReplayToExternalVideo({ liveVideo, liveSession, publishedAt: payload.publishedAt, replayDirectory: payload.replayDirectory }) | 52 | await saveReplayToExternalVideo({ liveVideo, liveSession, publishedAt: payload.publishedAt, replayDirectory: payload.replayDirectory }) |
53 | 53 | ||
54 | return cleanupLiveAndFederate({ liveVideo }) | 54 | return cleanupLiveAndFederate({ live, video: liveVideo }) |
55 | } | 55 | } |
56 | 56 | ||
57 | return replaceLiveByReplay({ liveVideo, live, liveSession, replayDirectory: payload.replayDirectory }) | 57 | return replaceLiveByReplay({ liveVideo, live, liveSession, replayDirectory: payload.replayDirectory }) |
@@ -164,7 +164,11 @@ async function replaceLiveByReplay (options: { | |||
164 | 164 | ||
165 | await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory }) | 165 | await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory }) |
166 | 166 | ||
167 | await remove(getLiveReplayBaseDirectory(videoWithFiles)) | 167 | if (live.permanentLive) { // Remove session replay |
168 | await remove(replayDirectory) | ||
169 | } else { // We won't stream again in this live, we can delete the base replay directory | ||
170 | await remove(getLiveReplayBaseDirectory(videoWithFiles)) | ||
171 | } | ||
168 | 172 | ||
169 | // Regenerate the thumbnail & preview? | 173 | // Regenerate the thumbnail & preview? |
170 | if (videoWithFiles.getMiniature().automaticallyGenerated === true) { | 174 | if (videoWithFiles.getMiniature().automaticallyGenerated === true) { |
@@ -227,34 +231,23 @@ async function assignReplayFilesToVideo (options: { | |||
227 | } | 231 | } |
228 | 232 | ||
229 | async function cleanupLiveAndFederate (options: { | 233 | async function cleanupLiveAndFederate (options: { |
230 | liveVideo: MVideo | 234 | live: MVideoLive |
235 | video: MVideo | ||
231 | }) { | 236 | }) { |
232 | const { liveVideo } = options | 237 | const { live, video } = options |
233 | |||
234 | const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(liveVideo.id) | ||
235 | await cleanupLive(liveVideo, streamingPlaylist) | ||
236 | 238 | ||
237 | const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(liveVideo.id) | 239 | const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) |
238 | return federateVideoIfNeeded(fullVideo, false, undefined) | ||
239 | } | ||
240 | 240 | ||
241 | async function cleanupTMPLiveFiles (hlsDirectory: string) { | 241 | if (live.permanentLive) { |
242 | if (!await pathExists(hlsDirectory)) return | 242 | await cleanupPermanentLive(video, streamingPlaylist) |
243 | 243 | } else { | |
244 | const files = await readdir(hlsDirectory) | 244 | await cleanupNormalLive(video, streamingPlaylist) |
245 | 245 | } | |
246 | for (const filename of files) { | ||
247 | if ( | ||
248 | filename.endsWith('.ts') || | ||
249 | filename.endsWith('.m3u8') || | ||
250 | filename.endsWith('.mpd') || | ||
251 | filename.endsWith('.m4s') || | ||
252 | filename.endsWith('.tmp') | ||
253 | ) { | ||
254 | const p = join(hlsDirectory, filename) | ||
255 | 246 | ||
256 | remove(p) | 247 | try { |
257 | .catch(err => logger.error('Cannot remove %s.', p, { err })) | 248 | const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id) |
258 | } | 249 | return federateVideoIfNeeded(fullVideo, false, undefined) |
250 | } catch (err) { | ||
251 | logger.warn('Cannot federate live after cleanup', { videoId: video.id, err }) | ||
259 | } | 252 | } |
260 | } | 253 | } |
diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts index e04ae9fef..0f14a6851 100644 --- a/server/lib/live/live-manager.ts +++ b/server/lib/live/live-manager.ts | |||
@@ -28,7 +28,7 @@ import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, g | |||
28 | import { PeerTubeSocket } from '../peertube-socket' | 28 | import { PeerTubeSocket } from '../peertube-socket' |
29 | import { LiveQuotaStore } from './live-quota-store' | 29 | import { LiveQuotaStore } from './live-quota-store' |
30 | import { LiveSegmentShaStore } from './live-segment-sha-store' | 30 | import { LiveSegmentShaStore } from './live-segment-sha-store' |
31 | import { cleanupLive } from './live-utils' | 31 | import { cleanupPermanentLive } from './live-utils' |
32 | import { MuxingSession } from './shared' | 32 | import { MuxingSession } from './shared' |
33 | 33 | ||
34 | const NodeRtmpSession = require('node-media-server/src/node_rtmp_session') | 34 | const NodeRtmpSession = require('node-media-server/src/node_rtmp_session') |
@@ -224,7 +224,9 @@ class LiveManager { | |||
224 | 224 | ||
225 | const oldStreamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) | 225 | const oldStreamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) |
226 | if (oldStreamingPlaylist) { | 226 | if (oldStreamingPlaylist) { |
227 | await cleanupLive(video, oldStreamingPlaylist) | 227 | if (!videoLive.permanentLive) throw new Error('Found previous session in a non permanent live: ' + video.uuid) |
228 | |||
229 | await cleanupPermanentLive(video, oldStreamingPlaylist) | ||
228 | } | 230 | } |
229 | 231 | ||
230 | this.videoSessions.set(video.id, sessionId) | 232 | this.videoSessions.set(video.id, sessionId) |
diff --git a/server/lib/live/live-utils.ts b/server/lib/live/live-utils.ts index 46c7fd2f8..6365e23db 100644 --- a/server/lib/live/live-utils.ts +++ b/server/lib/live/live-utils.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { remove } from 'fs-extra' | 1 | import { pathExists, readdir, remove } from 'fs-extra' |
2 | import { basename } from 'path' | 2 | import { basename, join } from 'path' |
3 | import { logger } from '@server/helpers/logger' | ||
3 | import { MStreamingPlaylist, MVideo } from '@server/types/models' | 4 | import { MStreamingPlaylist, MVideo } from '@server/types/models' |
4 | import { getLiveDirectory } from '../paths' | 5 | import { getLiveDirectory } from '../paths' |
5 | 6 | ||
@@ -9,7 +10,15 @@ function buildConcatenatedName (segmentOrPlaylistPath: string) { | |||
9 | return 'concat-' + num[1] + '.ts' | 10 | return 'concat-' + num[1] + '.ts' |
10 | } | 11 | } |
11 | 12 | ||
12 | async function cleanupLive (video: MVideo, streamingPlaylist?: MStreamingPlaylist) { | 13 | async function cleanupPermanentLive (video: MVideo, streamingPlaylist?: MStreamingPlaylist) { |
14 | const hlsDirectory = getLiveDirectory(video) | ||
15 | |||
16 | await cleanupTMPLiveFiles(hlsDirectory) | ||
17 | |||
18 | if (streamingPlaylist) await streamingPlaylist.destroy() | ||
19 | } | ||
20 | |||
21 | async function cleanupNormalLive (video: MVideo, streamingPlaylist?: MStreamingPlaylist) { | ||
13 | const hlsDirectory = getLiveDirectory(video) | 22 | const hlsDirectory = getLiveDirectory(video) |
14 | 23 | ||
15 | await remove(hlsDirectory) | 24 | await remove(hlsDirectory) |
@@ -17,7 +26,30 @@ async function cleanupLive (video: MVideo, streamingPlaylist?: MStreamingPlaylis | |||
17 | if (streamingPlaylist) await streamingPlaylist.destroy() | 26 | if (streamingPlaylist) await streamingPlaylist.destroy() |
18 | } | 27 | } |
19 | 28 | ||
29 | async function cleanupTMPLiveFiles (hlsDirectory: string) { | ||
30 | if (!await pathExists(hlsDirectory)) return | ||
31 | |||
32 | const files = await readdir(hlsDirectory) | ||
33 | |||
34 | for (const filename of files) { | ||
35 | if ( | ||
36 | filename.endsWith('.ts') || | ||
37 | filename.endsWith('.m3u8') || | ||
38 | filename.endsWith('.mpd') || | ||
39 | filename.endsWith('.m4s') || | ||
40 | filename.endsWith('.tmp') | ||
41 | ) { | ||
42 | const p = join(hlsDirectory, filename) | ||
43 | |||
44 | remove(p) | ||
45 | .catch(err => logger.error('Cannot remove %s.', p, { err })) | ||
46 | } | ||
47 | } | ||
48 | } | ||
49 | |||
20 | export { | 50 | export { |
21 | cleanupLive, | 51 | cleanupPermanentLive, |
52 | cleanupNormalLive, | ||
53 | cleanupTMPLiveFiles, | ||
22 | buildConcatenatedName | 54 | buildConcatenatedName |
23 | } | 55 | } |
diff --git a/server/lib/live/shared/muxing-session.ts b/server/lib/live/shared/muxing-session.ts index 1ee9b430f..98a7b2613 100644 --- a/server/lib/live/shared/muxing-session.ts +++ b/server/lib/live/shared/muxing-session.ts | |||
@@ -150,8 +150,8 @@ class MuxingSession extends EventEmitter { | |||
150 | 150 | ||
151 | logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags()) | 151 | logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags()) |
152 | 152 | ||
153 | this.watchTSFiles(this.outDirectory) | 153 | this.watchTSFiles() |
154 | this.watchMasterFile(this.outDirectory) | 154 | this.watchMasterFile() |
155 | 155 | ||
156 | let ffmpegShellCommand: string | 156 | let ffmpegShellCommand: string |
157 | this.ffmpegCommand.on('start', cmdline => { | 157 | this.ffmpegCommand.on('start', cmdline => { |
@@ -161,13 +161,13 @@ class MuxingSession extends EventEmitter { | |||
161 | }) | 161 | }) |
162 | 162 | ||
163 | this.ffmpegCommand.on('error', (err, stdout, stderr) => { | 163 | this.ffmpegCommand.on('error', (err, stdout, stderr) => { |
164 | this.onFFmpegError({ err, stdout, stderr, outPath: this.outDirectory, ffmpegShellCommand }) | 164 | this.onFFmpegError({ err, stdout, stderr, ffmpegShellCommand }) |
165 | }) | 165 | }) |
166 | 166 | ||
167 | this.ffmpegCommand.on('end', () => { | 167 | this.ffmpegCommand.on('end', () => { |
168 | this.emit('ffmpeg-end', ({ videoId: this.videoId })) | 168 | this.emit('ffmpeg-end', ({ videoId: this.videoId })) |
169 | 169 | ||
170 | this.onFFmpegEnded(this.outDirectory) | 170 | this.onFFmpegEnded() |
171 | }) | 171 | }) |
172 | 172 | ||
173 | this.ffmpegCommand.run() | 173 | this.ffmpegCommand.run() |
@@ -189,12 +189,11 @@ class MuxingSession extends EventEmitter { | |||
189 | err: any | 189 | err: any |
190 | stdout: string | 190 | stdout: string |
191 | stderr: string | 191 | stderr: string |
192 | outPath: string | ||
193 | ffmpegShellCommand: string | 192 | ffmpegShellCommand: string |
194 | }) { | 193 | }) { |
195 | const { err, stdout, stderr, outPath, ffmpegShellCommand } = options | 194 | const { err, stdout, stderr, ffmpegShellCommand } = options |
196 | 195 | ||
197 | this.onFFmpegEnded(outPath) | 196 | this.onFFmpegEnded() |
198 | 197 | ||
199 | // Don't care that we killed the ffmpeg process | 198 | // Don't care that we killed the ffmpeg process |
200 | if (err?.message?.includes('Exiting normally')) return | 199 | if (err?.message?.includes('Exiting normally')) return |
@@ -204,7 +203,7 @@ class MuxingSession extends EventEmitter { | |||
204 | this.emit('ffmpeg-error', ({ videoId: this.videoId })) | 203 | this.emit('ffmpeg-error', ({ videoId: this.videoId })) |
205 | } | 204 | } |
206 | 205 | ||
207 | private onFFmpegEnded (outPath: string) { | 206 | private onFFmpegEnded () { |
208 | logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', this.inputUrl, this.lTags()) | 207 | logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', this.inputUrl, this.lTags()) |
209 | 208 | ||
210 | setTimeout(() => { | 209 | setTimeout(() => { |
@@ -214,12 +213,12 @@ class MuxingSession extends EventEmitter { | |||
214 | .then(() => { | 213 | .then(() => { |
215 | // Process remaining segments hash | 214 | // Process remaining segments hash |
216 | for (const key of Object.keys(this.segmentsToProcessPerPlaylist)) { | 215 | for (const key of Object.keys(this.segmentsToProcessPerPlaylist)) { |
217 | this.processSegments(outPath, this.segmentsToProcessPerPlaylist[key]) | 216 | this.processSegments(this.segmentsToProcessPerPlaylist[key]) |
218 | } | 217 | } |
219 | }) | 218 | }) |
220 | .catch(err => { | 219 | .catch(err => { |
221 | logger.error( | 220 | logger.error( |
222 | 'Cannot close watchers of %s or process remaining hash segments.', outPath, | 221 | 'Cannot close watchers of %s or process remaining hash segments.', this.outDirectory, |
223 | { err, ...this.lTags() } | 222 | { err, ...this.lTags() } |
224 | ) | 223 | ) |
225 | }) | 224 | }) |
@@ -228,21 +227,21 @@ class MuxingSession extends EventEmitter { | |||
228 | }, 1000) | 227 | }, 1000) |
229 | } | 228 | } |
230 | 229 | ||
231 | private watchMasterFile (outPath: string) { | 230 | private watchMasterFile () { |
232 | this.masterWatcher = watch(outPath + '/' + this.streamingPlaylist.playlistFilename) | 231 | this.masterWatcher = watch(this.outDirectory + '/' + this.streamingPlaylist.playlistFilename) |
233 | 232 | ||
234 | this.masterWatcher.on('add', () => { | 233 | this.masterWatcher.on('add', () => { |
235 | this.emit('master-playlist-created', { videoId: this.videoId }) | 234 | this.emit('master-playlist-created', { videoId: this.videoId }) |
236 | 235 | ||
237 | this.masterWatcher.close() | 236 | this.masterWatcher.close() |
238 | .catch(err => logger.error('Cannot close master watcher of %s.', outPath, { err, ...this.lTags() })) | 237 | .catch(err => logger.error('Cannot close master watcher of %s.', this.outDirectory, { err, ...this.lTags() })) |
239 | }) | 238 | }) |
240 | } | 239 | } |
241 | 240 | ||
242 | private watchTSFiles (outPath: string) { | 241 | private watchTSFiles () { |
243 | const startStreamDateTime = new Date().getTime() | 242 | const startStreamDateTime = new Date().getTime() |
244 | 243 | ||
245 | this.tsWatcher = watch(outPath + '/*.ts') | 244 | this.tsWatcher = watch(this.outDirectory + '/*.ts') |
246 | 245 | ||
247 | const playlistIdMatcher = /^([\d+])-/ | 246 | const playlistIdMatcher = /^([\d+])-/ |
248 | 247 | ||
@@ -252,7 +251,7 @@ class MuxingSession extends EventEmitter { | |||
252 | const playlistId = basename(segmentPath).match(playlistIdMatcher)[0] | 251 | const playlistId = basename(segmentPath).match(playlistIdMatcher)[0] |
253 | 252 | ||
254 | const segmentsToProcess = this.segmentsToProcessPerPlaylist[playlistId] || [] | 253 | const segmentsToProcess = this.segmentsToProcessPerPlaylist[playlistId] || [] |
255 | this.processSegments(outPath, segmentsToProcess) | 254 | this.processSegments(segmentsToProcess) |
256 | 255 | ||
257 | this.segmentsToProcessPerPlaylist[playlistId] = [ segmentPath ] | 256 | this.segmentsToProcessPerPlaylist[playlistId] = [ segmentPath ] |
258 | 257 | ||
@@ -273,7 +272,7 @@ class MuxingSession extends EventEmitter { | |||
273 | } | 272 | } |
274 | } | 273 | } |
275 | 274 | ||
276 | const deleteHandler = segmentPath => LiveSegmentShaStore.Instance.removeSegmentSha(this.videoUUID, segmentPath) | 275 | const deleteHandler = (segmentPath: string) => LiveSegmentShaStore.Instance.removeSegmentSha(this.videoUUID, segmentPath) |
277 | 276 | ||
278 | this.tsWatcher.on('add', p => addHandler(p)) | 277 | this.tsWatcher.on('add', p => addHandler(p)) |
279 | this.tsWatcher.on('unlink', p => deleteHandler(p)) | 278 | this.tsWatcher.on('unlink', p => deleteHandler(p)) |
@@ -332,15 +331,15 @@ class MuxingSession extends EventEmitter { | |||
332 | return now <= max | 331 | return now <= max |
333 | } | 332 | } |
334 | 333 | ||
335 | private processSegments (hlsVideoPath: string, segmentPaths: string[]) { | 334 | private processSegments (segmentPaths: string[]) { |
336 | mapSeries(segmentPaths, async previousSegment => { | 335 | mapSeries(segmentPaths, async previousSegment => { |
337 | // Add sha hash of previous segments, because ffmpeg should have finished generating them | 336 | // Add sha hash of previous segments, because ffmpeg should have finished generating them |
338 | await LiveSegmentShaStore.Instance.addSegmentSha(this.videoUUID, previousSegment) | 337 | await LiveSegmentShaStore.Instance.addSegmentSha(this.videoUUID, previousSegment) |
339 | 338 | ||
340 | if (this.saveReplay) { | 339 | if (this.saveReplay) { |
341 | await this.addSegmentToReplay(hlsVideoPath, previousSegment) | 340 | await this.addSegmentToReplay(previousSegment) |
342 | } | 341 | } |
343 | }).catch(err => logger.error('Cannot process segments in %s', hlsVideoPath, { err, ...this.lTags() })) | 342 | }).catch(err => logger.error('Cannot process segments', { err, ...this.lTags() })) |
344 | } | 343 | } |
345 | 344 | ||
346 | private hasClientSocketInBadHealth (sessionId: string) { | 345 | private hasClientSocketInBadHealth (sessionId: string) { |
@@ -367,7 +366,7 @@ class MuxingSession extends EventEmitter { | |||
367 | return false | 366 | return false |
368 | } | 367 | } |
369 | 368 | ||
370 | private async addSegmentToReplay (hlsVideoPath: string, segmentPath: string) { | 369 | private async addSegmentToReplay (segmentPath: string) { |
371 | const segmentName = basename(segmentPath) | 370 | const segmentName = basename(segmentPath) |
372 | const dest = join(this.replayDirectory, buildConcatenatedName(segmentName)) | 371 | const dest = join(this.replayDirectory, buildConcatenatedName(segmentName)) |
373 | 372 | ||
diff --git a/server/models/actor/actor-image.ts b/server/models/actor/actor-image.ts index f74ab735e..9d44ef4d1 100644 --- a/server/models/actor/actor-image.ts +++ b/server/models/actor/actor-image.ts | |||
@@ -138,6 +138,9 @@ export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageMode | |||
138 | 138 | ||
139 | case ActorImageType.BANNER: | 139 | case ActorImageType.BANNER: |
140 | return join(LAZY_STATIC_PATHS.BANNERS, this.filename) | 140 | return join(LAZY_STATIC_PATHS.BANNERS, this.filename) |
141 | |||
142 | default: | ||
143 | throw new Error('Unknown actor image type: ' + this.type) | ||
141 | } | 144 | } |
142 | } | 145 | } |
143 | 146 | ||
diff --git a/server/models/user/sql/user-notitication-list-query-builder.ts b/server/models/user/sql/user-notitication-list-query-builder.ts index 6a6a71e3a..31b4932bf 100644 --- a/server/models/user/sql/user-notitication-list-query-builder.ts +++ b/server/models/user/sql/user-notitication-list-query-builder.ts | |||
@@ -86,6 +86,7 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery { | |||
86 | "Video->VideoChannel->Actor"."preferredUsername" AS "Video.VideoChannel.Actor.preferredUsername", | 86 | "Video->VideoChannel->Actor"."preferredUsername" AS "Video.VideoChannel.Actor.preferredUsername", |
87 | "Video->VideoChannel->Actor->Avatars"."id" AS "Video.VideoChannel.Actor.Avatars.id", | 87 | "Video->VideoChannel->Actor->Avatars"."id" AS "Video.VideoChannel.Actor.Avatars.id", |
88 | "Video->VideoChannel->Actor->Avatars"."width" AS "Video.VideoChannel.Actor.Avatars.width", | 88 | "Video->VideoChannel->Actor->Avatars"."width" AS "Video.VideoChannel.Actor.Avatars.width", |
89 | "Video->VideoChannel->Actor->Avatars"."type" AS "Video.VideoChannel.Actor.Avatars.type", | ||
89 | "Video->VideoChannel->Actor->Avatars"."filename" AS "Video.VideoChannel.Actor.Avatars.filename", | 90 | "Video->VideoChannel->Actor->Avatars"."filename" AS "Video.VideoChannel.Actor.Avatars.filename", |
90 | "Video->VideoChannel->Actor->Server"."id" AS "Video.VideoChannel.Actor.Server.id", | 91 | "Video->VideoChannel->Actor->Server"."id" AS "Video.VideoChannel.Actor.Server.id", |
91 | "Video->VideoChannel->Actor->Server"."host" AS "Video.VideoChannel.Actor.Server.host", | 92 | "Video->VideoChannel->Actor->Server"."host" AS "Video.VideoChannel.Actor.Server.host", |
@@ -97,6 +98,7 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery { | |||
97 | "VideoComment->Account->Actor"."preferredUsername" AS "VideoComment.Account.Actor.preferredUsername", | 98 | "VideoComment->Account->Actor"."preferredUsername" AS "VideoComment.Account.Actor.preferredUsername", |
98 | "VideoComment->Account->Actor->Avatars"."id" AS "VideoComment.Account.Actor.Avatars.id", | 99 | "VideoComment->Account->Actor->Avatars"."id" AS "VideoComment.Account.Actor.Avatars.id", |
99 | "VideoComment->Account->Actor->Avatars"."width" AS "VideoComment.Account.Actor.Avatars.width", | 100 | "VideoComment->Account->Actor->Avatars"."width" AS "VideoComment.Account.Actor.Avatars.width", |
101 | "VideoComment->Account->Actor->Avatars"."type" AS "VideoComment.Account.Actor.Avatars.type", | ||
100 | "VideoComment->Account->Actor->Avatars"."filename" AS "VideoComment.Account.Actor.Avatars.filename", | 102 | "VideoComment->Account->Actor->Avatars"."filename" AS "VideoComment.Account.Actor.Avatars.filename", |
101 | "VideoComment->Account->Actor->Server"."id" AS "VideoComment.Account.Actor.Server.id", | 103 | "VideoComment->Account->Actor->Server"."id" AS "VideoComment.Account.Actor.Server.id", |
102 | "VideoComment->Account->Actor->Server"."host" AS "VideoComment.Account.Actor.Server.host", | 104 | "VideoComment->Account->Actor->Server"."host" AS "VideoComment.Account.Actor.Server.host", |
@@ -127,6 +129,7 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery { | |||
127 | "Abuse->FlaggedAccount->Actor"."preferredUsername" AS "Abuse.FlaggedAccount.Actor.preferredUsername", | 129 | "Abuse->FlaggedAccount->Actor"."preferredUsername" AS "Abuse.FlaggedAccount.Actor.preferredUsername", |
128 | "Abuse->FlaggedAccount->Actor->Avatars"."id" AS "Abuse.FlaggedAccount.Actor.Avatars.id", | 130 | "Abuse->FlaggedAccount->Actor->Avatars"."id" AS "Abuse.FlaggedAccount.Actor.Avatars.id", |
129 | "Abuse->FlaggedAccount->Actor->Avatars"."width" AS "Abuse.FlaggedAccount.Actor.Avatars.width", | 131 | "Abuse->FlaggedAccount->Actor->Avatars"."width" AS "Abuse.FlaggedAccount.Actor.Avatars.width", |
132 | "Abuse->FlaggedAccount->Actor->Avatars"."type" AS "Abuse.FlaggedAccount.Actor.Avatars.type", | ||
130 | "Abuse->FlaggedAccount->Actor->Avatars"."filename" AS "Abuse.FlaggedAccount.Actor.Avatars.filename", | 133 | "Abuse->FlaggedAccount->Actor->Avatars"."filename" AS "Abuse.FlaggedAccount.Actor.Avatars.filename", |
131 | "Abuse->FlaggedAccount->Actor->Server"."id" AS "Abuse.FlaggedAccount.Actor.Server.id", | 134 | "Abuse->FlaggedAccount->Actor->Server"."id" AS "Abuse.FlaggedAccount.Actor.Server.id", |
132 | "Abuse->FlaggedAccount->Actor->Server"."host" AS "Abuse.FlaggedAccount.Actor.Server.host", | 135 | "Abuse->FlaggedAccount->Actor->Server"."host" AS "Abuse.FlaggedAccount.Actor.Server.host", |
@@ -155,6 +158,7 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery { | |||
155 | "ActorFollow->ActorFollower->Account"."name" AS "ActorFollow.ActorFollower.Account.name", | 158 | "ActorFollow->ActorFollower->Account"."name" AS "ActorFollow.ActorFollower.Account.name", |
156 | "ActorFollow->ActorFollower->Avatars"."id" AS "ActorFollow.ActorFollower.Avatars.id", | 159 | "ActorFollow->ActorFollower->Avatars"."id" AS "ActorFollow.ActorFollower.Avatars.id", |
157 | "ActorFollow->ActorFollower->Avatars"."width" AS "ActorFollow.ActorFollower.Avatars.width", | 160 | "ActorFollow->ActorFollower->Avatars"."width" AS "ActorFollow.ActorFollower.Avatars.width", |
161 | "ActorFollow->ActorFollower->Avatars"."type" AS "ActorFollow.ActorFollower.Avatars.type", | ||
158 | "ActorFollow->ActorFollower->Avatars"."filename" AS "ActorFollow.ActorFollower.Avatars.filename", | 162 | "ActorFollow->ActorFollower->Avatars"."filename" AS "ActorFollow.ActorFollower.Avatars.filename", |
159 | "ActorFollow->ActorFollower->Server"."id" AS "ActorFollow.ActorFollower.Server.id", | 163 | "ActorFollow->ActorFollower->Server"."id" AS "ActorFollow.ActorFollower.Server.id", |
160 | "ActorFollow->ActorFollower->Server"."host" AS "ActorFollow.ActorFollower.Server.host", | 164 | "ActorFollow->ActorFollower->Server"."host" AS "ActorFollow.ActorFollower.Server.host", |
@@ -173,6 +177,7 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery { | |||
173 | "Account->Actor"."preferredUsername" AS "Account.Actor.preferredUsername", | 177 | "Account->Actor"."preferredUsername" AS "Account.Actor.preferredUsername", |
174 | "Account->Actor->Avatars"."id" AS "Account.Actor.Avatars.id", | 178 | "Account->Actor->Avatars"."id" AS "Account.Actor.Avatars.id", |
175 | "Account->Actor->Avatars"."width" AS "Account.Actor.Avatars.width", | 179 | "Account->Actor->Avatars"."width" AS "Account.Actor.Avatars.width", |
180 | "Account->Actor->Avatars"."type" AS "Account.Actor.Avatars.type", | ||
176 | "Account->Actor->Avatars"."filename" AS "Account.Actor.Avatars.filename", | 181 | "Account->Actor->Avatars"."filename" AS "Account.Actor.Avatars.filename", |
177 | "Account->Actor->Server"."id" AS "Account.Actor.Server.id", | 182 | "Account->Actor->Server"."id" AS "Account.Actor.Server.id", |
178 | "Account->Actor->Server"."host" AS "Account.Actor.Server.host"` | 183 | "Account->Actor->Server"."host" AS "Account.Actor.Server.host"` |
diff --git a/server/tests/api/live/live-save-replay.ts b/server/tests/api/live/live-save-replay.ts index 7ddcb04ef..007af51e9 100644 --- a/server/tests/api/live/live-save-replay.ts +++ b/server/tests/api/live/live-save-replay.ts | |||
@@ -441,6 +441,40 @@ describe('Save replay setting', function () { | |||
441 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) | 441 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) |
442 | await checkLiveCleanup(servers[0], liveVideoUUID, []) | 442 | await checkLiveCleanup(servers[0], liveVideoUUID, []) |
443 | }) | 443 | }) |
444 | |||
445 | it('Should correctly save replays with multiple sessions', async function () { | ||
446 | this.timeout(120000) | ||
447 | |||
448 | liveVideoUUID = await createLiveWrapper({ permanent: true, replay: true }) | ||
449 | await waitJobs(servers) | ||
450 | |||
451 | // Streaming session #1 | ||
452 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
453 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
454 | await stopFfmpeg(ffmpegCommand) | ||
455 | await servers[0].live.waitUntilWaiting({ videoId: liveVideoUUID }) | ||
456 | |||
457 | // Streaming session #2 | ||
458 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
459 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
460 | await stopFfmpeg(ffmpegCommand) | ||
461 | await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID) | ||
462 | |||
463 | // Wait for replays | ||
464 | await waitJobs(servers) | ||
465 | |||
466 | const { total, data: sessions } = await servers[0].live.listSessions({ videoId: liveVideoUUID }) | ||
467 | |||
468 | expect(total).to.equal(2) | ||
469 | expect(sessions).to.have.lengthOf(2) | ||
470 | |||
471 | for (const session of sessions) { | ||
472 | expect(session.error).to.be.null | ||
473 | expect(session.replayVideo).to.exist | ||
474 | |||
475 | await servers[0].videos.get({ id: session.replayVideo.uuid }) | ||
476 | } | ||
477 | }) | ||
444 | }) | 478 | }) |
445 | 479 | ||
446 | after(async function () { | 480 | after(async function () { |
diff --git a/server/tests/api/live/live-socket-messages.ts b/server/tests/api/live/live-socket-messages.ts index 7668ed5b9..1669369c0 100644 --- a/server/tests/api/live/live-socket-messages.ts +++ b/server/tests/api/live/live-socket-messages.ts | |||
@@ -18,7 +18,7 @@ import { | |||
18 | 18 | ||
19 | const expect = chai.expect | 19 | const expect = chai.expect |
20 | 20 | ||
21 | describe('Test live', function () { | 21 | describe('Test live socket messages', function () { |
22 | let servers: PeerTubeServer[] = [] | 22 | let servers: PeerTubeServer[] = [] |
23 | 23 | ||
24 | before(async function () { | 24 | before(async function () { |
diff --git a/server/tests/shared/live.ts b/server/tests/shared/live.ts index 6ee4899b0..4bd4786fc 100644 --- a/server/tests/shared/live.ts +++ b/server/tests/shared/live.ts | |||
@@ -3,15 +3,35 @@ | |||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { pathExists, readdir } from 'fs-extra' | 4 | import { pathExists, readdir } from 'fs-extra' |
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import { LiveVideo } from '@shared/models' | ||
6 | import { PeerTubeServer } from '@shared/server-commands' | 7 | import { PeerTubeServer } from '@shared/server-commands' |
7 | 8 | ||
8 | async function checkLiveCleanup (server: PeerTubeServer, videoUUID: string, savedResolutions: number[] = []) { | 9 | async function checkLiveCleanup (server: PeerTubeServer, videoUUID: string, savedResolutions: number[] = []) { |
10 | let live: LiveVideo | ||
11 | |||
12 | try { | ||
13 | live = await server.live.get({ videoId: videoUUID }) | ||
14 | } catch {} | ||
15 | |||
9 | const basePath = server.servers.buildDirectory('streaming-playlists') | 16 | const basePath = server.servers.buildDirectory('streaming-playlists') |
10 | const hlsPath = join(basePath, 'hls', videoUUID) | 17 | const hlsPath = join(basePath, 'hls', videoUUID) |
11 | 18 | ||
12 | if (savedResolutions.length === 0) { | 19 | if (savedResolutions.length === 0) { |
13 | const result = await pathExists(hlsPath) | 20 | |
14 | expect(result).to.be.false | 21 | if (live?.permanentLive) { |
22 | expect(await pathExists(hlsPath)).to.be.true | ||
23 | |||
24 | const hlsFiles = await readdir(hlsPath) | ||
25 | expect(hlsFiles).to.have.lengthOf(1) // Only replays directory | ||
26 | |||
27 | const replayDir = join(hlsPath, 'replay') | ||
28 | expect(await pathExists(replayDir)).to.be.true | ||
29 | |||
30 | const replayFiles = await readdir(join(hlsPath, 'replay')) | ||
31 | expect(replayFiles).to.have.lengthOf(0) | ||
32 | } else { | ||
33 | expect(await pathExists(hlsPath)).to.be.false | ||
34 | } | ||
15 | 35 | ||
16 | return | 36 | return |
17 | } | 37 | } |
diff --git a/server/tests/shared/notifications.ts b/server/tests/shared/notifications.ts index a62410880..09bc8da03 100644 --- a/server/tests/shared/notifications.ts +++ b/server/tests/shared/notifications.ts | |||
@@ -185,7 +185,7 @@ async function checkUserRegistered (options: CheckerBaseParams & { | |||
185 | expect(notification).to.not.be.undefined | 185 | expect(notification).to.not.be.undefined |
186 | expect(notification.type).to.equal(notificationType) | 186 | expect(notification.type).to.equal(notificationType) |
187 | 187 | ||
188 | checkActor(notification.account) | 188 | checkActor(notification.account, { withAvatar: false }) |
189 | expect(notification.account.name).to.equal(username) | 189 | expect(notification.account.name).to.equal(username) |
190 | } else { | 190 | } else { |
191 | expect(notification).to.satisfy(n => n.type !== notificationType || n.account.name !== username) | 191 | expect(notification).to.satisfy(n => n.type !== notificationType || n.account.name !== username) |
@@ -253,7 +253,7 @@ async function checkNewInstanceFollower (options: CheckerBaseParams & { | |||
253 | expect(notification).to.not.be.undefined | 253 | expect(notification).to.not.be.undefined |
254 | expect(notification.type).to.equal(notificationType) | 254 | expect(notification.type).to.equal(notificationType) |
255 | 255 | ||
256 | checkActor(notification.actorFollow.follower) | 256 | checkActor(notification.actorFollow.follower, { withAvatar: false }) |
257 | expect(notification.actorFollow.follower.name).to.equal('peertube') | 257 | expect(notification.actorFollow.follower.name).to.equal('peertube') |
258 | expect(notification.actorFollow.follower.host).to.equal(followerHost) | 258 | expect(notification.actorFollow.follower.host).to.equal(followerHost) |
259 | 259 | ||
@@ -288,7 +288,8 @@ async function checkAutoInstanceFollowing (options: CheckerBaseParams & { | |||
288 | expect(notification.type).to.equal(notificationType) | 288 | expect(notification.type).to.equal(notificationType) |
289 | 289 | ||
290 | const following = notification.actorFollow.following | 290 | const following = notification.actorFollow.following |
291 | checkActor(following) | 291 | |
292 | checkActor(following, { withAvatar: false }) | ||
292 | expect(following.name).to.equal('peertube') | 293 | expect(following.name).to.equal('peertube') |
293 | expect(following.host).to.equal(followingHost) | 294 | expect(following.host).to.equal(followingHost) |
294 | 295 | ||
@@ -701,6 +702,9 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an | |||
701 | const userAccessToken = await servers[0].login.getAccessToken(user) | 702 | const userAccessToken = await servers[0].login.getAccessToken(user) |
702 | 703 | ||
703 | await servers[0].notifications.updateMySettings({ token: userAccessToken, settings: getAllNotificationsSettings() }) | 704 | await servers[0].notifications.updateMySettings({ token: userAccessToken, settings: getAllNotificationsSettings() }) |
705 | await servers[0].users.updateMyAvatar({ token: userAccessToken, fixture: 'avatar.png' }) | ||
706 | await servers[0].channels.updateImage({ channelName: 'user_1_channel', token: userAccessToken, fixture: 'avatar.png', type: 'avatar' }) | ||
707 | |||
704 | await servers[0].notifications.updateMySettings({ settings: getAllNotificationsSettings() }) | 708 | await servers[0].notifications.updateMySettings({ settings: getAllNotificationsSettings() }) |
705 | 709 | ||
706 | if (serversCount > 1) { | 710 | if (serversCount > 1) { |
@@ -832,10 +836,18 @@ function checkVideo (video: any, videoName?: string, shortUUID?: string) { | |||
832 | expect(video.id).to.be.a('number') | 836 | expect(video.id).to.be.a('number') |
833 | } | 837 | } |
834 | 838 | ||
835 | function checkActor (actor: any) { | 839 | function checkActor (actor: any, options: { withAvatar?: boolean } = {}) { |
840 | const { withAvatar = true } = options | ||
841 | |||
836 | expect(actor.displayName).to.be.a('string') | 842 | expect(actor.displayName).to.be.a('string') |
837 | expect(actor.displayName).to.not.be.empty | 843 | expect(actor.displayName).to.not.be.empty |
838 | expect(actor.host).to.not.be.undefined | 844 | expect(actor.host).to.not.be.undefined |
845 | |||
846 | if (withAvatar) { | ||
847 | expect(actor.avatars).to.be.an('array') | ||
848 | expect(actor.avatars).to.have.lengthOf(2) | ||
849 | expect(actor.avatars[0].path).to.exist.and.not.empty | ||
850 | } | ||
839 | } | 851 | } |
840 | 852 | ||
841 | function checkComment (comment: any, commentId: number, threadId: number) { | 853 | function checkComment (comment: any, commentId: number, threadId: number) { |