mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-15 07:26:44 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df647b45cd | ||
|
|
b74659dcd4 | ||
|
|
4f2fb534a4 | ||
|
|
bd17f8d0f8 | ||
|
|
cee43795f8 |
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "one-kvm"
|
name = "one-kvm"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["SilentWind"]
|
authors = ["SilentWind"]
|
||||||
description = "A open and lightweight IP-KVM solution written in Rust"
|
description = "A open and lightweight IP-KVM solution written in Rust"
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ pub fn placeholder_html() -> &'static str {
|
|||||||
<h1>One-KVM</h1>
|
<h1>One-KVM</h1>
|
||||||
<p>Frontend not built yet.</p>
|
<p>Frontend not built yet.</p>
|
||||||
<p>Please build the frontend or access the API directly.</p>
|
<p>Please build the frontend or access the API directly.</p>
|
||||||
<div class="version">v0.1.0</div>
|
<div class="version">v0.1.6</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>"#
|
</html>"#
|
||||||
|
|||||||
@@ -46,6 +46,53 @@ use webrtc::ice_transport::ice_gatherer_state::RTCIceGathererState;
|
|||||||
/// H.265/HEVC MIME type (RFC 7798)
|
/// H.265/HEVC MIME type (RFC 7798)
|
||||||
const MIME_TYPE_H265: &str = "video/H265";
|
const MIME_TYPE_H265: &str = "video/H265";
|
||||||
|
|
||||||
|
fn h264_contains_parameter_sets(data: &[u8]) -> bool {
|
||||||
|
// Annex-B start code path
|
||||||
|
let mut i = 0usize;
|
||||||
|
while i + 4 <= data.len() {
|
||||||
|
let sc_len = if i + 4 <= data.len()
|
||||||
|
&& data[i] == 0
|
||||||
|
&& data[i + 1] == 0
|
||||||
|
&& data[i + 2] == 0
|
||||||
|
&& data[i + 3] == 1
|
||||||
|
{
|
||||||
|
4
|
||||||
|
} else if i + 3 <= data.len() && data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
|
||||||
|
3
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let nal_start = i + sc_len;
|
||||||
|
if nal_start < data.len() {
|
||||||
|
let nal_type = data[nal_start] & 0x1F;
|
||||||
|
if nal_type == 7 || nal_type == 8 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = nal_start.saturating_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length-prefixed fallback
|
||||||
|
let mut pos = 0usize;
|
||||||
|
while pos + 4 <= data.len() {
|
||||||
|
let nalu_len =
|
||||||
|
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
|
||||||
|
pos += 4;
|
||||||
|
if nalu_len == 0 || pos + nalu_len > data.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let nal_type = data[pos] & 0x1F;
|
||||||
|
if nal_type == 7 || nal_type == 8 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
pos += nalu_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Universal WebRTC session configuration
|
/// Universal WebRTC session configuration
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct UniversalSessionConfig {
|
pub struct UniversalSessionConfig {
|
||||||
@@ -649,6 +696,13 @@ impl UniversalSession {
|
|||||||
if gap_detected {
|
if gap_detected {
|
||||||
waiting_for_keyframe = true;
|
waiting_for_keyframe = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Some H264 encoders output SPS/PPS in a separate non-keyframe AU
|
||||||
|
// before IDR. Keep this frame so browser can decode the next IDR.
|
||||||
|
let forward_h264_parameter_frame = waiting_for_keyframe
|
||||||
|
&& expected_codec == VideoEncoderType::H264
|
||||||
|
&& h264_contains_parameter_sets(encoded_frame.data.as_ref());
|
||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
if now.duration_since(last_keyframe_request)
|
if now.duration_since(last_keyframe_request)
|
||||||
>= Duration::from_millis(200)
|
>= Duration::from_millis(200)
|
||||||
@@ -656,7 +710,9 @@ impl UniversalSession {
|
|||||||
request_keyframe();
|
request_keyframe();
|
||||||
last_keyframe_request = now;
|
last_keyframe_request = now;
|
||||||
}
|
}
|
||||||
continue;
|
if !forward_h264_parameter_frame {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { Power, RotateCcw, CircleDot, Wifi, Send } from 'lucide-vue-next'
|
import { Power, RotateCcw, CircleDot, Wifi, Send } from 'lucide-vue-next'
|
||||||
|
import { atxApi } from '@/api'
|
||||||
import { atxConfigApi } from '@/api/config'
|
import { atxConfigApi } from '@/api/config'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -34,6 +35,7 @@ const activeTab = ref('atx')
|
|||||||
|
|
||||||
// ATX state
|
// ATX state
|
||||||
const powerState = ref<'on' | 'off' | 'unknown'>('unknown')
|
const powerState = ref<'on' | 'off' | 'unknown'>('unknown')
|
||||||
|
let powerStateTimer: number | null = null
|
||||||
// Decouple action data from dialog visibility to prevent race conditions
|
// Decouple action data from dialog visibility to prevent race conditions
|
||||||
const pendingAction = ref<'short' | 'long' | 'reset' | null>(null)
|
const pendingAction = ref<'short' | 'long' | 'reset' | null>(null)
|
||||||
const confirmDialogOpen = ref(false)
|
const confirmDialogOpen = ref(false)
|
||||||
@@ -71,6 +73,9 @@ function handleAction() {
|
|||||||
else if (pendingAction.value === 'long') emit('powerLong')
|
else if (pendingAction.value === 'long') emit('powerLong')
|
||||||
else if (pendingAction.value === 'reset') emit('reset')
|
else if (pendingAction.value === 'reset') emit('reset')
|
||||||
confirmDialogOpen.value = false
|
confirmDialogOpen.value = false
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshPowerState().catch(() => {})
|
||||||
|
}, 1200)
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmTitle = computed(() => {
|
const confirmTitle = computed(() => {
|
||||||
@@ -139,6 +144,29 @@ async function loadWolHistory() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshPowerState() {
|
||||||
|
try {
|
||||||
|
const state = await atxApi.status()
|
||||||
|
powerState.value = state.power_status
|
||||||
|
} catch {
|
||||||
|
powerState.value = 'unknown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
refreshPowerState().catch(() => {})
|
||||||
|
powerStateTimer = window.setInterval(() => {
|
||||||
|
refreshPowerState().catch(() => {})
|
||||||
|
}, 3000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (powerStateTimer !== null) {
|
||||||
|
window.clearInterval(powerStateTimer)
|
||||||
|
powerStateTimer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => activeTab.value,
|
() => activeTab.value,
|
||||||
(tab) => {
|
(tab) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user