Merge branch 'release/6.3.0' into develop

pull/6633/head
Chocobozzz 2024-10-22 10:53:11 +02:00
commit 5af6cf6e82
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
10 changed files with 126 additions and 29 deletions

View File

@ -27,7 +27,7 @@
[buttonTooltip]="getReactiveFileButtonTooltip()">
</my-reactive-file>
@if (video) {
@if (canSelectFromVideo()) {
<button i18n type="button" class="peertube-button grey-button ms-2" (click)="selectFromVideo()">Select from video</button>
}
}

View File

@ -13,7 +13,7 @@ import {
import { ReactiveFileComponent } from '@app/shared/shared-forms/reactive-file.component'
import { BytesPipe } from '@app/shared/shared-main/common/bytes.pipe'
import { EmbedComponent, EmbedVideoInput } from '@app/shared/shared-main/video/embed.component'
import { HTMLServerConfig } from '@peertube/peertube-models'
import { HTMLServerConfig, Video, VideoState } from '@peertube/peertube-models'
import { imageToDataURL } from '@root-helpers/images'
import { PeerTubePlayer } from '../../../../../standalone/embed-player-api/player'
@ -34,7 +34,7 @@ import { PeerTubePlayer } from '../../../../../standalone/embed-player-api/playe
export class ThumbnailManagerComponent implements OnInit, ControlValueAccessor {
@ViewChild('embed') embed: EmbedComponent
@Input() video: EmbedVideoInput
@Input() video: EmbedVideoInput & Pick<Video, 'isLive' | 'state'>
imageSrc: string
allowedExtensionsMessage = ''
@ -71,6 +71,10 @@ export class ThumbnailManagerComponent implements OnInit, ControlValueAccessor {
return this.bytesPipe.transform(this.maxVideoImageSize)
}
canSelectFromVideo () {
return this.video && !this.video.isLive && this.video.state.id === VideoState.PUBLISHED
}
getReactiveFileButtonTooltip () {
return $localize`(extensions: ${this.videoImageExtensions}, ${this.maxSizeText}\: ${this.maxVideoImageSizeInBytes})`
}

View File

@ -1,10 +1,10 @@
import { Component, Input, OnInit } from '@angular/core'
import { HooksService } from '@app/core'
import { VideoDetails } from '@app/shared/shared-main/video/video-details.model'
import { TimeDurationFormatterPipe } from '../../../../shared/shared-main/date/time-duration-formatter.pipe'
import { GlobalIconComponent } from '../../../../shared/shared-icons/global-icon.component'
import { DatePipe, NgFor, NgIf } from '@angular/common'
import { Component, Input, OnChanges } from '@angular/core'
import { RouterLink } from '@angular/router'
import { NgIf, NgFor, DatePipe } from '@angular/common'
import { HooksService } from '@app/core'
import { TimeDurationFormatterPipe } from '@app/shared/shared-main/date/time-duration-formatter.pipe'
import { VideoDetails } from '@app/shared/shared-main/video/video-details.model'
import { GlobalIconComponent } from '../../../../shared/shared-icons/global-icon.component'
type PluginMetadata = {
label: string
@ -20,14 +20,14 @@ type PluginMetadata = {
standalone: true,
imports: [ NgIf, RouterLink, GlobalIconComponent, NgFor, DatePipe, TimeDurationFormatterPipe ]
})
export class VideoAttributesComponent implements OnInit {
export class VideoAttributesComponent implements OnChanges {
@Input() video: VideoDetails
pluginMetadata: PluginMetadata[] = []
constructor (private hooks: HooksService) { }
async ngOnInit () {
async ngOnChanges () {
this.pluginMetadata = await this.hooks.wrapObject(
this.pluginMetadata,
'video-watch',

View File

@ -1,6 +1,6 @@
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'
import { buildAspectRatio, forceNumber } from '@peertube/peertube-core-utils'
import { VideoResolution } from '@peertube/peertube-models'
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'
/**
*
@ -111,7 +111,11 @@ async function getVideoStreamDimensionsInfo (path: string, existingProbe?: Ffpro
}
}
if (videoStream.rotation === '90' || videoStream.rotation === '-90') {
const rotation = videoStream.rotation
? videoStream.rotation + ''
: undefined
if (rotation === '90' || rotation === '-90') {
const width = videoStream.width
videoStream.width = videoStream.height
videoStream.height = width
@ -202,16 +206,19 @@ async function getChaptersFromContainer (options: {
// ---------------------------------------------------------------------------
export {
getVideoStreamDimensionsInfo,
getChaptersFromContainer,
getMaxAudioBitrate,
getVideoStream,
getVideoStreamDuration,
getAudioStream,
getVideoStreamFPS,
isAudioFile,
ffprobePromise,
getAudioStream,
getChaptersFromContainer,
getMaxAudioBitrate,
getVideoStream,
getVideoStreamBitrate,
getVideoStreamDimensionsInfo,
getVideoStreamDuration,
getVideoStreamFPS,
hasAudioStream,
hasVideoStream
hasVideoStream,
isAudioFile
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -27,6 +27,7 @@ import {
waitUntilLiveReplacedByReplayOnAllServers,
waitUntilLiveWaitingOnAllServers
} from '@peertube/peertube-server-commands'
import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js'
import { checkLiveCleanup } from '@tests/shared/live.js'
import { expect } from 'chai'
import { FfmpegCommand } from 'fluent-ffmpeg'
@ -36,7 +37,13 @@ describe('Save replay setting', function () {
let liveVideoUUID: string
let ffmpegCommand: FfmpegCommand
async function createLiveWrapper (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacyType } }) {
async function createLiveWrapper (options: {
permanent: boolean
replay: boolean
replaySettings?: { privacy: VideoPrivacyType }
thumbnailfile?: string
previewfile?: string
}) {
if (liveVideoUUID) {
try {
await servers[0].videos.remove({ id: liveVideoUUID })
@ -51,7 +58,9 @@ describe('Save replay setting', function () {
tags: [ 'tag1', 'tag2' ],
saveReplay: options.replay,
replaySettings: options.replaySettings,
permanentLive: options.permanent
permanentLive: options.permanent,
thumbnailfile: options.thumbnailfile,
previewfile: options.previewfile
}
const { uuid } = await servers[0].live.create({ fields: attributes })
@ -142,6 +151,15 @@ describe('Save replay setting', function () {
}
}
async function checkVideoThumbnail (videoId: string, thumbnailfile: string, previewfile?: string) {
for (const server of servers) {
const video = await server.videos.get({ id: videoId })
await testImageGeneratedByFFmpeg(server.url, thumbnailfile, video.thumbnailPath, '')
if (previewfile) await testImageGeneratedByFFmpeg(server.url, previewfile, video.previewPath, '')
}
}
before(async function () {
this.timeout(120000)
@ -285,6 +303,7 @@ describe('Save replay setting', function () {
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
await checkVideoThumbnail(liveVideoUUID, 'default-live-thumbnail.jpg', 'default-live-preview.jpg')
})
it('Should correctly have updated the live and federated it when streaming in the live', async function () {
@ -298,6 +317,7 @@ describe('Save replay setting', function () {
await checkVideosExist(liveVideoUUID, 1, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
await checkVideoThumbnail(liveVideoUUID, 'default-live-thumbnail.jpg', 'default-live-preview.jpg')
})
it('Should correctly have saved the live and federated it after the streaming', async function () {
@ -345,7 +365,15 @@ describe('Save replay setting', function () {
it('Should update the saved live and correctly federate the updated attributes', async function () {
this.timeout(120000)
await servers[0].videos.update({ id: liveVideoUUID, attributes: { name: 'video updated', privacy: VideoPrivacy.PUBLIC } })
await servers[0].videos.update({
id: liveVideoUUID,
attributes: {
name: 'video updated',
privacy: VideoPrivacy.PUBLIC,
thumbnailfile: 'custom-thumbnail.jpg',
previewfile: 'custom-preview.jpg'
}
})
await waitJobs(servers)
for (const server of servers) {
@ -353,6 +381,8 @@ describe('Save replay setting', function () {
expect(video.name).to.equal('video updated')
expect(video.isLive).to.be.false
expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC)
await checkVideoThumbnail(liveVideoUUID, 'custom-thumbnail.jpg', 'custom-preview.jpg')
}
})
@ -406,13 +436,20 @@ describe('Save replay setting', function () {
it('Should correctly create and federate the "waiting for stream" live', async function () {
this.timeout(120000)
liveVideoUUID = await createLiveWrapper({ permanent: true, replay: true, replaySettings: { privacy: VideoPrivacy.UNLISTED } })
liveVideoUUID = await createLiveWrapper({
permanent: true,
replay: true,
replaySettings: { privacy: VideoPrivacy.UNLISTED },
thumbnailfile: 'custom-thumbnail.jpg',
previewfile: 'custom-preview.jpg'
})
await waitJobs(servers)
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
await checkVideoThumbnail(liveVideoUUID, 'custom-thumbnail.jpg', 'custom-preview.jpg')
})
it('Should correctly have updated the live and federated it when streaming in the live', async function () {
@ -484,10 +521,19 @@ describe('Save replay setting', function () {
await checkVideosExist(lastReplayUUID, 1, HttpStatusCode.OK_200)
await checkVideoState(lastReplayUUID, VideoState.PUBLISHED)
await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.PUBLIC)
await checkVideoThumbnail(lastReplayUUID, 'custom-thumbnail-from-preview.jpg', 'custom-preview.jpg')
})
it('Should update the live replay thumbnail', async function () {
await servers[0].videos.update({ id: lastReplayUUID, attributes: { thumbnailfile: 'custom-thumbnail-2.jpg' } })
await waitJobs(servers)
await checkVideoThumbnail(liveVideoUUID, 'custom-thumbnail.jpg', 'custom-preview.jpg')
await checkVideoThumbnail(lastReplayUUID, 'custom-thumbnail-2.jpg')
})
})
describe('With a second live and its replay', function () {
describe('With a second live session', function () {
it('Should update the replay settings', async function () {
await servers[0].live.update({ videoId: liveVideoUUID, fields: { replaySettings: { privacy: VideoPrivacy.PUBLIC } } })
@ -498,7 +544,6 @@ describe('Save replay setting', function () {
expect(live.saveReplay).to.be.true
expect(live.replaySettings).to.exist
expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC)
})
it('Should correctly have updated the live and federated it when streaming in the live', async function () {
@ -572,6 +617,9 @@ describe('Save replay setting', function () {
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: true, deleted: true })
})
})
describe('With terminated sessions', function () {
it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () {
this.timeout(120000)
@ -612,6 +660,42 @@ describe('Save replay setting', function () {
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: true, deleted: true })
})
})
describe('With a live without custom thumbnail', function () {
it('Should correctly set the default thumbnail to the live replay', async function () {
this.timeout(120000)
liveVideoUUID = await createLiveWrapper({
permanent: true,
replay: true,
replaySettings: { privacy: VideoPrivacy.PUBLIC }
})
ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
await stopFfmpeg(ffmpegCommand)
await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID)
await waitJobs(servers)
const video = await findExternalSavedVideo(servers[0], liveVideoUUID)
lastReplayUUID = video.uuid
await checkVideoThumbnail(liveVideoUUID, 'default-live-thumbnail.jpg', 'default-live-preview.jpg')
})
it('Should update the live replay thumbnail', async function () {
await servers[0].videos.update({
id: lastReplayUUID,
attributes: { thumbnailfile: 'custom-thumbnail.jpg', previewfile: 'custom-preview.jpg' }
})
await waitJobs(servers)
await checkVideoThumbnail(liveVideoUUID, 'default-live-thumbnail.jpg', 'default-live-preview.jpg')
await checkVideoThumbnail(lastReplayUUID, 'custom-thumbnail.jpg', 'custom-preview.jpg')
})
})
})
after(async function () {

View File

@ -1336,6 +1336,7 @@ function buildVideoMimetypeExt () {
// Developed by Apple
'video/quicktime': [ '.mov', '.qt', '.mqv' ], // often used as output format by editing software
'video/mov': '.mov', // Windows: https://github.com/Chocobozzz/PeerTube/issues/6669
'video/x-m4v': '.m4v',
'video/m4v': '.m4v',

View File

@ -209,7 +209,8 @@ async function copyOrRegenerateThumbnails (options: {
inputPath: preview.getPath(),
video: replayVideo,
type,
automaticallyGenerated: false
automaticallyGenerated: false,
keepOriginal: true
})
})
)