Update package.json and package-lock.json to include flv.js and hls.js dependencies; enhance Vite config with additional API proxy; refactor App.vue and ChinaMap.vue for improved component functionality and layout; optimize MarketEnvironmentMonitor and MarketRealtimeMonitor components for better user experience; update SupplyDemandData and PurchaserAnalysis components for enhanced data presentation.
@ -0,0 +1,54 @@ |
||||
{ |
||||
"pageSize": 2, |
||||
"autoPlayInterval": 10000, |
||||
"cameras": [ |
||||
{ |
||||
"id": 1, |
||||
"name": "交易大厅主区", |
||||
"resolution": "1920x1080", |
||||
"preview": "/images/monitor/交易大厅主区.jpg", |
||||
"streamUrl": "", |
||||
"status": "online" |
||||
}, |
||||
{ |
||||
"id": 2, |
||||
"name": "牦牛展示区", |
||||
"resolution": "1920x1080", |
||||
"preview": "/images/monitor/牦牛展示区.jpg", |
||||
"streamUrl": "", |
||||
"status": "online" |
||||
}, |
||||
{ |
||||
"id": 3, |
||||
"name": "停车场入口", |
||||
"resolution": "1280x720", |
||||
"preview": "/images/monitor/停车场入口.jpg", |
||||
"streamUrl": "", |
||||
"status": "online" |
||||
}, |
||||
{ |
||||
"id": 4, |
||||
"name": "安全出口", |
||||
"resolution": "1280x720", |
||||
"preview": "/images/monitor/安全出口.jpg", |
||||
"streamUrl": "", |
||||
"status": "offline" |
||||
}, |
||||
{ |
||||
"id": 5, |
||||
"name": "办公区域", |
||||
"resolution": "1920x1080", |
||||
"preview": "/images/monitor/办公区域.jpg", |
||||
"streamUrl": "", |
||||
"status": "error" |
||||
}, |
||||
{ |
||||
"id": 6, |
||||
"name": "仓储区域", |
||||
"resolution": "1280x720", |
||||
"preview": "/images/monitor/仓储区域.jpg", |
||||
"streamUrl": "", |
||||
"status": "online" |
||||
} |
||||
] |
||||
} |
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 878 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
@ -0,0 +1,76 @@ |
||||
<template> |
||||
<div class="live-player"> |
||||
<video |
||||
ref="videoRef" |
||||
class="screen-media" |
||||
muted |
||||
autoplay |
||||
playsinline |
||||
/> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup> |
||||
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue' |
||||
import { createLivePlayer, destroyLivePlayer } from '../utils/liveStreamPlayer.js' |
||||
|
||||
const props = defineProps({ |
||||
url: { |
||||
type: String, |
||||
default: '' |
||||
}, |
||||
playerConfig: { |
||||
type: Object, |
||||
default: () => ({}) |
||||
}, |
||||
active: { |
||||
type: Boolean, |
||||
default: true |
||||
} |
||||
}) |
||||
|
||||
const videoRef = ref(null) |
||||
let playerInstance = null |
||||
|
||||
const teardown = () => { |
||||
destroyLivePlayer(playerInstance) |
||||
playerInstance = null |
||||
} |
||||
|
||||
const setup = async () => { |
||||
teardown() |
||||
if (!props.active || !props.url) { |
||||
return |
||||
} |
||||
await nextTick() |
||||
if (!videoRef.value) { |
||||
return |
||||
} |
||||
playerInstance = createLivePlayer(videoRef.value, props.url, props.playerConfig) |
||||
} |
||||
|
||||
watch( |
||||
() => [props.url, props.active, props.playerConfig], |
||||
setup, |
||||
{ deep: true } |
||||
) |
||||
|
||||
onMounted(setup) |
||||
onBeforeUnmount(teardown) |
||||
</script> |
||||
|
||||
<style scoped> |
||||
.live-player { |
||||
width: 100%; |
||||
height: 100%; |
||||
overflow: hidden; |
||||
} |
||||
|
||||
.screen-media { |
||||
width: 100%; |
||||
height: 100%; |
||||
object-fit: cover; |
||||
display: block; |
||||
background: #0a1418; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,123 @@ |
||||
import Hls from 'hls.js' |
||||
import flvjs from 'flv.js' |
||||
|
||||
const DEFAULT_FORMAT_RULES = [ |
||||
{ type: 'hls', match: '.m3u8', library: 'hls.js' }, |
||||
{ type: 'flv', match: '.flv', library: 'flv.js' }, |
||||
{ type: 'mp4', match: '.mp4', library: 'native' }, |
||||
{ type: 'webm', match: '.webm', library: 'native' } |
||||
] |
||||
|
||||
export function getFormatRules(playerConfig) { |
||||
const rules = playerConfig?.formatRules |
||||
return Array.isArray(rules) && rules.length ? rules : DEFAULT_FORMAT_RULES |
||||
} |
||||
|
||||
export function detectStreamType(url, playerConfig) { |
||||
if (!url) { |
||||
return 'native' |
||||
} |
||||
const lower = ('' + url).toLowerCase() |
||||
const rules = getFormatRules(playerConfig) |
||||
for (const rule of rules) { |
||||
const match = rule.match ? ('' + rule.match).toLowerCase() : '' |
||||
if (match && lower.includes(match)) { |
||||
return rule.type || 'native' |
||||
} |
||||
} |
||||
return 'native' |
||||
} |
||||
|
||||
export function resolveStreamUrl(camera, playerConfig) { |
||||
if (!camera) { |
||||
return '' |
||||
} |
||||
const priority = playerConfig?.urlPriority || ['hdStreamUrl', 'streamUrl', 'playUrl', 'preview'] |
||||
for (const key of priority) { |
||||
const value = camera[key] |
||||
if (value) { |
||||
return value |
||||
} |
||||
} |
||||
return '' |
||||
} |
||||
|
||||
export function createLivePlayer(videoEl, url, playerConfig = {}) { |
||||
if (!videoEl || !url) { |
||||
return null |
||||
} |
||||
|
||||
const streamType = detectStreamType(url, playerConfig) |
||||
const options = playerConfig?.playerOptions || {} |
||||
|
||||
if (streamType === 'hls') { |
||||
if (videoEl.canPlayType('application/vnd.apple.mpegurl')) { |
||||
videoEl.src = url |
||||
videoEl.play().catch(() => {}) |
||||
return { type: 'native-hls', videoEl } |
||||
} |
||||
if (Hls.isSupported()) { |
||||
const hls = new Hls({ |
||||
enableWorker: true, |
||||
lowLatencyMode: true, |
||||
...(options.hls || {}) |
||||
}) |
||||
hls.loadSource(url) |
||||
hls.attachMedia(videoEl) |
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => { |
||||
videoEl.play().catch(() => {}) |
||||
}) |
||||
hls.on(Hls.Events.ERROR, (_, data) => { |
||||
if (data?.fatal) { |
||||
console.error('HLS 播放失败:', data) |
||||
} |
||||
}) |
||||
return { type: 'hls', instance: hls, videoEl } |
||||
} |
||||
} |
||||
|
||||
if (streamType === 'flv' && flvjs.isSupported()) { |
||||
const flvPlayer = flvjs.createPlayer( |
||||
{ |
||||
type: 'flv', |
||||
url, |
||||
isLive: true, |
||||
hasAudio: true, |
||||
hasVideo: true, |
||||
...(options.flv || {}) |
||||
}, |
||||
{ |
||||
enableWorker: true, |
||||
enableStashBuffer: false, |
||||
...(options.flvMedia || {}) |
||||
} |
||||
) |
||||
flvPlayer.attachMediaElement(videoEl) |
||||
flvPlayer.load() |
||||
flvPlayer.play().catch(() => {}) |
||||
return { type: 'flv', instance: flvPlayer, videoEl } |
||||
} |
||||
|
||||
videoEl.src = url |
||||
videoEl.play().catch(() => {}) |
||||
return { type: 'native', videoEl } |
||||
} |
||||
|
||||
export function destroyLivePlayer(player) { |
||||
if (!player) { |
||||
return |
||||
} |
||||
if (player.type === 'hls' && player.instance) { |
||||
player.instance.destroy() |
||||
} else if (player.type === 'flv' && player.instance) { |
||||
player.instance.pause() |
||||
player.instance.unload() |
||||
player.instance.detachMediaElement() |
||||
player.instance.destroy() |
||||
} |
||||
if (player.videoEl) { |
||||
player.videoEl.pause() |
||||
player.videoEl.removeAttribute('src') |
||||
player.videoEl.load() |
||||
} |
||||
} |
||||