Silent timeline removed when hitting export
This commit is contained in:
@@ -10,6 +10,7 @@ type WsMsg =
|
|||||||
| { type: 'segments'; segments: { start: number; end: number }[]; duration: number }
|
| { type: 'segments'; segments: { start: number; end: number }[]; duration: number }
|
||||||
| { type: 'progress'; percent: number }
|
| { type: 'progress'; percent: number }
|
||||||
| { type: 'done'; message: string }
|
| { type: 'done'; message: string }
|
||||||
|
| { type: 'timeline'; segments: { start: number; end: number }[]; duration: number }
|
||||||
| { type: 'error'; message: string }
|
| { type: 'error'; message: string }
|
||||||
|
|
||||||
type Phase = 'idle' | 'uploading' | 'analyzing' | 'ready' | 'exporting' | 'done'
|
type Phase = 'idle' | 'uploading' | 'analyzing' | 'ready' | 'exporting' | 'done'
|
||||||
@@ -66,6 +67,13 @@ export default function App() {
|
|||||||
return { ...prev, progress: msg.percent }
|
return { ...prev, progress: msg.percent }
|
||||||
case 'done':
|
case 'done':
|
||||||
return { ...prev, phase: 'done', outputFile: msg.message, progress: 100 }
|
return { ...prev, phase: 'done', outputFile: msg.message, progress: 100 }
|
||||||
|
case 'timeline':
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
phase: 'ready',
|
||||||
|
duration: msg.duration,
|
||||||
|
segments: msg.segments.map((s) => ({ ...s, kept: true })),
|
||||||
|
}
|
||||||
case 'error':
|
case 'error':
|
||||||
return { ...prev, phase: 'ready', error: msg.message }
|
return { ...prev, phase: 'ready', error: msg.message }
|
||||||
default:
|
default:
|
||||||
@@ -86,7 +94,7 @@ export default function App() {
|
|||||||
minSilence: number,
|
minSilence: number,
|
||||||
padding: number,
|
padding: number,
|
||||||
) => {
|
) => {
|
||||||
setState((prev) => ({ ...prev, phase: 'analyzing', segments: [], error: null }))
|
setState((prev) => ({ ...prev, phase: 'analyzing', segments: [], outputFile: null, error: null }))
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/analyze', {
|
const res = await fetch('/analyze', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -136,7 +144,7 @@ export default function App() {
|
|||||||
const res = await fetch('/export', {
|
const res = await fetch('/export', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ filename: state.filename, segments: kept }),
|
body: JSON.stringify({ filename: state.filename, segments: kept, duration: state.duration }),
|
||||||
})
|
})
|
||||||
if (!res.ok) throw new Error(await res.text())
|
if (!res.ok) throw new Error(await res.text())
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -254,7 +262,7 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{state.phase === 'done' && state.outputFile && (
|
{state.outputFile && (
|
||||||
<div className="done-panel">
|
<div className="done-panel">
|
||||||
<span className="done-check">✓</span>
|
<span className="done-check">✓</span>
|
||||||
<p>Export complete!</p>
|
<p>Export complete!</p>
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ import (
|
|||||||
type exportRequest struct {
|
type exportRequest struct {
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
Segments []transcode.Segment `json:"segments"`
|
Segments []transcode.Segment `json:"segments"`
|
||||||
|
Duration float64 `json:"duration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateTimeLine struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Segments []transcode.Segment `json:"segments"`
|
||||||
|
Duration float64 `json:"duration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExportHandler(st *store.Store, hub *ws.Hub) http.HandlerFunc {
|
func ExportHandler(st *store.Store, hub *ws.Hub) http.HandlerFunc {
|
||||||
@@ -28,6 +35,7 @@ func ExportHandler(st *store.Store, hub *ws.Hub) http.HandlerFunc {
|
|||||||
http.Error(w, "bad JSON: "+err.Error(), http.StatusBadRequest)
|
http.Error(w, "bad JSON: "+err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(req.Segments) == 0 {
|
if len(req.Segments) == 0 {
|
||||||
http.Error(w, "no segments", http.StatusBadRequest)
|
http.Error(w, "no segments", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -50,15 +58,30 @@ func ExportHandler(st *store.Store, hub *ws.Hub) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
msg, _ := json.Marshal(map[string]string{
|
doneMsg, _ := json.Marshal(map[string]string{
|
||||||
"type": "done",
|
"type": "done",
|
||||||
"message": outputKey,
|
"message": outputKey,
|
||||||
})
|
})
|
||||||
hub.Broadcast(msg)
|
hub.Broadcast(doneMsg)
|
||||||
|
|
||||||
|
// Build normalized segments for the exported video: timestamps start at 0
|
||||||
|
// with no gaps, so the timeline reflects the exported file's layout.
|
||||||
|
var normalized []transcode.Segment
|
||||||
|
cursor := 0.0
|
||||||
|
for _, seg := range req.Segments {
|
||||||
|
dur := seg.End - seg.Start
|
||||||
|
normalized = append(normalized, transcode.Segment{Start: cursor, End: cursor + dur})
|
||||||
|
cursor += dur
|
||||||
|
}
|
||||||
|
timelineMsg, _ := json.Marshal(updateTimeLine{
|
||||||
|
Type: "timeline",
|
||||||
|
Segments: normalized,
|
||||||
|
Duration: cursor,
|
||||||
|
})
|
||||||
|
hub.Broadcast(timelineMsg)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
w.WriteHeader(http.StatusAccepted)
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
|
||||||
log.Println("Export handler works")
|
log.Println("Export handler works")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ func ZoomHandler(hub *ws.Hub) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h := transcode.HandlerZoom{Center: req.Center}
|
result := transcode.TimelineZoom(req.Center, req.Zoom, req.Duration)
|
||||||
result := h.TimelineZoom(req.Zoom, req.Duration)
|
|
||||||
|
|
||||||
data, _ := json.Marshal(result)
|
data, _ := json.Marshal(result)
|
||||||
hub.Broadcast(data)
|
hub.Broadcast(data)
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
package transcode
|
package transcode
|
||||||
|
|
||||||
// HandlerZoom holds the current scroll center for timeline zoom calculations.
|
|
||||||
type HandlerZoom struct {
|
|
||||||
Center float64 // center of the visible window in seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZoomResult is the result of a TimelineZoom calculation.
|
// ZoomResult is the result of a TimelineZoom calculation.
|
||||||
type ZoomResult struct {
|
type ZoomResult struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -14,13 +9,13 @@ type ZoomResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TimelineZoom computes the visible time window for the given zoomPercentage
|
// TimelineZoom computes the visible time window for the given zoomPercentage
|
||||||
// (1–100, where 100 = full duration visible) centered on h.Center.
|
// (1–100, where 100 = full duration visible) centered on zoomCenter.
|
||||||
func (h *HandlerZoom) TimelineZoom(zoomPercentage, duration float64) *ZoomResult {
|
func TimelineZoom(zoomCenter, zoomPercentage, duration float64) *ZoomResult {
|
||||||
visibleDuration := duration * (zoomPercentage / 100)
|
visibleDuration := duration * (zoomPercentage / 100)
|
||||||
half := visibleDuration / 2
|
half := visibleDuration / 2
|
||||||
|
|
||||||
viewStart := h.Center - half
|
viewStart := zoomCenter - half
|
||||||
viewEnd := h.Center + half
|
viewEnd := zoomCenter + half
|
||||||
|
|
||||||
// Clamp to [0, duration], shifting the window rather than just truncating
|
// Clamp to [0, duration], shifting the window rather than just truncating
|
||||||
// so the visible span stays the same size when near the edges.
|
// so the visible span stays the same size when near the edges.
|
||||||
|
|||||||
Reference in New Issue
Block a user