Add audio only transcoding tests
This commit is contained in:
parent
6ad88df896
commit
3a149e9f8b
|
@ -75,11 +75,15 @@ class ResolutionMenuButton extends MenuButton {
|
||||||
// Skip auto resolution, we'll add it ourselves
|
// Skip auto resolution, we'll add it ourselves
|
||||||
if (d.id === -1) continue
|
if (d.id === -1) continue
|
||||||
|
|
||||||
|
const label = d.id === 0
|
||||||
|
? this.player.localize('Audio-only')
|
||||||
|
: d.label
|
||||||
|
|
||||||
this.menu.addChild(new ResolutionMenuItem(
|
this.menu.addChild(new ResolutionMenuItem(
|
||||||
this.player_,
|
this.player_,
|
||||||
{
|
{
|
||||||
id: d.id,
|
id: d.id,
|
||||||
label: d.id == 0 ? this.player .localize('Audio-only') : d.label,
|
label,
|
||||||
selected: d.selected,
|
selected: d.selected,
|
||||||
callback: data.qualitySwitchCallback
|
callback: data.qualitySwitchCallback
|
||||||
})
|
})
|
||||||
|
|
|
@ -186,15 +186,15 @@ class WebTorrentPlugin extends Plugin {
|
||||||
this.player.bigPlayButton.hide()
|
this.player.bigPlayButton.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Audio-only (resolutionId == 0) gets special treatment
|
// Audio-only (resolutionId === 0) gets special treatment
|
||||||
if (resolutionId > 0) {
|
if (resolutionId === 0) {
|
||||||
// Hide poster to have black background
|
|
||||||
this.player.removeClass('vjs-playing-audio-only-content')
|
|
||||||
this.player.posterImage.hide()
|
|
||||||
} else {
|
|
||||||
// Audio-only: show poster, do not auto-hide controls
|
// Audio-only: show poster, do not auto-hide controls
|
||||||
this.player.addClass('vjs-playing-audio-only-content')
|
this.player.addClass('vjs-playing-audio-only-content')
|
||||||
this.player.posterImage.show()
|
this.player.posterImage.show()
|
||||||
|
} else {
|
||||||
|
// Hide poster to have black background
|
||||||
|
this.player.removeClass('vjs-playing-audio-only-content')
|
||||||
|
this.player.posterImage.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
const newVideoFile = this.videoFiles.find(f => f.resolution.id === resolutionId)
|
const newVideoFile = this.videoFiles.find(f => f.resolution.id === resolutionId)
|
||||||
|
|
|
@ -72,7 +72,7 @@ transcoding:
|
||||||
allow_audio_files: false
|
allow_audio_files: false
|
||||||
threads: 2
|
threads: 2
|
||||||
resolutions:
|
resolutions:
|
||||||
0p: true
|
0p: false
|
||||||
240p: true
|
240p: true
|
||||||
360p: true
|
360p: true
|
||||||
480p: true
|
480p: true
|
||||||
|
|
|
@ -357,7 +357,7 @@ function convertCustomConfigBody (body: CustomConfig) {
|
||||||
function keyConverter (k: string) {
|
function keyConverter (k: string) {
|
||||||
// Transcoding resolutions exception
|
// Transcoding resolutions exception
|
||||||
if (/^\d{3,4}p$/.exec(k)) return k
|
if (/^\d{3,4}p$/.exec(k)) return k
|
||||||
if (/^0p$/.exec(k)) return k
|
if (k === '0p') return k
|
||||||
|
|
||||||
return snakeCase(k)
|
return snakeCase(k)
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,15 +35,9 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
|
||||||
async function getVideoFileSize (path: string) {
|
async function getVideoFileSize (path: string) {
|
||||||
const videoStream = await getVideoStreamFromFile(path)
|
const videoStream = await getVideoStreamFromFile(path)
|
||||||
|
|
||||||
return videoStream == null
|
return videoStream === null
|
||||||
? {
|
? { width: 0, height: 0 }
|
||||||
width: 0,
|
: { width: videoStream.width, height: videoStream.height }
|
||||||
height: 0
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
width: videoStream.width,
|
|
||||||
height: videoStream.height
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVideoFileResolution (path: string) {
|
async function getVideoFileResolution (path: string) {
|
||||||
|
@ -57,13 +51,10 @@ async function getVideoFileResolution (path: string) {
|
||||||
|
|
||||||
async function getVideoFileFPS (path: string) {
|
async function getVideoFileFPS (path: string) {
|
||||||
const videoStream = await getVideoStreamFromFile(path)
|
const videoStream = await getVideoStreamFromFile(path)
|
||||||
|
if (videoStream === null) return 0
|
||||||
if (videoStream == null) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
|
for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
|
||||||
const valuesText: string = videoStream[key]
|
const valuesText: string = videoStream[ key ]
|
||||||
if (!valuesText) continue
|
if (!valuesText) continue
|
||||||
|
|
||||||
const [ frames, seconds ] = valuesText.split('/')
|
const [ frames, seconds ] = valuesText.split('/')
|
||||||
|
@ -128,7 +119,7 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 'split-audio'
|
type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio'
|
||||||
|
|
||||||
interface BaseTranscodeOptions {
|
interface BaseTranscodeOptions {
|
||||||
type: TranscodeOptionsType
|
type: TranscodeOptionsType
|
||||||
|
@ -159,11 +150,15 @@ interface MergeAudioTranscodeOptions extends BaseTranscodeOptions {
|
||||||
audioPath: string
|
audioPath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SplitAudioTranscodeOptions extends BaseTranscodeOptions {
|
interface OnlyAudioTranscodeOptions extends BaseTranscodeOptions {
|
||||||
type: 'split-audio'
|
type: 'only-audio'
|
||||||
}
|
}
|
||||||
|
|
||||||
type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | SplitAudioTranscodeOptions | QuickTranscodeOptions
|
type TranscodeOptions = HLSTranscodeOptions
|
||||||
|
| VideoTranscodeOptions
|
||||||
|
| MergeAudioTranscodeOptions
|
||||||
|
| OnlyAudioTranscodeOptions
|
||||||
|
| QuickTranscodeOptions
|
||||||
|
|
||||||
function transcode (options: TranscodeOptions) {
|
function transcode (options: TranscodeOptions) {
|
||||||
return new Promise<void>(async (res, rej) => {
|
return new Promise<void>(async (res, rej) => {
|
||||||
|
@ -177,8 +172,8 @@ function transcode (options: TranscodeOptions) {
|
||||||
command = await buildHLSCommand(command, options)
|
command = await buildHLSCommand(command, options)
|
||||||
} else if (options.type === 'merge-audio') {
|
} else if (options.type === 'merge-audio') {
|
||||||
command = await buildAudioMergeCommand(command, options)
|
command = await buildAudioMergeCommand(command, options)
|
||||||
} else if (options.type === 'split-audio') {
|
} else if (options.type === 'only-audio') {
|
||||||
command = await buildAudioSplitCommand(command, options)
|
command = await buildOnlyAudioCommand(command, options)
|
||||||
} else {
|
} else {
|
||||||
command = await buildx264Command(command, options)
|
command = await buildx264Command(command, options)
|
||||||
}
|
}
|
||||||
|
@ -220,7 +215,7 @@ async function canDoQuickTranscode (path: string): Promise<boolean> {
|
||||||
if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
|
if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
|
||||||
if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false
|
if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false
|
||||||
|
|
||||||
// check audio params (if audio stream exists)
|
// check audio params (if audio stream exists)
|
||||||
if (parsedAudio.audioStream) {
|
if (parsedAudio.audioStream) {
|
||||||
if (parsedAudio.audioStream[ 'codec_name' ] !== 'aac') return false
|
if (parsedAudio.audioStream[ 'codec_name' ] !== 'aac') return false
|
||||||
|
|
||||||
|
@ -293,8 +288,8 @@ async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: M
|
||||||
return command
|
return command
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildAudioSplitCommand (command: ffmpeg.FfmpegCommand, options: SplitAudioTranscodeOptions) {
|
async function buildOnlyAudioCommand (command: ffmpeg.FfmpegCommand, options: OnlyAudioTranscodeOptions) {
|
||||||
command = await presetAudioSplit(command)
|
command = await presetOnlyAudio(command)
|
||||||
|
|
||||||
return command
|
return command
|
||||||
}
|
}
|
||||||
|
@ -350,9 +345,7 @@ function getVideoStreamFromFile (path: string) {
|
||||||
if (err) return rej(err)
|
if (err) return rej(err)
|
||||||
|
|
||||||
const videoStream = metadata.streams.find(s => s.codec_type === 'video')
|
const videoStream = metadata.streams.find(s => s.codec_type === 'video')
|
||||||
//if (!videoStream) return rej(new Error('Cannot find video stream of ' + path))
|
return res(videoStream || null)
|
||||||
|
|
||||||
return res(videoStream)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -384,7 +377,7 @@ async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, input: string,
|
||||||
* A toolbox to play with audio
|
* A toolbox to play with audio
|
||||||
*/
|
*/
|
||||||
namespace audio {
|
namespace audio {
|
||||||
export const get = (option: string) => {
|
export const get = (videoPath: string) => {
|
||||||
// without position, ffprobe considers the last input only
|
// without position, ffprobe considers the last input only
|
||||||
// we make it consider the first input only
|
// we make it consider the first input only
|
||||||
// if you pass a file path to pos, then ffprobe acts on that file directly
|
// if you pass a file path to pos, then ffprobe acts on that file directly
|
||||||
|
@ -394,7 +387,7 @@ namespace audio {
|
||||||
if (err) return rej(err)
|
if (err) return rej(err)
|
||||||
|
|
||||||
if ('streams' in data) {
|
if ('streams' in data) {
|
||||||
const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio')
|
const audioStream = data.streams.find(stream => stream[ 'codec_type' ] === 'audio')
|
||||||
if (audioStream) {
|
if (audioStream) {
|
||||||
return res({
|
return res({
|
||||||
absolutePath: data.format.filename,
|
absolutePath: data.format.filename,
|
||||||
|
@ -406,7 +399,7 @@ namespace audio {
|
||||||
return res({ absolutePath: data.format.filename })
|
return res({ absolutePath: data.format.filename })
|
||||||
}
|
}
|
||||||
|
|
||||||
return ffmpeg.ffprobe(option, parseFfprobe)
|
return ffmpeg.ffprobe(videoPath, parseFfprobe)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -506,8 +499,7 @@ async function presetCopy (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.Ffmpeg
|
||||||
.audioCodec('copy')
|
.audioCodec('copy')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function presetOnlyAudio (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.FfmpegCommand> {
|
||||||
async function presetAudioSplit (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.FfmpegCommand> {
|
|
||||||
return command
|
return command
|
||||||
.format('mp4')
|
.format('mp4')
|
||||||
.audioCodec('copy')
|
.audioCodec('copy')
|
||||||
|
|
|
@ -83,51 +83,18 @@ async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoR
|
||||||
|
|
||||||
const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
|
const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
|
||||||
? {
|
? {
|
||||||
type: 'split-audio' as 'split-audio',
|
type: 'only-audio' as 'only-audio',
|
||||||
inputPath: videoInputPath,
|
inputPath: videoInputPath,
|
||||||
outputPath: videoTranscodedPath,
|
outputPath: videoTranscodedPath,
|
||||||
resolution,
|
resolution
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
type: 'video' as 'video',
|
type: 'video' as 'video',
|
||||||
inputPath: videoInputPath,
|
inputPath: videoInputPath,
|
||||||
outputPath: videoTranscodedPath,
|
outputPath: videoTranscodedPath,
|
||||||
resolution,
|
resolution,
|
||||||
isPortraitMode: isPortrait
|
isPortraitMode: isPortrait
|
||||||
}
|
}
|
||||||
|
|
||||||
await transcode(transcodeOptions)
|
|
||||||
|
|
||||||
return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract audio into a separate audio-only mp4.
|
|
||||||
*/
|
|
||||||
async function splitAudioFile (video: MVideoWithFile) {
|
|
||||||
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
|
|
||||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
|
||||||
const extname = '.mp4'
|
|
||||||
const resolution = VideoResolution.H_NOVIDEO
|
|
||||||
|
|
||||||
// We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
|
|
||||||
const videoInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile()))
|
|
||||||
|
|
||||||
const newVideoFile = new VideoFileModel({
|
|
||||||
resolution,
|
|
||||||
extname,
|
|
||||||
size: 0,
|
|
||||||
videoId: video.id
|
|
||||||
})
|
|
||||||
const videoOutputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(newVideoFile))
|
|
||||||
const videoTranscodedPath = join(transcodeDirectory, video.getVideoFilename(newVideoFile))
|
|
||||||
|
|
||||||
const transcodeOptions = {
|
|
||||||
type: 'split-audio' as 'split-audio',
|
|
||||||
inputPath: videoInputPath,
|
|
||||||
outputPath: videoTranscodedPath,
|
|
||||||
resolution
|
|
||||||
}
|
|
||||||
|
|
||||||
await transcode(transcodeOptions)
|
await transcode(transcodeOptions)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
/* tslint:disable:no-unused-expression */
|
||||||
|
|
||||||
|
import * as chai from 'chai'
|
||||||
|
import 'mocha'
|
||||||
|
import {
|
||||||
|
checkDirectoryIsEmpty,
|
||||||
|
checkSegmentHash,
|
||||||
|
checkTmpIsEmpty,
|
||||||
|
cleanupTests,
|
||||||
|
doubleFollow,
|
||||||
|
flushAndRunMultipleServers,
|
||||||
|
getPlaylist,
|
||||||
|
getVideo, makeGetRequest, makeRawRequest,
|
||||||
|
removeVideo, root,
|
||||||
|
ServerInfo,
|
||||||
|
setAccessTokensToServers, updateCustomSubConfig,
|
||||||
|
updateVideo,
|
||||||
|
uploadVideo,
|
||||||
|
waitJobs, webtorrentAdd
|
||||||
|
} from '../../../../shared/extra-utils'
|
||||||
|
import { VideoDetails } from '../../../../shared/models/videos'
|
||||||
|
import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants'
|
||||||
|
import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution, audio, getVideoFileSize } from '@server/helpers/ffmpeg-utils'
|
||||||
|
|
||||||
|
const expect = chai.expect
|
||||||
|
|
||||||
|
describe('Test audio only video transcoding', function () {
|
||||||
|
let servers: ServerInfo[] = []
|
||||||
|
let videoUUID: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(120000)
|
||||||
|
|
||||||
|
const configOverride = {
|
||||||
|
transcoding: {
|
||||||
|
enabled: true,
|
||||||
|
resolutions: {
|
||||||
|
'0p': true,
|
||||||
|
'240p': true,
|
||||||
|
'360p': false,
|
||||||
|
'480p': false,
|
||||||
|
'720p': false,
|
||||||
|
'1080p': false,
|
||||||
|
'2160p': false
|
||||||
|
},
|
||||||
|
hls: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
webtorrent: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
servers = await flushAndRunMultipleServers(2, configOverride)
|
||||||
|
|
||||||
|
// Get the access tokens
|
||||||
|
await setAccessTokensToServers(servers)
|
||||||
|
|
||||||
|
// Server 1 and server 2 follow each other
|
||||||
|
await doubleFollow(servers[0], servers[1])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should upload a video and transcode it', async function () {
|
||||||
|
this.timeout(120000)
|
||||||
|
|
||||||
|
const resUpload = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'audio only'})
|
||||||
|
videoUUID = resUpload.body.video.uuid
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
for (const server of servers) {
|
||||||
|
const res = await getVideo(server.url, videoUUID)
|
||||||
|
const video: VideoDetails = res.body
|
||||||
|
|
||||||
|
expect(video.streamingPlaylists).to.have.lengthOf(1)
|
||||||
|
|
||||||
|
for (const files of [ video.files, video.streamingPlaylists[0].files ]) {
|
||||||
|
expect(files).to.have.lengthOf(3)
|
||||||
|
expect(files[0].resolution.id).to.equal(720)
|
||||||
|
expect(files[1].resolution.id).to.equal(240)
|
||||||
|
expect(files[2].resolution.id).to.equal(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('0p transcoded video should not have video', async function () {
|
||||||
|
const paths = [
|
||||||
|
join(root(), 'test' + servers[ 0 ].internalServerNumber, 'videos', videoUUID + '-0.mp4'),
|
||||||
|
join(root(), 'test' + servers[ 0 ].internalServerNumber, 'streaming-playlists', 'hls', videoUUID, videoUUID + '-0-fragmented.mp4')
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const path of paths) {
|
||||||
|
const { audioStream } = await audio.get(path)
|
||||||
|
expect(audioStream[ 'codec_name' ]).to.be.equal('aac')
|
||||||
|
expect(audioStream[ 'bit_rate' ]).to.be.at.most(384 * 8000)
|
||||||
|
|
||||||
|
const size = await getVideoFileSize(path)
|
||||||
|
expect(size.height).to.equal(0)
|
||||||
|
expect(size.width).to.equal(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await cleanupTests(servers)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,3 +1,4 @@
|
||||||
|
import './audio-only'
|
||||||
import './multiple-servers'
|
import './multiple-servers'
|
||||||
import './services'
|
import './services'
|
||||||
import './single-server'
|
import './single-server'
|
||||||
|
|
Loading…
Reference in New Issue