Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add components: play in mpv #2605

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions registry/dist/components/video/play-in-mpv.js

Large diffs are not rendered by default.

420 changes: 420 additions & 0 deletions registry/lib/components/video/play-in-mpv/PlayMpv.vue

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions registry/lib/components/video/play-in-mpv/Widget.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<template>
<div class="multiple-widgets">
<DefaultWidget
ref="button"
name="MPV播放"
icon="mdi-download"
@mouseover="createDownloadPanel()"
@click="toggleDownloadPanel()"
></DefaultWidget>
</div>
</template>
<script lang="ts">
let panel: { open: boolean } & Vue
export default Vue.extend({
components: {
DefaultWidget: coreApis.ui.DefaultWidget,
},
methods: {
async createDownloadPanel() {
if (!panel) {
const holder = document.createElement('div')
document.body.appendChild(holder)
const PlayMpvPanel = await import('./PlayMpv.vue').then(m => m.default)
panel = new PlayMpvPanel({
propsData: {
triggerElement: this.$refs.button,
},
}).$mount(holder)
}
},
async toggleDownloadPanel() {
if (!panel) {
return
}
panel.open = !panel.open
},
},
})
</script>
188 changes: 188 additions & 0 deletions registry/lib/components/video/play-in-mpv/apis/dash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { bilibiliApi, getJsonWithCredentials } from '@/core/ajax'
import { formData } from '@/core/utils'
import { ascendingSort, descendingSort } from '@/core/utils/sort'
import { allQualities, VideoQuality } from '@/components/video/video-quality'
import { compareQuality } from '../error'
import {
PlayMpvApi, PlayMpvFragment, PlayMpvInfo, PlayMpvInputItem,
} from '../types'

/* spell-checker: disable */

/** dash 格式更明确的扩展名 */
export const DashExtensions = {
video: '.mp4',
audio: '.m4a',
}
/** dash 格式原本的扩展名 */
export const DashFragmentExtension = '.m4s'
/** dash 格式支持的编码类型 */
export type DashCodec = 'AVC/H.264' | 'HEVC/H.265'
export interface Dash {
type: keyof typeof DashExtensions
bandWidth: number
codecs: string
codecId: number
backupUrls: string[]
downloadUrl: string
duration: number
}
export interface AudioDash extends Dash {
type: 'audio'
}
export interface VideoDash extends Dash {
type: 'video'
quality: VideoQuality
frameRate: string
height: number
width: number
videoCodec: DashCodec
}
export interface DashFilters {
video?: (dash: VideoDash) => boolean
audio?: (dash: AudioDash) => boolean
}
const dashToFragment = (dash: Dash): PlayMpvFragment => ({
url: dash.downloadUrl,
backupUrls: dash.backupUrls,
length: dash.duration,
size: Math.trunc((dash.bandWidth * dash.duration) / 8),
extension: DashExtensions[dash.type] ?? DashFragmentExtension,
})
export const dashToFragments = (info: {
videoDashes: VideoDash[]
audioDashes: AudioDash[]
videoCodec: DashCodec
}) => {
const { videoDashes, audioDashes, videoCodec } = info
const results: PlayMpvFragment[] = []
// 画面按照首选编码选择, 若没有相应编码则选择大小较小的编码
if (videoDashes.length !== 0) {
const matchPreferredCodec = (d: VideoDash) => d.videoCodec === videoCodec
if (videoDashes.some(matchPreferredCodec)) {
const dash = videoDashes
.filter(matchPreferredCodec)
.sort(ascendingSort(d => d.bandWidth))[0]
results.push(dashToFragment(dash))
} else {
results.push(dashToFragment(videoDashes.sort(ascendingSort(d => d.bandWidth))[0]))
}
}
if (audioDashes.length !== 0) {
// 声音倒序排, 选择最高音质
const audio = audioDashes.sort(descendingSort(d => d.bandWidth))[0]
results.push(dashToFragment(audio))
}
return results
}
const downloadDash = async (
input: PlayMpvInputItem,
config: {
codec?: DashCodec
filters?: DashFilters
},
) => {
const { codec = 'AVC/H.264', filters } = config
const dashFilters = {
video: () => true,
audio: () => true,
...filters,
}
const { aid, cid, quality } = input
const params = {
avid: aid,
cid,
qn: quality?.value ?? '',
otype: 'json',
fourk: 1,
fnver: 0,
fnval: 2000,
}
const api = `https://api.bilibili.com/x/player/playurl?${formData(params)}`
const data = await bilibiliApi(
getJsonWithCredentials(api),
'获取视频链接失败',
)
if (!data.dash) {
throw new Error('此视频没有 dash 格式, 请改用其他格式.')
}
const currentQuality = allQualities.find(q => q.value === data.quality)
const { duration, video, audio } = data.dash
const videoDashes: VideoDash[] = (video as any[])
.filter((d: any) => d.id === currentQuality.value)
.map((d: any): VideoDash => {
const videoCodec: DashCodec = (() => {
switch (d.codecid) {
case 12:
return 'HEVC/H.265'
default:
case 7:
return 'AVC/H.264'
}
})()
const dash: VideoDash = {
type: 'video',
quality: currentQuality,
width: d.width,
height: d.height,
codecs: d.codecs,
codecId: d.codecid,
bandWidth: d.bandwidth,
frameRate: d.frameRate,
backupUrls: (d.backupUrl || d.backup_url || []).map(
(it: string) => it.replace('http:', 'https:'),
),
downloadUrl: (d.baseUrl || d.base_url || '').replace('http:', 'https:'),
duration,
videoCodec,
}
return dash
})
.filter(d => dashFilters.video(d))
const audioDashes: AudioDash[] = (audio as any[] || []).map((d: any): AudioDash => ({
type: 'audio',
bandWidth: d.bandwidth,
codecs: d.codecs,
codecId: d.codecid,
backupUrls: (d.backupUrl || d.backup_url || []).map(
(it: string) => it.replace('http:', 'https:'),
),
downloadUrl: (d.baseUrl || d.base_url || '').replace('http:', 'https:'),
duration,
})).filter(d => dashFilters.audio(d))
const fragments: PlayMpvFragment[] = dashToFragments({
audioDashes,
videoDashes,
videoCodec: codec,
})
const qualities = (data.accept_quality as number[])
.map(qn => allQualities.find(q => q.value === qn))
.filter(q => q !== undefined)
const info = new PlayMpvInfo({
input,
jsonData: data,
fragments,
qualities,
currentQuality,
})
compareQuality(input, info)
return info
}
export const videoDashAVC: PlayMpvApi = {
name: 'video.dash.avc',
displayName: 'dash (AVC/H.264)',
description: '音画分离的 mp4 格式, 编码为 H.264, 兼容性较好. 下载后可以合并为单个 mp4 文件.',
playMpvInfo: async input => downloadDash(input, { codec: 'AVC/H.264' }),
}
export const videoDashHEVC: PlayMpvApi = {
name: 'video.dash.hevc',
displayName: 'dash (HEVC/H.265)',
description: '音画分离的 mp4 格式, 编码为 H.265, 体积较小, 兼容性较差. 下载后可以合并为单个 mp4 文件.',
playMpvInfo: async input => downloadDash(input, { codec: 'HEVC/H.265' }),
}
export const audioDash: PlayMpvApi = {
name: 'video.dash.audio',
displayName: 'dash (仅音频)',
description: '仅MPV播放中的音频轨道.',
playMpvInfo: async input => downloadDash(input, { filters: { video: () => false } }),
}
64 changes: 64 additions & 0 deletions registry/lib/components/video/play-in-mpv/apis/flv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { bilibiliApi, getJsonWithCredentials } from '@/core/ajax'
import { formData } from '@/core/utils'
import { allQualities } from '@/components/video/video-quality'
import { compareQuality } from '../error'
import {
PlayMpvApi,
PlayMpvFragment,
PlayMpvInfo,
} from '../types'

const parseInfoFromJson = (data: any, extensions: string[]) => {
const getExtension = (index: number) => {
if (extensions.length > index) {
return extensions[index]
}
return extensions[extensions.length - 1]
}
const fragments = data.durl.map((it: any, index: number) => ({
length: it.length,
size: it.size,
url: it.url,
backupUrls: it.backup_url,
extension: getExtension(index),
} as PlayMpvFragment))
const qualities = (data.accept_quality as number[])
.map(qn => allQualities.find(q => q.value === qn))
.filter(q => q !== undefined)
const currentQuality = allQualities.find(q => q.value === data.quality)
return {
fragments,
qualities,
currentQuality,
}
}
/* spell-checker: disable */
export const videoFlv: PlayMpvApi = {
name: 'video.flv',
displayName: 'flv',
description: '使用 flv 格式下载, 兼容 H.264 编码.',
playMpvInfo: async input => {
const { aid, cid, quality } = input
const params = {
avid: aid,
cid,
qn: quality?.value ?? '',
otype: 'json',
fourk: 1,
fnver: 0,
fnval: 0,
}
const api = `https://api.bilibili.com/x/player/playurl?${formData(params)}`
const data = await bilibiliApi(
getJsonWithCredentials(api),
'获取视频链接失败',
)
const info = new PlayMpvInfo({
input,
jsonData: data,
...parseInfoFromJson(data, ['.flv']),
})
compareQuality(input, info)
return info
},
}
23 changes: 23 additions & 0 deletions registry/lib/components/video/play-in-mpv/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { loginRequiredQualities, vipRequiredQualities } from '@/components/video/video-quality'
import { PlayMpvInfo, PlayMpvInputItem } from './types'

export const throwQualityError = (value: number) => {
// 大会员清晰度: 4K 1080P60 1080P+ 720P60
if (vipRequiredQualities.find(q => q.value === value)) {
throw new Error('您选择的清晰度需要大会员, 请更改清晰度后重试.')
}
// 登录后可看清晰度: 1080P 720P
if (loginRequiredQualities.find(q => q.value === value)) {
throw new Error('您选择的清晰度需要先登录.')
}
throw new Error('获取下载链接失败, 请尝试更换清晰度或更换格式.')
}
export const compareQuality = (input: PlayMpvInputItem, info: PlayMpvInfo) => {
if (input.quality && info.currentQuality.value !== input.quality.value) {
if (input.allowQualityDrop) {
console.warn(`'${input.title}' 不支持选择的清晰度${input.quality.displayName}, 已降级为${info.currentQuality.displayName}`)
} else {
throwQualityError(input.quality.value)
}
}
}
26 changes: 26 additions & 0 deletions registry/lib/components/video/play-in-mpv/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ComponentMetadata } from '@/components/types'
import { hasVideo } from '@/core/spin-query'

export const component: ComponentMetadata = {
name: 'playMpv',
displayName: 'MPV播放',
description: '在功能面板中添加MPV播放支持。需本地程序设置支持。配置方法详见:https://github.com/diannaojiang/Bilibili-Playin-Mpv',
entry: none,
reload: none,
unload: none,
widget: {
component: () => import('./Widget.vue').then(m => m.default),
condition: () => hasVideo(),
},
tags: [
componentsTags.video,
],
options: {
basicConfig: {
defaultValue: {},
displayName: '基础配置',
hidden: true,
},
},
// plugin,
}
Loading