Skip to content

Commit e2f3edb

Browse files
author
Taois
committed
feat: 支持web://弹幕
1 parent c1b0c99 commit e2f3edb

File tree

3 files changed

+245
-8
lines changed

3 files changed

+245
-8
lines changed

dashboard/src/api/modules/module.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ export const parsePlayUrl = async (module, params) => {
252252
playType: 'direct', // 默认直链
253253
url: '',
254254
headers: {}, // 添加headers字段
255+
danmaku: '', // 添加弹幕字段
255256
needParse: false,
256257
needSniff: false,
257258
message: ''
@@ -265,6 +266,7 @@ export const parsePlayUrl = async (module, params) => {
265266
result.playType = 'parse'
266267
result.url = playData.url || playData.play_url || ''
267268
result.headers = parseHeaders(playData.headers || playData.header)
269+
result.danmaku = playData.danmaku || '' // 处理弹幕字段
268270
result.needParse = true
269271
result.qualities = []
270272
result.hasMultipleQualities = false
@@ -319,13 +321,15 @@ export const parsePlayUrl = async (module, params) => {
319321
}
320322

321323
result.headers = parseHeaders(playData.headers || playData.header)
324+
result.danmaku = playData.danmaku || '' // 处理弹幕字段
322325
result.needParse = false
323326
result.needSniff = false
324327
} else if (playData.parse === 1) {
325328
// 需要嗅探
326329
result.playType = 'sniff'
327330
result.url = playData.url || playData.play_url || ''
328331
result.headers = parseHeaders(playData.headers || playData.header)
332+
result.danmaku = playData.danmaku || '' // 处理弹幕字段
329333
result.needSniff = true
330334
result.qualities = []
331335
result.hasMultipleQualities = false
@@ -334,6 +338,7 @@ export const parsePlayUrl = async (module, params) => {
334338
// 默认处理为直链
335339
result.url = playData.url || playData.play_url || playData
336340
result.headers = parseHeaders(playData.headers || playData.header)
341+
result.danmaku = playData.danmaku || '' // 处理弹幕字段
337342
result.qualities = []
338343
result.hasMultipleQualities = false
339344
result.message = '直链播放'
@@ -342,6 +347,7 @@ export const parsePlayUrl = async (module, params) => {
342347
// 如果返回的是字符串,直接作为播放地址
343348
result.url = playData
344349
result.headers = {}
350+
result.danmaku = '' // 字符串类型无弹幕数据
345351
result.qualities = []
346352
result.hasMultipleQualities = false
347353
result.message = '直链播放'

dashboard/src/components/players/ArtVideoPlayer.vue

Lines changed: 233 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ const props = defineProps({
135135
type: Object,
136136
default: () => ({})
137137
},
138+
// 弹幕链接,用于弹幕功能
139+
danmakuUrl: {
140+
type: String,
141+
default: ''
142+
},
138143
// 画质相关属性
139144
qualities: {
140145
type: Array,
@@ -357,19 +362,39 @@ const loadDanmakuData = async (videoUrl) => {
357362
danmakuLoading.value = true
358363
359364
try {
360-
const danmakuUrl = parseDanmakuUrl(videoUrl)
365+
// 优先使用 T4 解析返回的弹幕链接
366+
let danmakuUrl = props.danmakuUrl
367+
368+
// 如果没有 T4 弹幕链接,尝试从视频URL解析
369+
if (!danmakuUrl) {
370+
danmakuUrl = parseDanmakuUrl(videoUrl)
371+
}
372+
361373
if (!danmakuUrl) {
362374
console.log('无法解析弹幕URL,使用默认弹幕数据')
363375
return getDefaultDanmakuData()
364376
}
365377
366-
// 这里可以实现实际的弹幕数据加载逻辑
367-
// const response = await fetch(danmakuUrl)
368-
// const data = await response.json()
369-
// return parseDanmakuData(data)
370-
371-
// 暂时返回默认弹幕数据
372-
return getDefaultDanmakuData()
378+
console.log('弹幕链接:', danmakuUrl)
379+
380+
// 处理不同协议的弹幕链接
381+
if (danmakuUrl.startsWith('http://') || danmakuUrl.startsWith('https://')) {
382+
// HTTP协议:直接请求弹幕数据
383+
console.log('处理HTTP协议弹幕链接:', danmakuUrl)
384+
const response = await fetch(danmakuUrl)
385+
const data = await response.json()
386+
return parseDanmakuData(data)
387+
} else if (danmakuUrl.startsWith('web://')) {
388+
// WEB协议:创建自定义HTML层显示iframe
389+
console.log('处理WEB协议弹幕链接:', danmakuUrl)
390+
const iframeUrl = danmakuUrl.replace('web://', '')
391+
await createDanmakuIframeLayer(iframeUrl)
392+
// WEB协议不返回弹幕数据,而是通过iframe显示
393+
return []
394+
} else {
395+
console.log('未知弹幕协议,使用默认弹幕数据')
396+
return getDefaultDanmakuData()
397+
}
373398
} catch (error) {
374399
console.error('加载弹幕数据失败:', error)
375400
return getDefaultDanmakuData()
@@ -401,6 +426,174 @@ const getDefaultDanmakuData = () => {
401426
]
402427
}
403428
429+
// 解析弹幕数据 - 将HTTP协议返回的数据转换为ArtPlayer弹幕格式
430+
const parseDanmakuData = (data) => {
431+
try {
432+
// 处理不同格式的弹幕数据
433+
if (Array.isArray(data)) {
434+
// 如果已经是数组格式,直接处理
435+
return data.map(item => ({
436+
text: item.text || item.content || '',
437+
time: parseFloat(item.time || item.timestamp || 0),
438+
color: item.color || '#ffffff',
439+
type: item.type || 'right'
440+
})).filter(item => item.text.trim().length > 0)
441+
} else if (data.data && Array.isArray(data.data)) {
442+
// 如果数据在data字段中
443+
return data.data.map(item => ({
444+
text: item.text || item.content || '',
445+
time: parseFloat(item.time || item.timestamp || 0),
446+
color: item.color || '#ffffff',
447+
type: item.type || 'right'
448+
})).filter(item => item.text.trim().length > 0)
449+
} else {
450+
console.warn('未知的弹幕数据格式:', data)
451+
return getDefaultDanmakuData()
452+
}
453+
} catch (error) {
454+
console.error('解析弹幕数据失败:', error)
455+
return getDefaultDanmakuData()
456+
}
457+
}
458+
459+
// 创建弹幕iframe层 - 处理web://协议的弹幕链接
460+
const createDanmakuIframeLayer = async (iframeUrl) => {
461+
try {
462+
if (!artPlayerInstance.value) {
463+
console.warn('ArtPlayer实例不存在,无法创建弹幕iframe层')
464+
return
465+
}
466+
467+
console.log('创建弹幕iframe层:', iframeUrl)
468+
469+
// 移除已存在的弹幕iframe层
470+
removeDanmakuIframeLayer()
471+
472+
// 更新iframe层内容
473+
artPlayerInstance.value.layers.update({
474+
name: 'danmaku-iframe',
475+
html: `
476+
<div class="danmaku-iframe-container" style="
477+
position: absolute;
478+
top: 0;
479+
left: 0;
480+
width: 100%;
481+
height: 100%;
482+
pointer-events: none;
483+
z-index: 10;
484+
">
485+
<iframe
486+
src="${iframeUrl}"
487+
style="
488+
width: 100%;
489+
height: 100%;
490+
border: none;
491+
background: transparent;
492+
pointer-events: auto;
493+
"
494+
frameborder="0"
495+
allowtransparency="true"
496+
></iframe>
497+
</div>
498+
`,
499+
style: {
500+
position: 'absolute',
501+
top: '0',
502+
left: '0',
503+
width: '100%',
504+
height: '100%',
505+
pointerEvents: 'none',
506+
display: 'block',
507+
zIndex: '10'
508+
}
509+
})
510+
511+
console.log('弹幕iframe层创建成功')
512+
} catch (error) {
513+
console.error('创建弹幕iframe层失败:', error)
514+
}
515+
}
516+
517+
// 移除弹幕iframe层
518+
const removeDanmakuIframeLayer = () => {
519+
try {
520+
if (artPlayerInstance.value && artPlayerInstance.value.layers) {
521+
artPlayerInstance.value.layers.update({
522+
name: 'danmaku-iframe',
523+
html: '',
524+
style: {
525+
position: 'absolute',
526+
top: '0',
527+
left: '0',
528+
width: '100%',
529+
height: '100%',
530+
pointerEvents: 'none',
531+
display: 'none',
532+
zIndex: '10'
533+
}
534+
})
535+
}
536+
} catch (error) {
537+
console.error('移除弹幕iframe层失败:', error)
538+
}
539+
}
540+
541+
// 处理弹幕URL - 根据协议类型选择处理方式
542+
const handleDanmakuUrl = async () => {
543+
try {
544+
if (!props.danmakuUrl || !props.danmakuUrl.trim()) {
545+
console.log('没有弹幕URL,使用默认弹幕数据')
546+
return
547+
}
548+
549+
const danmakuUrl = props.danmakuUrl.trim()
550+
console.log('处理弹幕URL:', danmakuUrl)
551+
552+
if (danmakuUrl.startsWith('web://')) {
553+
// 处理web://协议 - 创建iframe层
554+
const iframeUrl = danmakuUrl.substring(6) // 移除 'web://' 前缀
555+
console.log('检测到web://协议,创建iframe层:', iframeUrl)
556+
await createDanmakuIframeLayer(iframeUrl)
557+
} else if (danmakuUrl.startsWith('http://') || danmakuUrl.startsWith('https://')) {
558+
// 处理HTTP协议 - 加载弹幕数据
559+
console.log('检测到HTTP协议,加载弹幕数据:', danmakuUrl)
560+
danmakuLoading.value = true
561+
562+
try {
563+
const response = await fetch(danmakuUrl)
564+
if (!response.ok) {
565+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
566+
}
567+
568+
const data = await response.json()
569+
const parsedData = parseDanmakuData(data)
570+
571+
// 更新弹幕数据
572+
danmakuData.value = parsedData
573+
console.log('弹幕数据加载成功,条数:', parsedData.length)
574+
575+
// 如果弹幕插件已初始化,更新弹幕数据
576+
if (artPlayerInstance.value && artPlayerInstance.value.plugins && artPlayerInstance.value.plugins.artplayerPluginDanmuku) {
577+
artPlayerInstance.value.plugins.artplayerPluginDanmuku.config({
578+
danmuku: parsedData
579+
})
580+
console.log('弹幕插件数据已更新')
581+
}
582+
} catch (error) {
583+
console.error('加载弹幕数据失败:', error)
584+
// 使用默认弹幕数据
585+
danmakuData.value = getDefaultDanmakuData()
586+
} finally {
587+
danmakuLoading.value = false
588+
}
589+
} else {
590+
console.warn('不支持的弹幕URL协议:', danmakuUrl)
591+
}
592+
} catch (error) {
593+
console.error('处理弹幕URL失败:', error)
594+
}
595+
}
596+
404597
// 初始化 ArtPlayer
405598
const initArtPlayer = async (url) => {
406599
if (!artPlayerContainer.value || !url) return
@@ -681,6 +874,20 @@ const initArtPlayer = async (url) => {
681874
hideQualityLayer()
682875
}
683876
}
877+
},
878+
{
879+
name: 'danmaku-iframe',
880+
html: '',
881+
style: {
882+
position: 'absolute',
883+
top: '0',
884+
left: '0',
885+
width: '100%',
886+
height: '100%',
887+
pointerEvents: 'none',
888+
display: 'none',
889+
zIndex: '10'
890+
}
684891
}
685892
],
686893
// 插件配置
@@ -704,6 +911,8 @@ const initArtPlayer = async (url) => {
704911
console.log('ArtPlayer 准备就绪')
705912
// 应用片头片尾设置
706913
applySkipSettings()
914+
// 处理弹幕URL
915+
handleDanmakuUrl()
707916
})
708917
709918
art.on('video:loadstart', () => {
@@ -1689,6 +1898,19 @@ watch(danmakuEnabled, (newEnabled) => {
16891898
localStorage.setItem('danmakuEnabled', newEnabled.toString())
16901899
})
16911900
1901+
// 监听弹幕URL变化,重新处理弹幕
1902+
watch(() => props.danmakuUrl, async (newDanmakuUrl) => {
1903+
console.log('danmakuUrl 变化:', newDanmakuUrl)
1904+
1905+
if (artPlayerInstance.value) {
1906+
// 移除之前的iframe层
1907+
removeDanmakuIframeLayer()
1908+
1909+
// 处理新的弹幕URL
1910+
await handleDanmakuUrl()
1911+
}
1912+
})
1913+
16921914
// 窗口大小变化处理
16931915
const handleResize = () => {
16941916
if (artPlayerContainer.value && artPlayerInstance.value) {
@@ -1748,6 +1970,9 @@ onUnmounted(() => {
17481970
17491971
// 销毁播放器实例
17501972
if (artPlayerInstance.value) {
1973+
// 清理弹幕iframe层
1974+
removeDanmakuIframeLayer()
1975+
17511976
// 清理自定义播放器
17521977
if (artPlayerInstance.value.customPlayer && artPlayerInstance.value.customPlayerFormat) {
17531978
const format = artPlayerInstance.value.customPlayerFormat

dashboard/src/views/VideoDetail.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
:current-episode-index="currentEpisodeIndex"
9999
:auto-next="true"
100100
:headers="parsedHeaders"
101+
:danmaku-url="parsedDanmakuUrl"
101102
:qualities="parsedQualities"
102103
:has-multiple-qualities="hasMultipleQualities"
103104
:initial-quality="initialQuality"
@@ -425,6 +426,8 @@ const showVideoPlayer = ref(false)
425426
const parsedVideoUrl = ref('')
426427
// 解析后的请求头(用于T4接口解析结果)
427428
const parsedHeaders = ref({})
429+
// 解析后的弹幕链接(用于T4接口解析结果)
430+
const parsedDanmakuUrl = ref('')
428431
// 多画质相关数据
429432
const parsedQualities = ref([])
430433
const hasMultipleQualities = ref(false)
@@ -1584,11 +1587,14 @@ const selectEpisode = async (index) => {
15841587
// 普通视频内容
15851588
console.log('启动内置播放器播放直链视频:', parseResult.url)
15861589
console.log('T4解析结果headers:', parseResult.headers)
1590+
console.log('T4解析结果danmaku:', parseResult.danmaku)
15871591
console.log('T4解析结果画质信息:', parseResult.qualities, parseResult.hasMultipleQualities)
15881592
15891593
parsedVideoUrl.value = parseResult.url
15901594
// 提取并存储headers,如果没有headers则使用空对象
15911595
parsedHeaders.value = parseResult.headers || {}
1596+
// 提取并存储弹幕链接,如果没有danmaku则使用空字符串
1597+
parsedDanmakuUrl.value = parseResult.danmaku || ''
15921598
// 提取并存储画质信息
15931599
parsedQualities.value = parseResult.qualities || []
15941600
hasMultipleQualities.value = parseResult.hasMultipleQualities || false

0 commit comments

Comments
 (0)