diff --git a/client/src/assets/player/videojs-components/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts index 445b14b2b..86be03af7 100644 --- a/client/src/assets/player/videojs-components/resolution-menu-button.ts +++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts @@ -75,11 +75,15 @@ class ResolutionMenuButton extends MenuButton { // Skip auto resolution, we'll add it ourselves if (d.id === -1) continue + const label = d.id === 0 + ? this.player.localize('Audio-only') + : d.label + this.menu.addChild(new ResolutionMenuItem( this.player_, { id: d.id, - label: d.id == 0 ? this.player .localize('Audio-only') : d.label, + label, selected: d.selected, callback: data.qualitySwitchCallback }) diff --git a/client/src/assets/player/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts index 007fc58cc..8b5690cea 100644 --- a/client/src/assets/player/webtorrent/webtorrent-plugin.ts +++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts @@ -186,15 +186,15 @@ class WebTorrentPlugin extends Plugin { this.player.bigPlayButton.hide() } - // Audio-only (resolutionId == 0) gets special treatment - if (resolutionId > 0) { - // Hide poster to have black background - this.player.removeClass('vjs-playing-audio-only-content') - this.player.posterImage.hide() - } else { + // Audio-only (resolutionId === 0) gets special treatment + if (resolutionId === 0) { // Audio-only: show poster, do not auto-hide controls this.player.addClass('vjs-playing-audio-only-content') 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) diff --git a/config/test.yaml b/config/test.yaml index eedd28537..3ab391504 100644 --- a/config/test.yaml +++ b/config/test.yaml @@ -72,7 +72,7 @@ transcoding: allow_audio_files: false threads: 2 resolutions: - 0p: true + 0p: false 240p: true 360p: true 480p: true diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 8a00f9835..c593fa302 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts @@ -357,7 +357,7 @@ function convertCustomConfigBody (body: CustomConfig) { function keyConverter (k: string) { // Transcoding resolutions exception if (/^\d{3,4}p$/.exec(k)) return k - if (/^0p$/.exec(k)) return k + if (k === '0p') return k return snakeCase(k) } diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 2d9ce2bfa..ff80991b2 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts @@ -35,15 +35,9 @@ function computeResolutionsToTranscode (videoFileHeight: number) { async function getVideoFileSize (path: string) { const videoStream = await getVideoStreamFromFile(path) - return videoStream == null - ? { - width: 0, - height: 0 - } - : { - width: videoStream.width, - height: videoStream.height - } + return videoStream === null + ? { width: 0, height: 0 } + : { width: videoStream.width, height: videoStream.height } } async function getVideoFileResolution (path: string) { @@ -57,13 +51,10 @@ async function getVideoFileResolution (path: string) { async function getVideoFileFPS (path: string) { 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' ]) { - const valuesText: string = videoStream[key] + const valuesText: string = videoStream[ key ] if (!valuesText) continue 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 { type: TranscodeOptionsType @@ -159,11 +150,15 @@ interface MergeAudioTranscodeOptions extends BaseTranscodeOptions { audioPath: string } -interface SplitAudioTranscodeOptions extends BaseTranscodeOptions { - type: 'split-audio' +interface OnlyAudioTranscodeOptions extends BaseTranscodeOptions { + type: 'only-audio' } -type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | SplitAudioTranscodeOptions | QuickTranscodeOptions +type TranscodeOptions = HLSTranscodeOptions + | VideoTranscodeOptions + | MergeAudioTranscodeOptions + | OnlyAudioTranscodeOptions + | QuickTranscodeOptions function transcode (options: TranscodeOptions) { return new Promise(async (res, rej) => { @@ -177,8 +172,8 @@ function transcode (options: TranscodeOptions) { command = await buildHLSCommand(command, options) } else if (options.type === 'merge-audio') { command = await buildAudioMergeCommand(command, options) - } else if (options.type === 'split-audio') { - command = await buildAudioSplitCommand(command, options) + } else if (options.type === 'only-audio') { + command = await buildOnlyAudioCommand(command, options) } else { command = await buildx264Command(command, options) } @@ -220,7 +215,7 @@ async function canDoQuickTranscode (path: string): Promise { if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) 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[ 'codec_name' ] !== 'aac') return false @@ -293,8 +288,8 @@ async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: M return command } -async function buildAudioSplitCommand (command: ffmpeg.FfmpegCommand, options: SplitAudioTranscodeOptions) { - command = await presetAudioSplit(command) +async function buildOnlyAudioCommand (command: ffmpeg.FfmpegCommand, options: OnlyAudioTranscodeOptions) { + command = await presetOnlyAudio(command) return command } @@ -350,9 +345,7 @@ function getVideoStreamFromFile (path: string) { if (err) return rej(err) 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) + return res(videoStream || null) }) }) } @@ -384,7 +377,7 @@ async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, input: string, * A toolbox to play with audio */ namespace audio { - export const get = (option: string) => { + export const get = (videoPath: string) => { // without position, ffprobe considers the last 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 @@ -394,7 +387,7 @@ namespace audio { if (err) return rej(err) 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) { return res({ absolutePath: data.format.filename, @@ -406,7 +399,7 @@ namespace audio { 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 { +async function presetOnlyAudio (command: ffmpeg.FfmpegCommand): Promise { return command .format('mp4') .audioCodec('copy') diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index 9dd54837f..ab5200936 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts @@ -83,51 +83,18 @@ async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoR const transcodeOptions = resolution === VideoResolution.H_NOVIDEO ? { - type: 'split-audio' as 'split-audio', - inputPath: videoInputPath, - outputPath: videoTranscodedPath, - resolution, - } + type: 'only-audio' as 'only-audio', + inputPath: videoInputPath, + outputPath: videoTranscodedPath, + resolution + } : { - type: 'video' as 'video', - inputPath: videoInputPath, - outputPath: videoTranscodedPath, - resolution, - 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 - } + type: 'video' as 'video', + inputPath: videoInputPath, + outputPath: videoTranscodedPath, + resolution, + isPortraitMode: isPortrait + } await transcode(transcodeOptions) diff --git a/server/tests/api/videos/audio-only.ts b/server/tests/api/videos/audio-only.ts new file mode 100644 index 000000000..1ccae4351 --- /dev/null +++ b/server/tests/api/videos/audio-only.ts @@ -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) + }) +}) diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts index 72e6061bb..4d35d3b7c 100644 --- a/server/tests/api/videos/index.ts +++ b/server/tests/api/videos/index.ts @@ -1,3 +1,4 @@ +import './audio-only' import './multiple-servers' import './services' import './single-server'