diff --git a/package.json b/package.json index 97a4706..9c953d7 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "dev": "vite", "build": "vite build && electron-builder --linux --mac --win", "build:mac": "vite build && electron-builder --mac", - "preview": "vite preview" + "preview": "vite preview", + "postinstall": "patch-package" }, "build": { "asar": true, @@ -103,6 +104,7 @@ "electron-builder": "^24.13.3", "eslint": "^9.9.1", "naive-ui": "^2.41.0", + "patch-package": "^8.0.1", "prettier": "^3.3.3", "sass": "^1.78.0", "typescript": "^5.5.3", diff --git a/patches/guacamole-common-js-jumpserver+1.1.0-c.patch b/patches/guacamole-common-js-jumpserver+1.1.0-c.patch new file mode 100644 index 0000000..0e76148 --- /dev/null +++ b/patches/guacamole-common-js-jumpserver+1.1.0-c.patch @@ -0,0 +1,72 @@ +diff --git a/node_modules/guacamole-common-js-jumpserver/dist/guacamole-common.js b/node_modules/guacamole-common-js-jumpserver/dist/guacamole-common.js +index 542c77b..4d9e4bf 100644 +--- a/node_modules/guacamole-common-js-jumpserver/dist/guacamole-common.js ++++ b/node_modules/guacamole-common-js-jumpserver/dist/guacamole-common.js +@@ -10679,6 +10679,14 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { + */ + var seekTimeout = null; + ++ /** ++ * 播放倍速,1 为正常速度。 ++ * ++ * @private ++ * @type {Number} ++ */ ++ var playbackSpeed = 1; ++ + // Start playback client connected + playbackClient.connect(); + +@@ -10931,8 +10939,8 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { + var next = frames[currentFrame + 1]; + + // Calculate the real timestamp corresponding to when the next +- // frame begins +- var nextRealTimestamp = next.timestamp - startVideoTimestamp + startRealTimestamp; ++ // frame begins (scaled by playback speed) ++ var nextRealTimestamp = (next.timestamp - startVideoTimestamp) / playbackSpeed + startRealTimestamp; + + // Calculate the relative delay between the current time and + // the next frame start +@@ -11167,6 +11175,41 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { + + }; + ++ /** ++ * 设置播放倍速。播放过程中切换时会重新校准时间轴,避免进度跳变。 ++ * ++ * @param {Number} rate ++ * 倍速值,有效范围 0.25 ~ 4。 ++ */ ++ this.setSpeed = function setSpeed(rate) { ++ ++ var newRate = Math.max(0.25, Math.min(rate, 4)); ++ if (newRate === playbackSpeed) ++ return; ++ ++ var wasPlaying = recording.isPlaying(); ++ if (wasPlaying) { ++ abortSeek(); ++ startVideoTimestamp = frames[currentFrame].timestamp; ++ startRealTimestamp = new Date().getTime(); ++ playbackSpeed = newRate; ++ continuePlayback(); ++ } ++ else ++ playbackSpeed = newRate; ++ ++ }; ++ ++ /** ++ * 获取当前播放倍速。 ++ * ++ * @returns {Number} ++ * 当前倍速值。 ++ */ ++ this.getSpeed = function getSpeed() { ++ return playbackSpeed; ++ }; ++ + }; + + /** diff --git a/src/lang/modules/en.ts b/src/lang/modules/en.ts index 8953f5d..b1f02ab 100644 --- a/src/lang/modules/en.ts +++ b/src/lang/modules/en.ts @@ -14,5 +14,6 @@ export default { emptyCommand: 'The command list is empty', videoInformation: 'Video Information: ', toggleLangSuccess: 'Language changed', - scalingRatio: 'Scaling Ratio' + scalingRatio: 'Scaling Ratio', + playbackSpeed: 'Playback Speed', }; diff --git a/src/lang/modules/zh.ts b/src/lang/modules/zh.ts index 968cdea..4ad704c 100644 --- a/src/lang/modules/zh.ts +++ b/src/lang/modules/zh.ts @@ -15,4 +15,5 @@ export default { videoInformation: '视频信息:', toggleLangSuccess: '语言已更改', scalingRatio: '缩放比', + playbackSpeed: '播放速率', }; diff --git a/src/views/asciinemaPlayer/index.vue b/src/views/asciinemaPlayer/index.vue index b8b582e..7288507 100644 --- a/src/views/asciinemaPlayer/index.vue +++ b/src/views/asciinemaPlayer/index.vue @@ -1,6 +1,24 @@ @@ -8,33 +26,172 @@ // @ts-ignore import * as AsciinemaPlayer from '@cyolosecurity/asciinema-player'; import { useRoute } from 'vue-router'; -import { computed, onMounted } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; + +/** Asciinema 播放器实例类型(库未导出完整类型,按需声明常用方法) */ +interface IAsciinemaPlayerInstance { + dispose: () => void; + getCurrentTime: () => number; + play: () => void; + pause: () => void; + addEventListener: (name: string, callback: () => void) => void; +} const route = useRoute(); +const { t } = useI18n(); + const castUrl = computed(() => route.params?.castUrl as string); +const terminalRef = ref(null); +const playerAreaRef = ref(null); -onMounted(() => { - AsciinemaPlayer.create(castUrl.value, document.getElementById('terminal')!, { +/** 当前播放倍速,默认 1x */ +const playbackSpeed = ref(1); +/** 是否正在播放,用于切换倍速后恢复播放状态 */ +const isPlaying = ref(false); + +/** 倍速选项,与 Gua / MP4 播放器保持一致 */ +const speedOptions = [ + { label: '0.75x', value: 0.75 }, + { label: '1x', value: 1 }, + { label: '1.5x', value: 1.5 }, + { label: '2x', value: 2 } +]; + +let playerInstance: IAsciinemaPlayerInstance | null = null; + +/** + * @description 创建或重建 Asciinema 播放器 + * @param url cast 文件 ObjectURL + * @param speed 播放倍速 + * @param startAt 起始时间(秒) + * @param autoPlay 是否自动播放 + * + * 说明:Asciinema 仅在 create 时接受 speed 参数,运行时切换倍速需销毁并重建实例。 + */ +const mountPlayer = ( + url: string, + speed: number, + startAt = 0, + autoPlay = true +) => { + const container = terminalRef.value; + if (!container || !url) return; + + // 销毁旧实例,避免重复挂载 + if (playerInstance) { + playerInstance.dispose(); + playerInstance = null; + } + + container.innerHTML = ''; + + playerInstance = AsciinemaPlayer.create(url, container, { fit: 'both', preload: true, - autoplay: true + autoPlay, + speed, + startAt, + controls: 'auto' + }) as IAsciinemaPlayerInstance; + + // 监听播放状态,便于倍速切换后恢复 + playerInstance.addEventListener('play', () => { + isPlaying.value = true; + }); + playerInstance.addEventListener('pause', () => { + isPlaying.value = false; }); +}; + +/** + * @description 切换播放倍速 + * @param speed 新的倍速值 + */ +const handleSpeedChange = (speed: number) => { + if (!playerInstance) return; + + const currentTime = playerInstance.getCurrentTime(); + const shouldPlay = isPlaying.value; + + playerInstance.pause(); + mountPlayer(castUrl.value, speed, currentTime, shouldPlay); +}; + +watch( + () => castUrl.value, + url => { + playbackSpeed.value = 1; + isPlaying.value = false; + mountPlayer(url, 1, 0, true); + } +); + +onMounted(() => { + mountPlayer(castUrl.value, playbackSpeed.value, 0, true); +}); + +onBeforeUnmount(() => { + playerInstance?.dispose(); + playerInstance = null; });