feat: 视频播放器字幕添加对ass, ssa格式字幕的支持。优化视频插件与文件夹插件的相关逻辑
This commit is contained in:
1 parent
f578072997
commit
3a48fd5643
3 files changed
+194
-57
No files matched your search
@@ -1,8 +1,7 @@
|
|||||||
<!-- 基于naiveui的下拉菜单实现的自定义下拉菜单 -->
|
<!-- 基于naiveui的下拉菜单实现的自定义下拉菜单 -->
|
||||||
<!-- 支持长按和右键呼出下拉菜单 -->
|
<!-- 支持长按和右键呼出下拉菜单 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="main" @contextmenu="handleContextMenu" @mouseleave="handleMainMouseLeave" @touchstart="handleTouchStart"
|
<div class="main" @contextmenu="handleContextMenu" @mouseleave="handleMainMouseLeave">
|
||||||
@touchend="handleTouchEnd">
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
<n-dropdown placement="bottom-start" trigger="manual" :x="x" :y="y" :options="props.options"
|
<n-dropdown placement="bottom-start" trigger="manual" :x="x" :y="y" :options="props.options"
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ const handleItemDragStart = async (
|
|||||||
const items = uniq([item, ...selectedItem.value])
|
const items = uniq([item, ...selectedItem.value])
|
||||||
dataBus.set('dragData_vFiles', items.filter(v => v.type === 'file'))
|
dataBus.set('dragData_vFiles', items.filter(v => v.type === 'file'))
|
||||||
dataBus.set('dragData_vDirectory', items.filter(v => v.type === 'directory'))
|
dataBus.set('dragData_vDirectory', items.filter(v => v.type === 'directory'))
|
||||||
selectedItem.clear()
|
// selectedItem.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDropFiles = async (files: File[]) => {
|
const handleDropFiles = async (files: File[]) => {
|
||||||
@@ -606,4 +606,5 @@ $bottom-bar-height: 60px;
|
|||||||
// position: absolute;
|
// position: absolute;
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }</style>
|
// }
|
||||||
|
</style>
|
||||||
+190
-53
@@ -1,24 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<miao-drop-handler @on-virtual-files="handleDropVFiles">
|
<miao-drop-handler @on-virtual-files="handleDropVFiles">
|
||||||
<div class="player-container">
|
<div class="player-container">
|
||||||
<div ref="xgPlayer" id="xgplayer">
|
<div ref="xgPlayer" id="xgplayer"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</miao-drop-handler>
|
</miao-drop-handler>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { VirtualFile } from '@/class/VirtualDirectory';
|
import { VirtualFile } from '@/class/VirtualDirectory'
|
||||||
import miaoDropHandler from '@/components/miaoDropHandler.vue';
|
import miaoDropHandler from '@/components/miaoDropHandler.vue'
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
// import useViretualPages from '@/hooks/useVirtualPages';
|
// import useViretualPages from '@/hooks/useVirtualPages';
|
||||||
import XGPlayer from 'xgplayer'
|
import XGPlayer from 'xgplayer'
|
||||||
import '@/style/xgplayer.scss'
|
import '@/style/xgplayer.scss'
|
||||||
import TextTrack from 'xgplayer/es/plugins/track'
|
import TextTrack from 'xgplayer/es/plugins/track'
|
||||||
import 'xgplayer/es/plugins/track/index.css'
|
import 'xgplayer/es/plugins/track/index.css'
|
||||||
import axios from 'axios';
|
import axios from 'axios'
|
||||||
import config from '@/config';
|
import { uniq } from 'lodash'
|
||||||
|
// import config from '@/config'
|
||||||
|
|
||||||
// const props = defineProps<{
|
// const props = defineProps<{
|
||||||
// id: number
|
// id: number
|
||||||
@@ -36,29 +35,55 @@ const xgPlayer = ref()
|
|||||||
let _player: XGPlayer
|
let _player: XGPlayer
|
||||||
const { setVideo } = (() => {
|
const { setVideo } = (() => {
|
||||||
return {
|
return {
|
||||||
setVideo: async (videoVFile: VirtualFile, vttVFiles?: VirtualFile[]) => {
|
setVideo: async (
|
||||||
_player && _player.destroy()
|
videoVFile: VirtualFile,
|
||||||
|
textTrackFiles?: VirtualFile[]
|
||||||
|
) => {
|
||||||
|
if(!currentFiles.value.includes(videoVFile)) {
|
||||||
|
currentFiles.value = [videoVFile]
|
||||||
|
}
|
||||||
|
videoVFile || (videoVFile = findVideo(currentFiles.value))
|
||||||
|
textTrackFiles &&
|
||||||
|
textTrackFiles.push(...findTextTrack(currentFiles.value))
|
||||||
|
textTrackFiles = uniq(textTrackFiles)
|
||||||
|
currentFiles.value = [videoVFile, ...(textTrackFiles ?? [])]
|
||||||
const _list = []
|
const _list = []
|
||||||
if (vttVFiles) {
|
if (textTrackFiles) {
|
||||||
for (let i of vttVFiles) {
|
for (let i of textTrackFiles) {
|
||||||
if (i.name.endsWith('srt')) {
|
if (i.name.endsWith('srt')) {
|
||||||
const response = await axios.get(i.url)
|
const response = await axios.get(i.url)
|
||||||
console.log(srtToVtt(response.data))
|
// console.log(srtToVtt(response.data))
|
||||||
const url = setUrl(new Blob([srtToVtt(response.data)]))
|
const url = setUrl(new Blob([srtToVtt(response.data)]))
|
||||||
_list.push({
|
_list.push({
|
||||||
id: i.name,
|
id: i.uid,
|
||||||
url,
|
url,
|
||||||
language: 'zh-cn',
|
// 这个language会覆盖id的设置,导致无法切换字幕
|
||||||
text: i.name,
|
// language: 'zh-cn',
|
||||||
default: i === vttVFiles[0]
|
text: i.name
|
||||||
|
})
|
||||||
|
} else if (i.name.endsWith('ass')) {
|
||||||
|
const response = await axios.get(i.url)
|
||||||
|
// console.log(assToVtt(response.data))
|
||||||
|
const url = setUrl(new Blob([assToVtt(response.data)]))
|
||||||
|
_list.push({
|
||||||
|
id: i.uid,
|
||||||
|
url,
|
||||||
|
text: i.name
|
||||||
|
})
|
||||||
|
} else if (i.name.endsWith('ssa')) {
|
||||||
|
const response = await axios.get(i.url)
|
||||||
|
// console.log(ssaToVtt(response.data))
|
||||||
|
const url = setUrl(new Blob([ssaToVtt(response.data)]))
|
||||||
|
_list.push({
|
||||||
|
id: i.uid,
|
||||||
|
url,
|
||||||
|
text: i.name
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
_list.push({
|
_list.push({
|
||||||
id: i.name,
|
id: i.uid,
|
||||||
url: i.url,
|
url: i.url,
|
||||||
language: 'zh-cn',
|
text: i.name
|
||||||
text: i.name,
|
|
||||||
default: i === vttVFiles[0]
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +91,7 @@ const { setVideo } = (() => {
|
|||||||
const texttrack = {
|
const texttrack = {
|
||||||
list: _list
|
list: _list
|
||||||
}
|
}
|
||||||
|
_player && _player.destroy()
|
||||||
_player = new XGPlayer({
|
_player = new XGPlayer({
|
||||||
plugins: [TextTrack],
|
plugins: [TextTrack],
|
||||||
el: xgPlayer.value,
|
el: xgPlayer.value,
|
||||||
@@ -87,10 +113,9 @@ const { setVideo } = (() => {
|
|||||||
lang: 'zh-cn',
|
lang: 'zh-cn',
|
||||||
playbackRate: [0.1, 0.3, 0.5, 0.75, 1, 1.5, 2, 3, 5, 10],
|
playbackRate: [0.1, 0.3, 0.5, 0.75, 1, 1.5, 2, 3, 5, 10],
|
||||||
playNext: {
|
playNext: {
|
||||||
urlList: [
|
urlList: []
|
||||||
],
|
|
||||||
},
|
},
|
||||||
texttrack,
|
texttrack
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,67 +139,179 @@ const { setUrl, destroyUrl } = (() => {
|
|||||||
|
|
||||||
const handleDropVFiles = (files: VirtualFile[]) => {
|
const handleDropVFiles = (files: VirtualFile[]) => {
|
||||||
setVideo(findVideo(files), findTextTrack(files))
|
setVideo(findVideo(files), findTextTrack(files))
|
||||||
};
|
}
|
||||||
|
|
||||||
function srtToVtt(srt: string) {
|
function srtToVtt(srt: string) {
|
||||||
// 将 SRT 字幕按行分割
|
// 将 SRT 字幕按行分割
|
||||||
const lines = srt.split('\n');
|
const lines = srt.split('\n')
|
||||||
let vtt = 'WEBVTT\n\n'; // VTT 文件的开头
|
let vtt = 'WEBVTT\n\n' // VTT 文件的开头
|
||||||
|
|
||||||
let index = 0;
|
let index = 0
|
||||||
while (index < lines.length) {
|
while (index < lines.length) {
|
||||||
// 跳过空行
|
// 跳过空行
|
||||||
if (lines[index].trim() === '') {
|
if (lines[index].trim() === '') {
|
||||||
index++;
|
index++
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// SRT 中的序号
|
// SRT 中的序号
|
||||||
const sequenceNumber = lines[index].trim();
|
lines[index].trim()
|
||||||
index++;
|
index++
|
||||||
|
|
||||||
// 时间戳
|
// 时间戳
|
||||||
const timeLine = lines[index].trim().replace(/,/g, '.'); // 将逗号替换为点
|
const timeLine = lines[index].trim().replace(/,/g, '.') // 将逗号替换为点
|
||||||
index++;
|
index++
|
||||||
|
|
||||||
// 字幕文本
|
// 字幕文本
|
||||||
let text = '';
|
let text = ''
|
||||||
while (index < lines.length && lines[index].trim() !== '') {
|
while (index < lines.length && lines[index].trim() !== '') {
|
||||||
text += lines[index].trim() + '\n';
|
text += lines[index].trim() + '\n'
|
||||||
index++;
|
index++
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加到 VTT 字幕中
|
// 添加到 VTT 字幕中
|
||||||
vtt += `${timeLine}\n${text.trim()}\n\n`;
|
vtt += `${timeLine}\n${text.trim()}\n\n`
|
||||||
}
|
}
|
||||||
|
|
||||||
return vtt;
|
return vtt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assToVtt(assContent: string) {
|
||||||
|
// 分割 ASS 内容为行
|
||||||
|
const lines = assContent.split('\n')
|
||||||
|
let vttContent = 'WEBVTT\n\n' // VTT 文件头
|
||||||
|
|
||||||
const findVideo = (vFiles: VirtualFile[]) => vFiles.filter(v => {
|
// 遍历每一行,寻找字幕条目
|
||||||
for (let s of ['mp4', 'm3u8', 'hls']) {
|
for (let line of lines) {
|
||||||
if (v.name.endsWith(s)) {
|
// 只处理 [Events] 部分后的 Dialogue 行
|
||||||
return true
|
if (line.startsWith('Dialogue:')) {
|
||||||
|
// 提取时间戳和文本
|
||||||
|
const parts = line.split(',')
|
||||||
|
if (parts.length >= 5) {
|
||||||
|
// 将时间戳格式化为 VTT 格式
|
||||||
|
const startTime = parts[1]
|
||||||
|
.trim()
|
||||||
|
.replace('.', ',')
|
||||||
|
.replace(',', '.') // 替换 . 为 ,,然后再替换为 .
|
||||||
|
const endTime = parts[2]
|
||||||
|
.trim()
|
||||||
|
.replace('.', ',')
|
||||||
|
.replace(',', '.') // 替换 . 为 ,,然后再替换为 .
|
||||||
|
|
||||||
|
// 确保时间戳是两位数格式
|
||||||
|
const formattedStartTime = startTime.replace(
|
||||||
|
/(\d+):(\d+):(\d+)\.(\d+)/,
|
||||||
|
(match: any, h: any, m: any, s: any, ms: any) => {
|
||||||
|
match
|
||||||
|
return `${String(h).padStart(2, '0')}:${String(
|
||||||
|
m
|
||||||
|
).padStart(2, '0')}:${String(s).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}.${String(ms).padStart(3, '0')}`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const formattedEndTime = endTime.replace(
|
||||||
|
/(\d+):(\d+):(\d+)\.(\d+)/,
|
||||||
|
(match: any, h: any, m: any, s: any, ms: any) => {
|
||||||
|
match
|
||||||
|
return `${String(h).padStart(2, '0')}:${String(
|
||||||
|
m
|
||||||
|
).padStart(2, '0')}:${String(s).padStart(
|
||||||
|
2,
|
||||||
|
'0'
|
||||||
|
)}.${String(ms).padStart(3, '0')}`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const rawText = parts.slice(9).join(',').trim() // 提取文本
|
||||||
|
|
||||||
|
// 清理文本中的样式信息
|
||||||
|
const cleanText = rawText
|
||||||
|
.replace(/{[^}]*}/g, '')
|
||||||
|
.replace(/\\N/g, '\n')
|
||||||
|
.trim()
|
||||||
|
|
||||||
|
// 添加到 VTT 内容
|
||||||
|
vttContent += `${formattedStartTime} --> ${formattedEndTime}\n${cleanText}\n\n`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
})[0]
|
|
||||||
|
|
||||||
const findTextTrack = (vFiles: VirtualFile[]) => vFiles.filter(v => {
|
return vttContent.trim() // 返回 VTT 内容,去掉末尾空行
|
||||||
for (let s of ['vtt', 'srt']) {
|
}
|
||||||
if (v.name.endsWith(s)) {
|
|
||||||
return true
|
function ssaToVtt(ssaContent: string): string {
|
||||||
|
function convertSsaTimeToVtt(ssaTime: string): string {
|
||||||
|
const timeParts = ssaTime.split(':')
|
||||||
|
const hours = timeParts[0].padStart(2, '0')
|
||||||
|
const minutes = timeParts[1].padStart(2, '0')
|
||||||
|
const secondsAndMilliseconds = timeParts[2].split('.')
|
||||||
|
const seconds = secondsAndMilliseconds[0].padStart(2, '0')
|
||||||
|
const milliseconds = (secondsAndMilliseconds[1] || '0').padEnd(3, '0')
|
||||||
|
|
||||||
|
return `${hours}:${minutes}:${seconds}.${milliseconds}`
|
||||||
|
}
|
||||||
|
const lines = ssaContent.split('\n')
|
||||||
|
let vttContent = 'WEBVTT\n\n' // VTT文件头
|
||||||
|
let inEvents = false
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// 检查是否进入[Events]部分
|
||||||
|
if (line.startsWith('[Events]')) {
|
||||||
|
inEvents = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果在[Events]部分,处理对话行
|
||||||
|
if (inEvents && line.startsWith('Dialogue:')) {
|
||||||
|
const parts = line.split(',')
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
const startTime = convertSsaTimeToVtt(parts[1].trim())
|
||||||
|
const endTime = convertSsaTimeToVtt(parts[2].trim())
|
||||||
|
const text = parts.slice(9).join(',').trim() // 获取字幕文本
|
||||||
|
|
||||||
|
// 去除样式标签
|
||||||
|
const cleanText = text.replace(/{[^}]*}/g, '').trim()
|
||||||
|
|
||||||
|
// 添加到VTT内容
|
||||||
|
vttContent += `${startTime} --> ${endTime}\n${cleanText}\n\n`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
})
|
return vttContent
|
||||||
|
}
|
||||||
|
|
||||||
|
const findVideo = (vFiles: VirtualFile[]) =>
|
||||||
|
vFiles.filter((v) => {
|
||||||
|
for (let s of ['mp4', 'm3u8', 'hls']) {
|
||||||
|
if (v.name.endsWith(s)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})[0]
|
||||||
|
|
||||||
|
const findTextTrack = (vFiles: VirtualFile[]) =>
|
||||||
|
vFiles.filter((v) => {
|
||||||
|
for (let s of ['vtt', 'srt', 'ass', 'ssa']) {
|
||||||
|
if (v.name.endsWith(s)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (currentFiles.value.length) {
|
if (currentFiles.value.length) {
|
||||||
setVideo(findVideo(currentFiles.value), findTextTrack(currentFiles.value))
|
setVideo(
|
||||||
|
findVideo(currentFiles.value),
|
||||||
|
findTextTrack(currentFiles.value)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
_player.destroy()
|
_player.destroy()
|
||||||
|
|||||||
Reference in new issue
Block a user