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:
parent
e77ba2dfbc
commit
816f346a60
186 changed files with 5748 additions and 2807 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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' }))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue