This commit is contained in:
team3
2026-10-01 00:09:16 +02:00
parent 8ddd8208f5
commit bd1694528a
3 changed files with 52 additions and 17 deletions

View File

@@ -44,7 +44,9 @@ PLAYER_LATENZ = 4
# Das Fenster um die Marke: Standard und Grenzen
VORHER = 30
DAUER = 35
MAX_VORHER = 600
# Die Marke ist nur ein Anker — das Fenster darf überall auf dem
# Zeitstrahl liegen, auch weit nach der Marke (vorher negativ)
MAX_VORHER = 4 * 3600
MIN_DAUER = 3
MAX_DAUER = 180
MAX_TITEL = 80
@@ -138,7 +140,7 @@ def aendern(clip_id, session_id, host, titel=None, vorher=None, dauer=None):
if titel is not None:
felder["titel"] = titel.strip()[:MAX_TITEL] or None
if vorher is not None:
felder["vorher"] = max(0, min(MAX_VORHER, float(vorher)))
felder["vorher"] = max(-MAX_VORHER, min(MAX_VORHER, float(vorher)))
if dauer is not None:
felder["dauer"] = max(MIN_DAUER, min(MAX_DAUER, float(dauer)))
fenster_neu = any(
@@ -173,7 +175,7 @@ def _datei_weg(name):
def fenster(row, vorher=None, dauer=None):
"""Start (RFC3339) und Dauer des Ausschnitts um die Marke."""
marke = datetime.fromisoformat(row["marke_at"])
v = row["vorher"] if vorher is None else max(0, min(MAX_VORHER, float(vorher)))
v = row["vorher"] if vorher is None else max(-MAX_VORHER, min(MAX_VORHER, float(vorher)))
d = row["dauer"] if dauer is None else max(MIN_DAUER, min(MAX_DAUER, float(dauer)))
return _iso(marke - timedelta(seconds=v)), d

View File

@@ -128,8 +128,12 @@ def test_fremde_sehen_nichts_der_host_alles(client, client2, zuschauer, hostmodu
def test_fenster_wird_begrenzt_und_macht_schnitt_ungueltig(client, monkeypatch):
m = marke(client)
geaendert = client.patch(f"/api/clips/{m['id']}", json={"vorher": 5000, "dauer": 1, "titel": " Hallo "}).json()
geaendert = client.patch(f"/api/clips/{m['id']}", json={"vorher": 99999, "dauer": 1, "titel": " Hallo "}).json()
assert (geaendert["vorher"], geaendert["dauer"], geaendert["titel"]) == (clips.MAX_VORHER, clips.MIN_DAUER, "Hallo")
# Das Fenster darf auch nach der Marke liegen — bis vier Stunden weit
assert client.patch(f"/api/clips/{m['id']}", json={"vorher": -60}).json()["vorher"] == -60
assert client.patch(f"/api/clips/{m['id']}", json={"vorher": -99999}).json()["vorher"] == -clips.MAX_VORHER
client.patch(f"/api/clips/{m['id']}", json={"vorher": clips.MAX_VORHER, "dauer": 1})
# Schneiden: der Playback-Server wird nachgestellt
class Antwort:

View File

@@ -1,14 +1,15 @@
<script setup>
// Der Zeitstrahl der Aufnahme rund um die Marke. Ein Balken zeigt das
// Fenster des Clips; an seinen Enden zieht man Anfang und Ende, in der
// Mitte schiebt man das ganze Fenster. Zeiten in Sekunden relativ zur
// Marke: 0 ist der Moment, an dem C gedrückt wurde.
// Mitte schiebt man das ganze Fenster. Daneben zieht man den Strahl
// selbst nach links und rechts. Zeiten in Sekunden relativ zur Marke:
// 0 ist der Moment, an dem C gedrückt wurde.
import { computed, ref } from 'vue'
const props = defineProps({
// Stücke der Aufnahme: [{ von, bis }] relativ zur Marke
segmente: { type: Array, default: () => [] },
maxVorher: { type: Number, default: 600 },
maxVorher: { type: Number, default: 4 * 3600 },
minDauer: { type: Number, default: 3 },
maxDauer: { type: Number, default: 180 },
// Uhrzeit der Marke, für die Beschriftung
@@ -36,15 +37,28 @@ function prozent(t) {
const start = computed(() => -vorher.value)
const ende = computed(() => -vorher.value + dauer.value)
// Wo die Aufnahme überhaupt Material hat
// Wo die Aufnahme überhaupt Material hat — so weit lässt sich der Strahl schieben
const grenzen = computed(() => {
if (!props.segmente.length) return { von: -props.maxVorher, bis: props.maxDauer }
if (!props.segmente.length) return { von: -props.maxVorher, bis: props.maxVorher }
return {
von: Math.min(...props.segmente.map((s) => s.von)),
bis: Math.max(...props.segmente.map((s) => s.bis)),
}
})
// Das Stück der Aufnahme, in dem das Fenster liegt. Zwischen zwei
// Stücken ist Loch — dort gibt es kein Bild, dorthin darf das Fenster
// nicht. Liegt es in einem Loch, gilt das nähere Stück.
function abstand(s, t) {
return t < s.von ? s.von - t : t > s.bis ? t - s.bis : 0
}
function stueckUm(t) {
if (!props.segmente.length) return { von: -props.maxVorher, bis: props.maxVorher }
const s = props.segmente.reduce((a, b) => (abstand(a, t) <= abstand(b, t) ? a : b))
return { von: Math.max(s.von, -props.maxVorher), bis: Math.min(s.bis, props.maxVorher) }
}
function zoomen(schritt) {
zoom.value = Math.max(0, Math.min(ZOOMS.length - 1, zoom.value + schritt))
mitte.value = (start.value + ende.value) / 2
@@ -72,19 +86,30 @@ let ausgang = null
function anfassen(e, was) {
griff = was
ausgang = { x: e.clientX, vorher: vorher.value, dauer: dauer.value }
ausgang = {
x: e.clientX,
vorher: vorher.value,
dauer: dauer.value,
mitte: mitte.value,
// Das Stück, in dem das Fenster beim Anfassen liegt — darin bleibt es
stueck: stueckUm(-vorher.value + dauer.value / 2),
}
e.currentTarget.setPointerCapture(e.pointerId)
}
function ziehen(e) {
if (!griff || !strahl.value) return
const dt = ((e.clientX - ausgang.x) / strahl.value.clientWidth) * spanne.value
const g = grenzen.value
if (griff === 'links') {
const g = ausgang.stueck
if (griff === 'bahn') {
// Den Strahl selbst schieben — die Zeiten laufen der Hand entgegen
const r = grenzen.value
mitte.value = Math.max(r.von, Math.min(r.bis, ausgang.mitte - dt))
} else if (griff === 'links') {
// Der Anfang wandert, das Ende bleibt stehen
const altesEnde = -ausgang.vorher + ausgang.dauer
let neuerStart = -ausgang.vorher + dt
neuerStart = Math.max(g.von, -props.maxVorher, neuerStart)
neuerStart = Math.max(g.von, neuerStart)
neuerStart = Math.min(
altesEnde - props.minDauer,
Math.max(altesEnde - props.maxDauer, neuerStart),
@@ -97,18 +122,20 @@ function ziehen(e) {
const d = Math.max(props.minDauer, Math.min(props.maxDauer, neuesEnde + ausgang.vorher))
dauer.value = Math.round(d)
} else {
// Das ganze Fenster verschieben
// Das ganze Fenster verschieben, aber nicht aus dem Stück hinaus
let neuerStart = -ausgang.vorher + dt
neuerStart = Math.max(g.von, -props.maxVorher, Math.min(g.bis - ausgang.dauer, neuerStart))
neuerStart = Math.max(g.von, Math.min(g.bis - ausgang.dauer, neuerStart))
vorher.value = Math.round(-neuerStart)
}
}
function loslassen(e) {
if (!griff) return
const war = griff
griff = null
e.currentTarget.releasePointerCapture?.(e.pointerId)
emit('ende')
// Nur ein verändertes Fenster braucht eine neue Vorschau
if (war !== 'bahn') emit('ende')
}
</script>
@@ -134,6 +161,7 @@ function loslassen(e) {
<div
ref="strahl"
class="strahl"
@pointerdown="anfassen($event, 'bahn')"
@pointermove="ziehen"
@pointerup="loslassen"
@pointercancel="loslassen"
@@ -153,7 +181,7 @@ function loslassen(e) {
<span
class="bereich"
:style="{ left: prozent(start) + '%', width: prozent(ende) - prozent(start) + '%' }"
@pointerdown="anfassen($event, 'mitte')"
@pointerdown.stop="anfassen($event, 'mitte')"
>
<span class="griff links" @pointerdown.stop="anfassen($event, 'links')"></span>
<span class="griff rechts" @pointerdown.stop="anfassen($event, 'rechts')"></span>
@@ -206,6 +234,7 @@ function loslassen(e) {
background: var(--bg);
border: 1px solid var(--line);
border-radius: 8px;
cursor: grab;
touch-action: none;
}