mirror of
https://github.com/dfpc-coe/CloudTAK.git
synced 2025-12-22 13:47:22 +00:00
@@ -389,16 +389,11 @@ async function createPlayer(): Promise<void> {
|
||||
try {
|
||||
const url = new URL(videoProtocols.value!.hls!.url);
|
||||
|
||||
// Configure HLS.js with settings optimized for MediaMTX live streaming
|
||||
player.value = new Hls({
|
||||
enableWorker: true,
|
||||
lowLatencyMode: false, // More forgiving for stream restarts
|
||||
debug: false,
|
||||
backBufferLength: 90, // Keep more buffer for smoother playback
|
||||
maxBufferLength: 30, // Larger buffer for resilience
|
||||
maxMaxBufferLength: 600,
|
||||
liveSyncDurationCount: 3, // More tolerant of discontinuities
|
||||
liveMaxLatencyDurationCount: 10,
|
||||
lowLatencyMode: true,
|
||||
debug: localStorage.getItem('debug') === 'true',
|
||||
backBufferLength: 90,
|
||||
xhrSetup: (xhr: XMLHttpRequest) => {
|
||||
// Add authentication if stream requires it
|
||||
if (url.username && url.password) {
|
||||
@@ -407,10 +402,8 @@ async function createPlayer(): Promise<void> {
|
||||
}
|
||||
});
|
||||
|
||||
// Attach HLS player to video element
|
||||
player.value.attachMedia(videoTag.value!);
|
||||
|
||||
// Load HLS source when media is attached
|
||||
player.value.on(Hls.Events.MEDIA_ATTACHED, async () => {
|
||||
if (player.value) player.value.loadSource(url.toString());
|
||||
});
|
||||
@@ -431,7 +424,7 @@ async function createPlayer(): Promise<void> {
|
||||
switch (data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
if (!data.fatal) {
|
||||
handleStreamRestart(); // Handle muxer restart scenario
|
||||
player.value!.recoverMediaError();
|
||||
break;
|
||||
} else {
|
||||
console.log("Fatal network error:", data);
|
||||
@@ -440,7 +433,7 @@ async function createPlayer(): Promise<void> {
|
||||
}
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
if (!data.fatal) {
|
||||
handleStreamRestart(); // Handle muxer restart scenario
|
||||
player.value!.recoverMediaError();
|
||||
break;
|
||||
} else {
|
||||
console.log("Fatal media error:", data);
|
||||
@@ -472,24 +465,6 @@ async function createPlayer(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle MediaMTX muxer restarts gracefully
|
||||
* This occurs when MediaMTX creates new segment naming due to source hiccups
|
||||
*/
|
||||
function handleStreamRestart(): void {
|
||||
console.log('Handling HLS stream restart (muxer restart detected)');
|
||||
if (player.value && videoProtocols.value?.hls) {
|
||||
try {
|
||||
player.value.stopLoad();
|
||||
player.value.startLoad();
|
||||
} catch (err) {
|
||||
console.error('Error handling stream restart:', err);
|
||||
// Fall back to full retry if restart handling fails
|
||||
handleStreamError(err instanceof Error ? err : new Error(String(err)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle stream errors with exponential backoff retry logic
|
||||
* Implements 3-attempt retry system with increasing delays: 1s, 2s, 4s
|
||||
|
||||
@@ -1,31 +1,56 @@
|
||||
<template>
|
||||
<div
|
||||
v-if='url_links.length || responder_links.length'
|
||||
class='col-12 py-2'
|
||||
class='col-12'
|
||||
>
|
||||
<div
|
||||
v-if='url_links.length'
|
||||
class='col-12 mb-3'
|
||||
class='col-12'
|
||||
>
|
||||
<div class='col-12 mb-2'>
|
||||
<div
|
||||
class='d-flex align-items-center cursor-pointer user-select-none py-2 px-2 rounded transition-all mx-2'
|
||||
:class='{ "bg-accent": expandedLinks, "hover": !expandedLinks }'
|
||||
@click='expandedLinks = !expandedLinks'
|
||||
>
|
||||
<IconLink
|
||||
:size='18'
|
||||
stroke='1'
|
||||
color='#6b7990'
|
||||
class='ms-2 me-1'
|
||||
/>
|
||||
<label class='subheader user-select-none'>Links</label>
|
||||
<label class='subheader cursor-pointer m-0'>Links</label>
|
||||
<div class='ms-auto d-flex align-items-center'>
|
||||
<span class='badge bg-blue-lt me-2'>{{ url_links.length }}</span>
|
||||
<IconChevronDown
|
||||
class='transition-transform'
|
||||
:class='{ "rotate-180": !expandedLinks }'
|
||||
:size='18'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class='list-group list-group-flush bg-accent rounded mx-2'>
|
||||
<a
|
||||
v-for='(link, link_it) of url_links'
|
||||
:key='link_it'
|
||||
:href='link.url'
|
||||
target='_blank'
|
||||
class='list-group-item list-group-item-action d-flex align-items-center bg-transparent border-0'
|
||||
>
|
||||
<span class='text-truncate'>{{ link.remarks || link.url }}</span>
|
||||
</a>
|
||||
|
||||
<div
|
||||
class='grid-transition'
|
||||
:class='{ expanded: expandedLinks }'
|
||||
>
|
||||
<div class='overflow-hidden mb-2'>
|
||||
<div class='list-group list-group-flush bg-accent rounded mx-2 mt-2'>
|
||||
<a
|
||||
v-for='(link, link_it) of url_links'
|
||||
:key='link_it'
|
||||
:href='link.url'
|
||||
target='_blank'
|
||||
class='list-group-item list-group-item-action d-flex align-items-center bg-transparent border-0'
|
||||
>
|
||||
<IconExternalLink
|
||||
:size='18'
|
||||
stroke='1'
|
||||
class='me-2'
|
||||
/>
|
||||
<span class='text-truncate'>{{ link.remarks || link.url }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,8 +60,8 @@
|
||||
>
|
||||
<div
|
||||
class='d-flex align-items-center cursor-pointer user-select-none py-2 px-2 rounded transition-all mx-2'
|
||||
:class='{ "bg-accent": expanded, "hover": !expanded }'
|
||||
@click='expanded = !expanded'
|
||||
:class='{ "bg-accent": expandedResponders, "hover": !expandedResponders }'
|
||||
@click='expandedResponders = !expandedResponders'
|
||||
>
|
||||
<IconUsers
|
||||
:size='18'
|
||||
@@ -49,7 +74,7 @@
|
||||
<span class='badge bg-blue-lt me-2'>{{ responder_links.length }}</span>
|
||||
<IconChevronDown
|
||||
class='transition-transform'
|
||||
:class='{ "rotate-180": !expanded }'
|
||||
:class='{ "rotate-180": !expandedResponders }'
|
||||
:size='18'
|
||||
/>
|
||||
</div>
|
||||
@@ -57,7 +82,7 @@
|
||||
|
||||
<div
|
||||
class='grid-transition'
|
||||
:class='{ expanded: expanded }'
|
||||
:class='{ expanded: expandedResponders }'
|
||||
>
|
||||
<div class='overflow-hidden'>
|
||||
<div class='row row-cards mx-2 pt-2'>
|
||||
@@ -102,10 +127,11 @@
|
||||
|
||||
<script setup lang='ts'>
|
||||
import { computed, ref } from 'vue';
|
||||
import { IconUsers, IconLink, IconChevronDown } from '@tabler/icons-vue';
|
||||
import { IconUsers, IconLink, IconChevronDown, IconExternalLink } from '@tabler/icons-vue';
|
||||
import timediff from '../../../timediff';
|
||||
|
||||
const expanded = ref(false);
|
||||
const expandedResponders = ref(false);
|
||||
const expandedLinks = ref(false);
|
||||
|
||||
const props = defineProps<{
|
||||
links: Array<{
|
||||
@@ -150,4 +176,8 @@ const responder_links = computed(() => {
|
||||
.transition-transform {
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
|
||||
.list-group-item-action:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
/>
|
||||
<label class='subheader cursor-pointer m-0'>Times</label>
|
||||
<div class='ms-auto d-flex align-items-center'>
|
||||
<span
|
||||
v-if='props.cot.properties.start'
|
||||
class='cursor-pointer me-2 text-muted small'
|
||||
@click.stop='mode = mode === "relative" ? "absolute" : "relative"'
|
||||
v-text='`Start: ${startProp}`'
|
||||
/>
|
||||
<IconChevronDown
|
||||
class='transition-transform'
|
||||
:class='{ "rotate-180": !expanded }'
|
||||
|
||||
@@ -892,7 +892,12 @@ export const useMapStore = defineStore('cloudtak', {
|
||||
this.radial.mode = opts.mode;
|
||||
|
||||
if (feat.properties && feat.properties.center) {
|
||||
this.radial.lngLat = mapgl.LngLat.convert(feat.properties.center as LngLatLike);
|
||||
if (typeof feat.properties.center === 'string') {
|
||||
const parts = JSON.parse(feat.properties.center);
|
||||
this.radial.lngLat = new mapgl.LngLat(parts[0], parts[1]);
|
||||
} else {
|
||||
this.radial.lngLat = mapgl.LngLat.convert(feat.properties.center as LngLatLike);
|
||||
}
|
||||
} else {
|
||||
this.radial.lngLat = opts.lngLat;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user