Skip to content

Commit

Permalink
Support multiple audio tracks and AV1 for Invidious by using the loca…
Browse files Browse the repository at this point in the history
…l API DASH manifest generator (#3942)

* Support multiple audio tracks and AV1 for Invidious by using the local API DASH manifest generator

* Upgrade YouTube.js to 6.2.0 to fix default track selection

* Fix audio formats

* Use Intl.DisplayNames to get the language names

* Simplify returns

Co-authored-by: ChunkyProgrammer <[email protected]>

---------

Co-authored-by: ChunkyProgrammer <[email protected]>
  • Loading branch information
absidue and ChunkyProgrammer authored Sep 25, 2023
1 parent 112a76e commit d91f82f
Show file tree
Hide file tree
Showing 3 changed files with 276 additions and 76 deletions.
2 changes: 1 addition & 1 deletion src/renderer/components/ft-video-player/ft-video-player.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const EXPECTED_PLAY_RELATED_ERROR_MESSAGES = [
// videojs-http-streaming calls this hook everytime it makes a request,
// so we can use it to convert the Range header into the range query parameter for the streaming URLs
videojs.Vhs.xhr.beforeRequest = (options) => {
if (store.getters.getProxyVideos) {
if (store.getters.getProxyVideos && !options.uri.startsWith('data:application/dash+xml')) {
const { uri } = options
options.uri = getProxyUrl(uri)
}
Expand Down
90 changes: 85 additions & 5 deletions src/renderer/helpers/api/invidious.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import store from '../../store/index'
import { stripHTML, toLocalePublicationString } from '../utils'
import { isNullOrEmpty } from '../strings'
import autolinker from 'autolinker'
import { FormatUtils, Misc, Player } from 'youtubei.js'

function getCurrentInstance() {
return store.getters.getCurrentInvidiousInstance
Expand Down Expand Up @@ -318,11 +319,13 @@ export function filterInvidiousFormats(formats, allowAv1 = false) {
// Which is caused by Invidious API limitation on AV1 formats (see related issues)
// Commented code to be restored after Invidious issue fixed
//
// if (allowAv1 && av1Formats.length > 0) {
// return [...audioFormats, ...av1Formats]
// } else {
// return [...audioFormats, ...h264Formats]
// }
// As we generate our own DASH manifest (using YouTube.js) for multiple audio track support in Electron,
// we can allow AV1 in that situation. If we aren't in electron,
// we still can't use them until Invidious fixes the issue on their side
if (process.env.IS_ELECTRON && allowAv1 && av1Formats.length > 0) {
return [...audioFormats, ...av1Formats]
}

return [...audioFormats, ...h264Formats]
}

Expand All @@ -337,3 +340,80 @@ export async function getHashtagInvidious(hashtag, page) {
const response = await invidiousAPICall(payload)
return response.results
}

/**
* Generates a DASH manifest locally from Invidious' adaptive formats and manifest,
* doing so allows us to support multiple audio tracks, which Invidious doesn't support yet
* @param {import('youtubei.js').Misc.Format[]} formats
* @param {string=} invidiousInstance the formats will be proxied through the specified instance, when one is provided
*/
export async function generateInvidiousDashManifestLocally(formats, invidiousInstance) {
// create a dummy player, as deciphering requires making requests to YouTube,
// which we want to avoid when Invidious is selected as the backend
const player = new Player()
player.decipher = (url) => url

let urlTransformer

if (invidiousInstance) {
/**
* @param {URL} url
*/
urlTransformer = (url) => {
return new URL(url.toString().replace(url.origin, invidiousInstance))
}
}

return await FormatUtils.toDash({
adaptive_formats: formats
}, urlTransformer, undefined, undefined, player)
}

export function convertInvidiousToLocalFormat(format) {
const [initStart, initEnd] = format.init.split('-')
const [indexStart, indexEnd] = format.index.split('-')

const duration = parseInt(parseFloat(new URL(format.url).searchParams.get('dur')) * 1000)

// only converts the properties that are needed to generate a DASH manifest with YouTube.js
// audioQuality and qualityLabel don't go inside the DASH manifest, but are used by YouTube.js
// to determine whether a format is an audio or video stream respectively.

/** @type {import('./local').LocalFormat} */
const localFormat = new Misc.Format({
itag: format.itag,
mimeType: format.type,
bitrate: format.bitrate,
width: format.width,
height: format.height,
initRange: {
start: initStart,
end: initEnd
},
indexRange: {
start: indexStart,
end: indexEnd
},
// lastModified: format.lmt,
// contentLength: format.clen,
url: format.url,
approxDurationMs: duration,
...(format.type.startsWith('audio/')
? {
audioQuality: format.audioQuality,
audioSampleRate: format.audioSampleRate,
audioChannels: format.audioChannels
}
: {
fps: format.fps,
qualityLabel: format.qualityLabel,
colorInfo: format.colorInfo
})
})

// Adding freeTubeUrl allows us to reuse the code,
// to generate the audio tracks for audio only mode, that we use for the local API
localFormat.freeTubeUrl = format.url

return localFormat
}
Loading

0 comments on commit d91f82f

Please sign in to comment.