Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private updatePlayerOnNoLive () {
this.peertubePlayer.unload()
this.peertubePlayer.disable()
this.peertubePlayer.setPoster(this.video.previewPath)
this.peertubePlayer.setPoster(this.video.previewUrl)
}

private buildHotkeysHelp (video: Video) {
Expand Down
5 changes: 2 additions & 3 deletions client/src/app/shared/shared-main/video/video-edit.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { getAbsoluteAPIUrl } from '@app/helpers'
import { objectKeysTyped } from '@peertube/peertube-core-utils'
import {
VideoCommentPolicyType,
Expand Down Expand Up @@ -62,8 +61,8 @@ export class VideoEdit implements VideoUpdate {
this.commentsPolicy = video.commentsPolicy.id
this.downloadEnabled = video.downloadEnabled

if (video.thumbnailPath) this.thumbnailUrl = getAbsoluteAPIUrl() + video.thumbnailPath
if (video.previewPath) this.previewUrl = getAbsoluteAPIUrl() + video.previewPath
if (video.thumbnailUrl) this.thumbnailUrl = video.thumbnailUrl
if (video.previewUrl) this.previewUrl = video.previewUrl

this.scheduleUpdate = video.scheduledUpdate
this.originallyPublishedAt = video.originallyPublishedAt
Expand Down
16 changes: 3 additions & 13 deletions client/src/app/shared/shared-main/video/video.model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AuthUser } from '@app/core'
import { User } from '@app/core/users/user.model'
import { durationToString, getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers'
import { durationToString, getAbsoluteEmbedUrl } from '@app/helpers'
import { Actor } from '@app/shared/shared-main/account/actor.model'
import { buildVideoWatchPath, getAllFiles, peertubeTranslate } from '@peertube/peertube-core-utils'
import {
Expand Down Expand Up @@ -48,14 +48,12 @@ export class Video implements VideoServerModel {

name: string
serverHost: string
thumbnailPath: string
thumbnailUrl: string

aspectRatio: number

isLive: boolean

previewPath: string
previewUrl: string

embedPath: string
Expand Down Expand Up @@ -125,8 +123,6 @@ export class Video implements VideoServerModel {
}

constructor (hash: VideoServerModel, translations: { [ id: string ]: string } = {}) {
const absoluteAPIUrl = getAbsoluteAPIUrl()

this.createdAt = new Date(hash.createdAt.toString())
this.publishedAt = new Date(hash.publishedAt.toString())
this.category = hash.category
Expand All @@ -151,15 +147,9 @@ export class Video implements VideoServerModel {
this.isLocal = hash.isLocal
this.name = hash.name

this.thumbnailPath = hash.thumbnailPath
this.thumbnailUrl = this.thumbnailPath
? hash.thumbnailUrl || (absoluteAPIUrl + hash.thumbnailPath)
: null
this.thumbnailUrl = hash.thumbnailUrl

this.previewPath = hash.previewPath
this.previewUrl = this.previewPath
? hash.previewUrl || (absoluteAPIUrl + hash.previewPath)
: null
this.previewUrl = hash.previewUrl

this.embedPath = hash.embedPath
this.embedUrl = hash.embedUrl || (getAbsoluteEmbedUrl() + hash.embedPath)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<a [href]="getVideoUrl()" class="table-video-link" [title]="video().name" target="_blank" rel="noopener noreferrer">
<div class="table-video">
<div class="table-video-image">
<img [src]="video().thumbnailPath" alt="">
<img [src]="video().thumbnailUrl" alt="">

<ng-content select="[image]"></ng-content>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export class VideoPlaylist implements ServerVideoPlaylist {
ownerAccount: AccountSummary
videoChannel?: VideoChannelSummary

thumbnailPath: string
thumbnailUrl: string

embedPath: string
Expand Down Expand Up @@ -63,11 +62,7 @@ export class VideoPlaylist implements ServerVideoPlaylist {
this.description = hash.description
this.privacy = hash.privacy

this.thumbnailPath = hash.thumbnailPath

this.thumbnailUrl = this.thumbnailPath
? hash.thumbnailUrl || (absoluteAPIUrl + hash.thumbnailPath)
: absoluteAPIUrl + '/client/assets/images/default-playlist.jpg'
this.thumbnailUrl = hash.thumbnailUrl || absoluteAPIUrl + '/client/assets/images/default-playlist.jpg'

this.embedPath = hash.embedPath
this.embedUrl = hash.embedUrl || (getAbsoluteEmbedUrl() + hash.embedPath)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class PlaylistMenuItem extends Component {
positionBlock.appendChild(player)

const thumbnail = super.createEl('img', {
src: window.location.origin + videoElement.video.thumbnailPath
src: window.location.origin + videoElement.video.thumbnailUrl
})

const infoBlock = super.createEl('div', {
Expand Down
2 changes: 1 addition & 1 deletion client/src/standalone/videos/embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ export class PeerTubeEmbed {

this.peertubePlayer.unload()
this.peertubePlayer.disable()
this.peertubePlayer.setPoster(video.previewPath)
this.peertubePlayer.setPoster(video.previewUrl)
}

private async handlePasswordError (err: PeerTubeServerError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ export class PlayerOptionsBuilder {
duration: video.duration,
videoRatio: video.aspectRatio,

poster: getBackendUrl() + video.previewPath,
poster: video.previewUrl,

embedUrl: getBackendUrl() + video.embedPath,
embedTitle: video.name,
Expand Down
6 changes: 6 additions & 0 deletions config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,12 @@ object_storage:
prefix: ''
base_url: ''

# Video thumbnails
thumbnails:
bucket_name: 'thumbnails'
prefix: ''
base_url: ''

log:
level: 'info' # 'debug' | 'info' | 'warn' | 'error'

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@
"ip-anonymize": "^0.1.0",
"ipaddr.js": "2.2.0",
"iso-639-3": "3.0.1",
"jimp": "^0.22.4",
"js-yaml": "^4.0.0",
"jsonld": "~8.3.1",
"jsonwebtoken": "^9.0.2",
Expand Down Expand Up @@ -181,6 +180,7 @@
"sanitize-html": "2.x",
"sequelize": "~6.37.3",
"sequelize-typescript": "^2.0.0-beta.1",
"sharp": "^0.33.5",
"short-uuid": "^5.2.0",
"sitemap": "^8.0.0",
"socket.io": "^4.5.4",
Expand Down
80 changes: 53 additions & 27 deletions packages/ffmpeg/src/ffmpeg-images.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,62 @@
import { MutexInterface } from 'async-mutex'
import { FfprobeData } from 'fluent-ffmpeg'
import { Duplex, PassThrough, Readable, Stream, Writable } from 'node:stream'
import { FFmpegCommandWrapper, FFmpegCommandWrapperOptions } from './ffmpeg-command-wrapper.js'
import { getVideoStreamDuration } from './ffprobe.js'

async function streamToBuffer (readableStream: Stream): Promise<Buffer> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = []

readableStream.on('data', data => {
if (typeof data === 'string') {
chunks.push(Buffer.from(data, 'utf-8'))
} else if (data instanceof Buffer) {
chunks.push(data)
} else {
const jsonData = JSON.stringify(data)
chunks.push(Buffer.from(jsonData, 'utf-8'))
}
})

readableStream.on('end', () => {
resolve(Buffer.concat(chunks))
})

readableStream.on('error', reject)
})
}
export class FFmpegImage {
private readonly commandWrapper: FFmpegCommandWrapper

constructor (options: FFmpegCommandWrapperOptions) {
this.commandWrapper = new FFmpegCommandWrapper(options)
}

convertWebPToJPG (options: {
path: string
destination: string
}): Promise<void> {
const { path, destination } = options

this.commandWrapper.buildCommand(path)
.output(destination)

return this.commandWrapper.runCommand({ silent: true })
}

processGIF (options: {
path: string
destination: string
async processGIF (options: {
source: string | Buffer
destination: string | null
newSize?: { width: number, height: number }
}): Promise<void> {
const { path, destination, newSize } = options
}) {
const { source, destination, newSize } = options

const command = this.commandWrapper.buildCommand(path)
const command = this.commandWrapper.buildCommand(source === 'string' ? source : Readable.from(source))

if (newSize) command.size(`${newSize.width}x${newSize.height}`)

command.output(destination)
const stream = new Duplex()
command.output(destination ?? stream)

return this.commandWrapper.runCommand()
await this.commandWrapper.runCommand()

return streamToBuffer(stream)
}

// ---------------------------------------------------------------------------

async generateThumbnailFromVideo (options: {
fromPath: string
output: string
output: string | null
framesToAnalyze: number
scale?: {
width: number
Expand All @@ -54,26 +68,32 @@ export class FFmpegImage {

let duration = await getVideoStreamDuration(fromPath, ffprobe)
if (isNaN(duration)) duration = 0
const outputPath = options.output
const outputStream = new PassThrough()

this.buildGenerateThumbnailFromVideo(options)
this.buildGenerateThumbnailFromVideo({ ...options, output: outputPath ?? outputStream })
.seekInput(duration / 2)

try {
return await this.commandWrapper.runCommand()
await this.commandWrapper.runCommand()

return outputPath ?? await streamToBuffer(outputStream)
} catch (err) {
this.commandWrapper.debugLog('Cannot generate thumbnail from video using seek input, fallback to no seek', { err })

this.commandWrapper.resetCommand()

this.buildGenerateThumbnailFromVideo(options)
this.buildGenerateThumbnailFromVideo({ ...options, output: outputPath ?? outputStream })

await this.commandWrapper.runCommand()

return this.commandWrapper.runCommand()
return outputPath ?? await streamToBuffer(outputStream)
}
}

private buildGenerateThumbnailFromVideo (options: {
fromPath: string
output: string
output: string | Writable
framesToAnalyze: number
scale?: {
width: number
Expand All @@ -87,7 +107,13 @@ export class FFmpegImage {
.outputOption('-frames:v 1')
.outputOption('-q:v 5')
.outputOption('-abort_on empty_output')
.output(output)

if (output instanceof Writable) {
command.outputOption('-f image2')
command.pipe(output)
} else {
command.addOutput(output)
}

if (scale) {
command.videoFilter(`scale=${scale.width}x${scale.height}:force_original_aspect_ratio=decrease`)
Expand Down
3 changes: 1 addition & 2 deletions packages/models/src/videos/playlist/video-playlist.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ export interface VideoPlaylist {
description: string
privacy: VideoConstant<VideoPlaylistPrivacyType>

thumbnailPath: string
thumbnailUrl?: string
thumbnailUrl: string

videosLength: number

Expand Down
6 changes: 2 additions & 4 deletions packages/models/src/videos/video.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,9 @@ export interface Video extends Partial<VideoAdditionalAttributes> {

isLive: boolean

thumbnailPath: string
thumbnailUrl?: string
thumbnailUrl: string

previewPath: string
previewUrl?: string
previewUrl: string

embedPath: string
embedUrl?: string
Expand Down
4 changes: 2 additions & 2 deletions packages/tests/src/api/live/live-save-replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,9 @@ 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, '')
await testImageGeneratedByFFmpeg(server.url, thumbnailfile, video.thumbnailUrl, '')

if (previewfile) await testImageGeneratedByFFmpeg(server.url, previewfile, video.previewPath, '')
if (previewfile) await testImageGeneratedByFFmpeg(server.url, previewfile, video.previewUrl, '')
}
}

Expand Down
8 changes: 4 additions & 4 deletions packages/tests/src/api/live/live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ describe('Test live', function () {
expect(video.downloadEnabled).to.be.false
expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC)

await testImageGeneratedByFFmpeg(server.url, 'video_short1-preview.webm', video.previewPath)
await testImageGeneratedByFFmpeg(server.url, 'video_short1.webm', video.thumbnailPath)
await testImageGeneratedByFFmpeg(server.url, 'video_short1-preview.webm', video.previewUrl)
await testImageGeneratedByFFmpeg(server.url, 'video_short1.webm', video.thumbnailUrl)

const live = await server.live.get({ videoId: liveVideoUUID })

Expand Down Expand Up @@ -170,8 +170,8 @@ describe('Test live', function () {
expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED)
expect(video.nsfw).to.be.true

await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 })
await makeGetRequest({ url: video.thumbnailUrl, expectedStatus: HttpStatusCode.OK_200 })
await makeGetRequest({ url: video.previewUrl, expectedStatus: HttpStatusCode.OK_200 })
}
})

Expand Down
3 changes: 1 addition & 2 deletions packages/tests/src/api/runners/runner-vod-transcoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,8 +464,7 @@ describe('Test runner VOD transcoding', function () {

const video = await servers[0].videos.get({ id: videoUUID })
const { body: inputFile } = await makeGetRequest({
url: servers[0].url,
path: video.previewPath,
url: video.previewUrl,
expectedStatus: HttpStatusCode.OK_200
})

Expand Down
4 changes: 2 additions & 2 deletions packages/tests/src/api/server/lazy-static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ describe('Test lazy static endpoinds', function () {

const fetchRemoteImages = async () => {
const video = await servers[1].videos.get({ id: videoId })
await makeGetRequest({ url: servers[1].url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
await makeGetRequest({ url: servers[1].url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 })
await makeGetRequest({ url: servers[1].url, path: video.thumbnailUrl, expectedStatus: HttpStatusCode.OK_200 })
await makeGetRequest({ url: servers[1].url, path: video.previewUrl, expectedStatus: HttpStatusCode.OK_200 })
}

await fetchRemoteImages()
Expand Down
2 changes: 1 addition & 1 deletion packages/tests/src/api/server/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ describe('Test services', function () {
`title="${video.name}" src="http://${server.host}/videos/embed/${video.shortUUID}${suffix.output}" ` +
'frameborder="0" allowfullscreen></iframe>'

const expectedThumbnailUrl = 'http://' + server.host + video.previewPath
const expectedThumbnailUrl = video.previewUrl

expect(res.body.html).to.equal(expectedHtml)
expect(res.body.title).to.equal(video.name)
Expand Down
Loading
Loading