1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-03 09:49:20 +02:00

Separate HLS audio and video streams

Allows:
  * The HLS player to propose an "Audio only" resolution
  * The live to output an "Audio only" resolution
  * The live to ingest and output an "Audio only" stream

 This feature is under a config for VOD videos and is enabled by default for lives

 In the future we can imagine:
  * To propose multiple audio streams for a specific video
  * To ingest an audio only VOD and just output an audio only "video"
    (the player would play the audio file and PeerTube would not
    generate additional resolutions)

This commit introduce a new way to download videos:
 * Add "/download/videos/generate/:videoId" endpoint where PeerTube can
   mux an audio only and a video only file to a mp4 container
 * The download client modal introduces a new default panel where the
   user can choose resolutions it wants to download
This commit is contained in:
Chocobozzz 2024-07-23 16:38:51 +02:00 committed by Chocobozzz
parent e77ba2dfbc
commit 816f346a60
186 changed files with 5748 additions and 2807 deletions

View file

@ -5,7 +5,7 @@ import { AbstractCommand, OverrideCommandOptions } from '../shared/abstract-comm
export class ConfigCommand extends AbstractCommand {
static getCustomConfigResolutions (enabled: boolean, with0p = false) {
static getConfigResolutions (enabled: boolean, with0p = false) {
return {
'0p': enabled && with0p,
'144p': enabled,
@ -19,6 +19,20 @@ export class ConfigCommand extends AbstractCommand {
}
}
static getCustomConfigResolutions (enabled: number[]) {
return {
'0p': enabled.includes(0),
'144p': enabled.includes(144),
'240p': enabled.includes(240),
'360p': enabled.includes(360),
'480p': enabled.includes(480),
'720p': enabled.includes(720),
'1080p': enabled.includes(1080),
'1440p': enabled.includes(1440),
'2160p': enabled.includes(2160)
}
}
// ---------------------------------------------------------------------------
static getEmailOverrideConfig (emailPort: number) {
@ -211,19 +225,27 @@ export class ConfigCommand extends AbstractCommand {
enableLive (options: {
allowReplay?: boolean
resolutions?: 'min' | 'max' | number[] // default 'min'
transcoding?: boolean
resolutions?: 'min' | 'max' // Default max
maxDuration?: number
alwaysTranscodeOriginalResolution?: boolean
} = {}) {
const { allowReplay, transcoding, resolutions = 'max' } = options
const { allowReplay, transcoding, maxDuration, resolutions = 'min', alwaysTranscodeOriginalResolution } = options
return this.updateExistingConfig({
newConfig: {
live: {
enabled: true,
allowReplay: allowReplay ?? true,
allowReplay,
maxDuration,
transcoding: {
enabled: transcoding ?? true,
resolutions: ConfigCommand.getCustomConfigResolutions(resolutions === 'max')
enabled: transcoding,
alwaysTranscodeOriginalResolution,
resolutions: Array.isArray(resolutions)
? ConfigCommand.getCustomConfigResolutions(resolutions)
: ConfigCommand.getConfigResolutions(resolutions === 'max')
}
}
}
@ -246,10 +268,14 @@ export class ConfigCommand extends AbstractCommand {
enableTranscoding (options: {
webVideo?: boolean // default true
hls?: boolean // default true
with0p?: boolean // default false
keepOriginal?: boolean // default false
splitAudioAndVideo?: boolean // default false
resolutions?: 'min' | 'max' | number[] // default 'max'
with0p?: boolean // default false
} = {}) {
const { webVideo = true, hls = true, with0p = false, keepOriginal = false } = options
const { resolutions = 'max', webVideo = true, hls = true, with0p = false, keepOriginal = false, splitAudioAndVideo = false } = options
return this.updateExistingConfig({
newConfig: {
@ -262,25 +288,39 @@ export class ConfigCommand extends AbstractCommand {
allowAudioFiles: true,
allowAdditionalExtensions: true,
resolutions: ConfigCommand.getCustomConfigResolutions(true, with0p),
resolutions: Array.isArray(resolutions)
? ConfigCommand.getCustomConfigResolutions(resolutions)
: ConfigCommand.getConfigResolutions(resolutions === 'max', with0p),
webVideos: {
enabled: webVideo
},
hls: {
enabled: hls
enabled: hls,
splitAudioAndVideo
}
}
}
})
}
setTranscodingConcurrency (concurrency: number) {
return this.updateExistingConfig({
newConfig: {
transcoding: {
concurrency
}
}
})
}
enableMinimumTranscoding (options: {
webVideo?: boolean // default true
hls?: boolean // default true
splitAudioAndVideo?: boolean // default false
keepOriginal?: boolean // default false
} = {}) {
const { webVideo = true, hls = true, keepOriginal = false } = options
const { webVideo = true, hls = true, keepOriginal = false, splitAudioAndVideo = false } = options
return this.updateExistingConfig({
newConfig: {
@ -294,7 +334,7 @@ export class ConfigCommand extends AbstractCommand {
allowAdditionalExtensions: true,
resolutions: {
...ConfigCommand.getCustomConfigResolutions(false),
...ConfigCommand.getConfigResolutions(false),
'240p': true
},
@ -303,7 +343,8 @@ export class ConfigCommand extends AbstractCommand {
enabled: webVideo
},
hls: {
enabled: hls
enabled: hls,
splitAudioAndVideo
}
}
}

View file

@ -1,7 +1,7 @@
import { waitJobs } from './jobs.js'
import { PeerTubeServer } from './server.js'
async function doubleFollow (server1: PeerTubeServer, server2: PeerTubeServer) {
export async function doubleFollow (server1: PeerTubeServer, server2: PeerTubeServer) {
await Promise.all([
server1.follows.follow({ hosts: [ server2.url ] }),
server2.follows.follow({ hosts: [ server1.url ] })
@ -9,12 +9,18 @@ async function doubleFollow (server1: PeerTubeServer, server2: PeerTubeServer) {
// Wait request propagation
await waitJobs([ server1, server2 ])
return true
}
// ---------------------------------------------------------------------------
export function followAll (servers: PeerTubeServer[]) {
const p: Promise<void>[] = []
export {
doubleFollow
for (const server of servers) {
for (const remoteServer of servers) {
if (server === remoteServer) continue
p.push(doubleFollow(server, remoteServer))
}
}
return Promise.all(p)
}

View file

@ -29,7 +29,7 @@ async function waitJobs (
// Check if each server has pending request
for (const server of servers) {
if (process.env.DEBUG) console.log('Checking ' + server.url)
if (process.env.DEBUG) console.log(`${new Date().toISOString()} - Checking ${server.url}`)
for (const state of states) {
@ -45,7 +45,7 @@ async function waitJobs (
pendingRequests = true
if (process.env.DEBUG) {
console.log(jobs)
console.log(`${new Date().toISOString()}`, jobs)
}
}
})
@ -59,7 +59,7 @@ async function waitJobs (
pendingRequests = true
if (process.env.DEBUG) {
console.log('AP messages waiting: ' + obj.activityPubMessagesWaiting)
console.log(`${new Date().toISOString()} - AP messages waiting: ${obj.activityPubMessagesWaiting}`)
}
}
})
@ -73,7 +73,7 @@ async function waitJobs (
pendingRequests = true
if (process.env.DEBUG) {
console.log(job)
console.log(`${new Date().toISOString()}`, job)
}
}
}

View file

@ -4,7 +4,7 @@ import { PeerTubeServer } from '../server/server.js'
export async function setDefaultAccountAvatar (serversArg: PeerTubeServer | PeerTubeServer[], token?: string) {
const servers = arrayify(serversArg)
for (const server of servers) {
await server.users.updateMyAvatar({ fixture: 'avatar.png', token })
}
return Promise.all(
servers.map(s => s.users.updateMyAvatar({ fixture: 'avatar.png', token }))
)
}

View file

@ -2,22 +2,18 @@ import { arrayify } from '@peertube/peertube-core-utils'
import { PeerTubeServer } from '../server/server.js'
export function setDefaultVideoChannel (servers: PeerTubeServer[]) {
const tasks: Promise<any>[] = []
for (const server of servers) {
const p = server.users.getMyInfo()
.then(user => { server.store.channel = user.videoChannels[0] })
tasks.push(p)
}
return Promise.all(tasks)
return Promise.all(
servers.map(s => {
return s.users.getMyInfo()
.then(user => { s.store.channel = user.videoChannels[0] })
})
)
}
export async function setDefaultChannelAvatar (serversArg: PeerTubeServer | PeerTubeServer[], channelName: string = 'root_channel') {
const servers = arrayify(serversArg)
for (const server of servers) {
await server.channels.updateImage({ channelName, fixture: 'avatar.png', type: 'avatar' })
}
return Promise.all(
servers.map(s => s.channels.updateImage({ channelName, fixture: 'avatar.png', type: 'avatar' }))
)
}

View file

@ -167,6 +167,7 @@ export class LiveCommand extends AbstractCommand {
async runAndTestStreamError (options: OverrideCommandOptions & {
videoId: number | string
shouldHaveError: boolean
fixtureName?: string
}) {
const command = await this.sendRTMPStreamInVideo(options)

View file

@ -5,7 +5,7 @@ import ffmpeg, { FfmpegCommand } from 'fluent-ffmpeg'
import truncate from 'lodash-es/truncate.js'
import { PeerTubeServer } from '../server/server.js'
function sendRTMPStream (options: {
export function sendRTMPStream (options: {
rtmpBaseUrl: string
streamKey: string
fixtureName?: string // default video_short.mp4
@ -49,7 +49,7 @@ function sendRTMPStream (options: {
return command
}
function waitFfmpegUntilError (command: FfmpegCommand, successAfterMS = 10000) {
export function waitFfmpegUntilError (command: FfmpegCommand, successAfterMS = 10000) {
return new Promise<void>((res, rej) => {
command.on('error', err => {
return rej(err)
@ -61,7 +61,7 @@ function waitFfmpegUntilError (command: FfmpegCommand, successAfterMS = 10000) {
})
}
async function testFfmpegStreamError (command: FfmpegCommand, shouldHaveError: boolean) {
export async function testFfmpegStreamError (command: FfmpegCommand, shouldHaveError: boolean) {
let error: Error
try {
@ -76,31 +76,39 @@ async function testFfmpegStreamError (command: FfmpegCommand, shouldHaveError: b
if (!shouldHaveError && error) throw error
}
async function stopFfmpeg (command: FfmpegCommand) {
export async function stopFfmpeg (command: FfmpegCommand) {
command.kill('SIGINT')
await wait(500)
}
async function waitUntilLivePublishedOnAllServers (servers: PeerTubeServer[], videoId: string) {
export async function waitUntilLivePublishedOnAllServers (servers: PeerTubeServer[], videoId: string) {
for (const server of servers) {
await server.live.waitUntilPublished({ videoId })
}
}
async function waitUntilLiveWaitingOnAllServers (servers: PeerTubeServer[], videoId: string) {
export async function waitUntilLiveWaitingOnAllServers (servers: PeerTubeServer[], videoId: string) {
for (const server of servers) {
await server.live.waitUntilWaiting({ videoId })
}
}
async function waitUntilLiveReplacedByReplayOnAllServers (servers: PeerTubeServer[], videoId: string) {
export async function waitUntilLiveReplacedByReplayOnAllServers (servers: PeerTubeServer[], videoId: string) {
for (const server of servers) {
await server.live.waitUntilReplacedByReplay({ videoId })
}
}
async function findExternalSavedVideo (server: PeerTubeServer, liveDetails: VideoDetails) {
export async function findExternalSavedVideo (server: PeerTubeServer, liveVideoUUID: string) {
let liveDetails: VideoDetails
try {
liveDetails = await server.videos.getWithToken({ id: liveVideoUUID })
} catch {
return undefined
}
const include = VideoInclude.BLACKLISTED
const privacyOneOf = [ VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.PUBLIC, VideoPrivacy.UNLISTED ]
@ -114,16 +122,3 @@ async function findExternalSavedVideo (server: PeerTubeServer, liveDetails: Vide
return data.find(v => v.name === toFind)
}
export {
sendRTMPStream,
waitFfmpegUntilError,
testFfmpegStreamError,
stopFfmpeg,
waitUntilLivePublishedOnAllServers,
waitUntilLiveReplacedByReplayOnAllServers,
waitUntilLiveWaitingOnAllServers,
findExternalSavedVideo
}

View file

@ -341,6 +341,14 @@ export class VideosCommand extends AbstractCommand {
return data.find(v => v.name === options.name)
}
async findFull (options: OverrideCommandOptions & {
name: string
}) {
const { uuid } = await this.find(options)
return this.get({ id: uuid })
}
// ---------------------------------------------------------------------------
update (options: OverrideCommandOptions & {
@ -662,4 +670,25 @@ export class VideosCommand extends AbstractCommand {
endVideoResumableUpload (options: Parameters<AbstractCommand['endResumableUpload']>[0]) {
return super.endResumableUpload(options)
}
// ---------------------------------------------------------------------------
generateDownload (options: OverrideCommandOptions & {
videoId: number | string
videoFileIds: number[]
query?: Record<string, string>
}) {
const { videoFileIds, videoId, query = {} } = options
const path = '/download/videos/generate/' + videoId
return this.getRequestBody<Buffer>({
...options,
path,
query: { videoFileIds, ...query },
responseType: 'arraybuffer',
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
}