fix: use native img tag for MJPEG live display

Live view now uses <img :src='streamUrl'> (browser-native MJPEG
support, same as the working test HTML). The fetch-based multipart
parser is kept only for recording frame capture. Also fixed
multipart boundary parsing to correctly handle boundary values
that already include leading dashes.

- LiveView receives streamUrl string, passes it directly to <img>
- useMjpegStream simplified to URL + state management
- useRecorder manages its own fetch stream independently
- RecorderControls shows recorder-specific errors
This commit is contained in:
2026-08-01 16:04:20 +08:00
parent 97e6b4c056
commit 6bbe83e1f1
5 changed files with 206 additions and 222 deletions
+11 -13
View File
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { ref, computed, onUnmounted } from 'vue'
import { config } from './config'
import { useMjpegStream } from './composables/useMjpegStream'
import { useRecorder } from './composables/useRecorder'
import LiveView from './components/LiveView.vue'
@@ -8,25 +7,22 @@ import RecorderControls from './components/RecorderControls.vue'
import PlaybackPanel from './components/PlaybackPanel.vue'
import ConfigPanel from './components/ConfigPanel.vue'
// ---- Stream ----
const { isLive, error, frameUrl, start, stop, setOnFrame } = useMjpegStream()
// ---- Stream (native <img> — no CORS issues) ----
const { streamUrl, isLive, error, start, stop } = useMjpegStream()
// ---- Recorder ----
// ---- Recorder (fetch-based frame capture) ----
const {
recorderState,
elapsedMs,
frameCount,
onFrame,
recorderError,
startRecording,
stopRecording,
cancelRecording,
formatElapsed,
} = useRecorder()
// Wire recorder to stream frames
setOnFrame(onFrame)
// ---- Playback panel ref (to refresh list after save) ----
// ---- Playback panel ref ----
const playbackRef = ref<InstanceType<typeof PlaybackPanel> | null>(null)
// ---- Active tab ----
@@ -38,13 +34,13 @@ function handleConnect(url: string) {
start(url)
}
async function handleStartRecording() {
startRecording()
function handleStartRecording() {
if (!streamUrl.value) return
startRecording(streamUrl.value)
}
async function handleStopRecording() {
const recording = await stopRecording()
// Refresh playback list
playbackRef.value?.loadRecordings()
if (recording) {
activeTab.value = 'playback'
@@ -59,6 +55,7 @@ const elapsedFormatted = computed(() => formatElapsed(elapsedMs.value))
onUnmounted(() => {
stop()
cancelRecording()
})
</script>
@@ -94,12 +91,13 @@ onUnmounted(() => {
<!-- Tab: Live View -->
<section v-show="activeTab === 'live'" class="tab-content">
<LiveView :frame-url="frameUrl" :is-live="isLive" :error="error" />
<LiveView :stream-url="streamUrl" :is-live="isLive" :error="error" />
<RecorderControls
:state="recorderState"
:elapsed="elapsedFormatted"
:frame-count="frameCount"
:is-live="isLive"
:error="recorderError"
@start="handleStartRecording"
@stop="handleStopRecording"
@cancel="handleCancelRecording"