From 3fb1a65292351a32ab3a14059337f9bc8f4d6f96 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Sun, 12 Jul 2026 17:28:34 -0700 Subject: [PATCH] add clover pitch and bugs for sequencer --- pitch/Sources/Pitch/AudioEngine.swift | 41 + pitch/Sources/Pitch/Music.swift | 58 + pitch/Sources/Pitch/NoteSegmenter.swift | 120 + pitch/Sources/Pitch/PitchGraphView.swift | 27 + pitch/Sources/Pitch/Theme.swift | 13 +- pitch/Sources/Pitch/TopBar.swift | 39 + .../Sources/recorder/CaptureEngine.swift | 24 +- sequencer/CLAUDE.md | 45 +- sequencer/Deltarune Ch5.sq/project.json | 11285 ---------------- sequencer/Sources/Sequencer/AppDelegate.swift | 7 + sequencer/Sources/Sequencer/Cachetest.swift | 132 + .../Sources/Sequencer/ChunkedProxy.swift | 984 +- sequencer/Sources/Sequencer/Document.swift | 32 +- .../Sources/Sequencer/DocumentContext.swift | 43 +- sequencer/Sources/Sequencer/Export.swift | 34 +- sequencer/Sources/Sequencer/FrameCache.swift | 106 + sequencer/Sources/Sequencer/HangMonitor.swift | 190 + sequencer/Sources/Sequencer/LayerTest.swift | 77 + sequencer/Sources/Sequencer/Log.swift | 44 + .../Sources/Sequencer/MediaPipeline.swift | 407 +- sequencer/Sources/Sequencer/Model.swift | 45 +- sequencer/Sources/Sequencer/PerfTest.swift | 93 +- .../Sequencer/PlaybackController.swift | 91 +- .../Sources/Sequencer/SessionState.swift | 13 + sequencer/Sources/Sequencer/Store.swift | 99 +- sequencer/Sources/Sequencer/Storyboard.swift | 48 +- .../Sources/Sequencer/StoryboardEditor.swift | 18 - sequencer/Sources/Sequencer/Theme.swift | 23 + .../Sources/Sequencer/TimelineView.swift | 1345 +- sequencer/Sources/Sequencer/Tools.swift | 52 +- sequencer/Sources/Sequencer/Transcript.swift | 469 + .../Sources/Sequencer/TransportBar.swift | 27 +- sequencer/Sources/Sequencer/UITest.swift | 47 +- .../Sources/Sequencer/ViewerGridView.swift | 132 +- .../Sources/Sequencer/WindowController.swift | 22 +- sequencer/Sources/Sequencer/main.swift | 43 + sequencer/build.sh | 6 +- sequencer/run.sh | 15 +- 38 files changed, 4467 insertions(+), 11829 deletions(-) create mode 100644 pitch/Sources/Pitch/NoteSegmenter.swift delete mode 100644 sequencer/Deltarune Ch5.sq/project.json create mode 100644 sequencer/Sources/Sequencer/Cachetest.swift create mode 100644 sequencer/Sources/Sequencer/FrameCache.swift create mode 100644 sequencer/Sources/Sequencer/HangMonitor.swift create mode 100644 sequencer/Sources/Sequencer/LayerTest.swift create mode 100644 sequencer/Sources/Sequencer/Log.swift create mode 100644 sequencer/Sources/Sequencer/Transcript.swift diff --git a/pitch/Sources/Pitch/AudioEngine.swift b/pitch/Sources/Pitch/AudioEngine.swift index 0b338adf76a1f5da7ad21dddf021445435a34a4a..a3d890f500b2fd6094b966a007adbc920beebb9c 100644 --- a/pitch/Sources/Pitch/AudioEngine.swift +++ b/pitch/Sources/Pitch/AudioEngine.swift @@ -35,6 +35,9 @@ final class AudioEngine: ObservableObject, @unchecked Sendable { @Published var isRunning = false @Published var permission: MicPermission = .unknown @Published var statusMessage: String? + /// Rolling key estimate (updated on the main thread as notes land). Low + /// frequency, so publishing it doesn't churn the SwiftUI tree per frame. + @Published var keyEstimate: KeyEstimate? private let engine = AVAudioEngine() // Sensitive settings: low RMS gate so quiet singing registers, and a @@ -56,6 +59,11 @@ final class AudioEngine: ObservableObject, @unchecked Sendable { // Small median window to reject single-frame octave/spike errors. private var recentMidi = [Double]() + // Note segmentation ("intentional note" layer) + its guard lock. Fed on the + // audio thread; read by the graph on the display thread. + private let segmenter = NoteSegmenter() + private let notesLock = NSLock() + // History shared with the UI thread. private let historyLock = NSLock() private var history = [PitchSample]() @@ -179,6 +187,8 @@ final class AudioEngine: ObservableObject, @unchecked Sendable { recentMidi.removeAll(keepingCapacity: true) framesProcessed = 0 haveStart = false + notesLock.lock(); segmenter.reset(); notesLock.unlock() + DispatchQueue.main.async { self.keyEstimate = nil } } // MARK: - Audio-thread processing @@ -210,6 +220,7 @@ final class AudioEngine: ObservableObject, @unchecked Sendable { let sr = detector.sampleRate var newSamples = [PitchSample]() + var noteCommitted = false while accumulator.count >= windowSize { var result: YINDetector.Result? @@ -225,8 +236,14 @@ final class AudioEngine: ObservableObject, @unchecked Sendable { newSamples.append(PitchSample(time: windowStartTime, midi: midi, clarity: r.clarity)) setLive(LivePitch(frequency: freq, midi: midi, clarity: r.clarity, level: r.level), at: windowStartTime) + notesLock.lock() + if segmenter.feed(time: windowStartTime, midi: midi) { noteCommitted = true } + notesLock.unlock() } else { recentMidi.removeAll(keepingCapacity: true) + notesLock.lock() + if segmenter.feedSilence(now: windowStartTime) { noteCommitted = true } + notesLock.unlock() } accumulator.removeFirst(hop) @@ -234,6 +251,30 @@ final class AudioEngine: ObservableObject, @unchecked Sendable { } if !newSamples.isEmpty { appendHistory(newSamples) } + if noteCommitted { updateKey() } + } + + /// Recompute the key from committed notes (duration-weighted). Called only + /// when a note lands, so it's cheap; the result is published on the main + /// thread for the top-bar readout. + private func updateKey() { + notesLock.lock() + var weights = [Double](repeating: 0, count: 12) + for n in segmenter.committed { + weights[(((n.midi % 12) + 12) % 12)] += n.duration + } + notesLock.unlock() + let est = Music.estimateKey(weights: weights) + DispatchQueue.main.async { self.keyEstimate = est } + } + + /// Snapshot of committed notes ending at/after `since`, plus the note being + /// sung right now (if any). Read by the graph each frame. + func noteSnapshot(since: Double) -> (committed: [NoteEvent], pending: NoteEvent?) { + notesLock.lock() + defer { notesLock.unlock() } + let committed = segmenter.committed.filter { $0.offset >= since } + return (committed, segmenter.pending) } /// Median-of-3 over consecutive detections — removes lone octave/spike diff --git a/pitch/Sources/Pitch/Music.swift b/pitch/Sources/Pitch/Music.swift index 77b9dfbc2407ea73f63f6ac547b65b6e48bd1193..7550f24c11ad5d69ac13d25a86fa363435dbc3c1 100644 --- a/pitch/Sources/Pitch/Music.swift +++ b/pitch/Sources/Pitch/Music.swift @@ -73,6 +73,64 @@ enum Music { } } +// MARK: - Key estimation (Krumhansl–Schmuckler) + +struct KeyEstimate { + let tonic: Int // pitch class 0–11 (concert) + let isMajor: Bool + let confidence: Double // 0…1, gap between the best and next-best fit +} + +extension Music { + // Krumhansl–Kessler tonal-hierarchy profiles: how strongly each scale + // degree "belongs" in a major / minor key. Index 0 == the tonic. + static let majorProfile: [Double] = + [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88] + static let minorProfile: [Double] = + [6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17] + + /// Estimate key from a 12-bin, duration-weighted pitch-class histogram. + /// Correlates the histogram against all 24 key profiles and returns the best + /// fit. Returns nil until there's enough distinct pitch material to be useful. + static func estimateKey(weights: [Double]) -> KeyEstimate? { + guard weights.count == 12 else { return nil } + guard weights.filter({ $0 > 0 }).count >= 3 else { return nil } + + func correlate(_ x: [Double], _ p: [Double]) -> Double { + let mx = x.reduce(0, +) / 12, mp = p.reduce(0, +) / 12 + var num = 0.0, dx = 0.0, dp = 0.0 + for i in 0..<12 { + let a = x[i] - mx, b = p[i] - mp + num += a * b; dx += a * a; dp += b * b + } + let den = (dx * dp).squareRoot() + return den == 0 ? 0 : num / den + } + + var best = (r: -2.0, tonic: 0, major: true) + var second = -2.0 + for tonic in 0..<12 { + var rot = [Double](repeating: 0, count: 12) + for i in 0..<12 { rot[i] = weights[(i + tonic) % 12] } + for (r, isMaj) in [(correlate(rot, majorProfile), true), + (correlate(rot, minorProfile), false)] { + if r > best.r { second = best.r; best = (r, tonic, isMaj) } + else if r > second { second = r } + } + } + guard best.r > 0 else { return nil } + return KeyEstimate(tonic: best.tonic, isMajor: best.major, + confidence: max(0, min(1, best.r - max(0, second)))) + } + + /// Display name for a key, honoring the current naming + transpose, e.g. + /// "G major". + static func keyName(_ key: KeyEstimate, naming: NoteNaming, transpose: Int) -> String { + let pc = (((key.tonic + transpose) % 12) + 12) % 12 + return "\(naming.names[pc]) \(key.isMajor ? "major" : "minor")" + } +} + // MARK: - Vocal / instrument ranges struct PitchRange: Identifiable, Hashable { diff --git a/pitch/Sources/Pitch/NoteSegmenter.swift b/pitch/Sources/Pitch/NoteSegmenter.swift new file mode 100644 index 0000000000000000000000000000000000000000..eb2918aa443012b76f210bd6dbb31c8bf0d6bfc9 --- /dev/null +++ b/pitch/Sources/Pitch/NoteSegmenter.swift @@ -0,0 +1,120 @@ +import Foundation + +/// One committed "intentional" note — a stable pitch you actually meant to sing, +/// with a start, an end, and how sharp/flat you held it. +struct NoteEvent: Identifiable { + let id: Int + let midi: Int // integer MIDI (concert pitch, before transpose) + let onset: Double // seconds on the CACurrentMediaTime clock + var offset: Double // seconds + let meanCents: Double // signed average deviation from the integer, in cents + var duration: Double { offset - onset } +} + +/// Turns a stream of fractional-MIDI pitch samples into discrete note events — +/// the piano-roll layer that sits above the raw pitch contour. +/// +/// This is a lightweight, real-time analogue of pYIN's note HMM: +/// • a tolerance band around the current note absorbs vibrato and small drift, +/// so a wobbling held note reads as ONE note instead of flickering; +/// • when the pitch leaves that band the note is closed and a new one opens; +/// • notes shorter than `minDuration` are discarded, so scoops and passing +/// tones on the way to a target don't become spurious notes; +/// • a silence gap longer than `gapToEnd` closes the held note. +final class NoteSegmenter { + // Tunables. Cents are hundredths of a semitone; a semitone is 100 cents, so + // a note "owns" ±centsTolerance around its center — the overlap past the + // 50-cent midpoint is the hysteresis that keeps vibrato from splitting notes. + private let centsTolerance = 62.0 + private let minDuration = 0.10 // seconds; below this a region is dropped + private let gapToEnd = 0.13 // seconds of silence that ends a held note + private let retain = 30.0 // seconds of committed notes kept + + private(set) var committed: [NoteEvent] = [] + + // In-progress note. + private var pendingMidi: Int? + private var pendingOnset = 0.0 + private var pendingCentsSum = 0.0 + private var pendingCount = 0 + private var lastVoiced = 0.0 + private var nextID = 0 + + /// Feed one voiced sample. Returns true iff a note was committed (ended). + @discardableResult + func feed(time: Double, midi: Double) -> Bool { + var didCommit = false + + // A silence gap since the previous voiced sample closes the held note. + if pendingMidi != nil, time - lastVoiced > gapToEnd { + didCommit = closePending(offset: lastVoiced) + } + + if let cur = pendingMidi { + let distCents = abs(midi - Double(cur)) * 100 + if distCents <= centsTolerance { + // Still the same note — absorb the sample (this is what swallows + // vibrato even as the nearest semitone flips back and forth). + pendingCentsSum += (midi - Double(cur)) * 100 + pendingCount += 1 + lastVoiced = time + } else { + // Pitch has moved off the note: close it, open a new one. + didCommit = closePending(offset: lastVoiced) || didCommit + openPending(time: time, midi: midi) + } + } else { + openPending(time: time, midi: midi) + } + return didCommit + } + + /// Feed a detector "silence" tick so a held note eventually closes even if no + /// further voiced sample arrives. (Gaps are also caught lazily in `feed`.) + @discardableResult + func feedSilence(now: Double) -> Bool { + guard pendingMidi != nil, now - lastVoiced > gapToEnd else { return false } + return closePending(offset: lastVoiced) + } + + /// The note currently being sung, as a provisional event for live rendering. + var pending: NoteEvent? { + guard let m = pendingMidi, pendingCount > 0 else { return nil } + // Only surface it once it's plausibly a note, not a passing scoop. + guard lastVoiced - pendingOnset >= minDuration * 0.5 else { return nil } + return NoteEvent(id: -1, midi: m, onset: pendingOnset, offset: lastVoiced, + meanCents: pendingCentsSum / Double(pendingCount)) + } + + func reset() { + committed.removeAll(keepingCapacity: true) + pendingMidi = nil + pendingCount = 0 + } + + // MARK: - Private + + private func openPending(time: Double, midi: Double) { + let m = Int(midi.rounded()) + pendingMidi = m + pendingOnset = time + pendingCentsSum = (midi - Double(m)) * 100 + pendingCount = 1 + lastVoiced = time + } + + @discardableResult + private func closePending(offset: Double) -> Bool { + guard let m = pendingMidi else { return false } + pendingMidi = nil + guard offset - pendingOnset >= minDuration, pendingCount > 0 else { return false } + committed.append(NoteEvent(id: nextID, midi: m, onset: pendingOnset, + offset: offset, meanCents: pendingCentsSum / Double(pendingCount))) + nextID += 1 + let cutoff = offset - retain + if let first = committed.first, first.offset < cutoff { + committed.removeAll { $0.offset < cutoff } + } + return true + } +} diff --git a/pitch/Sources/Pitch/PitchGraphView.swift b/pitch/Sources/Pitch/PitchGraphView.swift index dd2131dc03bc342945c494e2d83867d0fc0b87fc..ffa740a15fc66cb6ecc21e9d5df01833689f674d 100644 --- a/pitch/Sources/Pitch/PitchGraphView.swift +++ b/pitch/Sources/Pitch/PitchGraphView.swift @@ -78,6 +78,7 @@ private struct TraceCanvas: View { let visible = settings.visibleSeconds let live = engine.currentLive() + drawNotes(ctx: &ctx, layout: layout, now: now, visible: visible) drawBand(ctx: &ctx, size: size, layout: layout, live: live) drawTrace(ctx: &ctx, layout: layout, now: now, visible: visible) drawPill(ctx: &ctx, size: size, layout: layout, live: live) @@ -91,6 +92,32 @@ private struct TraceCanvas: View { private static let clockOffset: Double = CACurrentMediaTime() - Date().timeIntervalSinceReferenceDate + // Piano-roll bars for the segmented "intentional" notes, drawn behind the + // live trace. Committed notes are solid; the note being sung right now is + // brighter and grows at the leading edge. + private func drawNotes(ctx: inout GraphicsContext, layout: PitchLayout, + now: Double, visible: Double) { + let (committed, pending) = engine.noteSnapshot(since: now - visible - 1) + // Bars are taller than the trace (4.2 px) so the note reads as a distinct + // block with the pitch line threading through it, not a sliver the trace + // hides. A crisp outline defines each note's edges. + let h = min(layout.rowHeight * 0.85, 26) + + func bar(_ n: NoteEvent, fill: Color) { + let x0 = max(0, layout.x(n.onset, now: now, visible: visible)) + let x1 = min(layout.plotWidth, layout.x(n.offset, now: now, visible: visible)) + guard x1 > x0 else { return } + let yy = layout.y(Double(n.midi)) + let rect = CGRect(x: x0, y: yy - h / 2, width: x1 - x0, height: h) + let path = Path(roundedRect: rect, cornerRadius: min(5, h / 2)) + ctx.fill(path, with: .color(fill)) + ctx.stroke(path, with: .color(palette.noteBarEdge), lineWidth: 1) + } + + for n in committed { bar(n, fill: palette.noteBar) } + if let p = pending { bar(p, fill: palette.notePending) } + } + // Thin solid coral line marking the nearest note — a couple of pixels // thicker than a staff line. private func drawBand(ctx: inout GraphicsContext, size: CGSize, diff --git a/pitch/Sources/Pitch/Theme.swift b/pitch/Sources/Pitch/Theme.swift index d969ec70afd25108e6b97f47412a47b4464a95a1..eb15beafdd8b51a51d2564fc65e16d887a18c3fe 100644 --- a/pitch/Sources/Pitch/Theme.swift +++ b/pitch/Sources/Pitch/Theme.swift @@ -15,6 +15,9 @@ struct Palette { var pillText: Color var trace: Color var dot: Color + var noteBar: Color // committed "intentional" note + var notePending: Color // the note being sung right now + var noteBarEdge: Color // crisp outline so bars read under the trace static func make(_ scheme: ColorScheme) -> Palette { scheme == .dark ? .dark : .light @@ -31,7 +34,10 @@ struct Palette { pill: Color(red: 0.93, green: 0.49, blue: 0.45), pillText: Color.white, trace: Color(red: 0.20, green: 0.24, blue: 0.31), - dot: Color(red: 0.20, green: 0.24, blue: 0.31)) + dot: Color(red: 0.20, green: 0.24, blue: 0.31), + noteBar: Color(red: 0.36, green: 0.52, blue: 0.90).opacity(0.28), + notePending: Color(red: 0.36, green: 0.52, blue: 0.90).opacity(0.44), + noteBarEdge: Color(red: 0.28, green: 0.44, blue: 0.85).opacity(0.65)) static let dark = Palette( background: Color(red: 0.09, green: 0.10, blue: 0.12), @@ -44,7 +50,10 @@ struct Palette { pill: Color(red: 0.90, green: 0.47, blue: 0.44), pillText: Color.white, trace: Color(red: 0.93, green: 0.95, blue: 0.99), - dot: Color(red: 0.93, green: 0.95, blue: 0.99)) + dot: Color(red: 0.93, green: 0.95, blue: 0.99), + noteBar: Color(red: 0.55, green: 0.68, blue: 0.99).opacity(0.26), + notePending: Color(red: 0.55, green: 0.68, blue: 0.99).opacity(0.44), + noteBarEdge: Color(red: 0.62, green: 0.74, blue: 1.0).opacity(0.7)) } /// Shared plot geometry so the static grid layer and the animated trace layer diff --git a/pitch/Sources/Pitch/TopBar.swift b/pitch/Sources/Pitch/TopBar.swift index 65be20863a14c4bdb15f696548461df9e712c2ee..7dcca49b9b31ffcc63ee2f5a44f5dd0cbc02820b 100644 --- a/pitch/Sources/Pitch/TopBar.swift +++ b/pitch/Sources/Pitch/TopBar.swift @@ -52,6 +52,7 @@ struct TopBar: View { Spacer(minLength: 8) + KeyPill(engine: engine, settings: settings) StatusPill(engine: engine) } .padding(.horizontal, 16) @@ -92,6 +93,44 @@ private struct Dropdown: View { } } +/// Live key estimate (Krumhansl–Schmuckler) shown in the top bar. Dims when the +/// fit is weak so a shaky guess reads as tentative. +private struct KeyPill: View { + @ObservedObject var engine: AudioEngine + @ObservedObject var settings: Settings + + var body: some View { + HStack(spacing: 6) { + Image(systemName: "music.note") + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(.secondary) + Text(text) + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.secondary) + .lineLimit(1) + } + .opacity(opacity) + .padding(.horizontal, 10) + .padding(.vertical, 5) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color(nsColor: .controlBackgroundColor)) + .overlay(RoundedRectangle(cornerRadius: 8).stroke(Color(nsColor: .separatorColor), lineWidth: 1)) + ) + .help("Estimated key (Krumhansl–Schmuckler), from the notes you've sung") + } + + private var text: String { + guard let k = engine.keyEstimate else { return "Key —" } + return "Key " + Music.keyName(k, naming: settings.naming, transpose: settings.transpose) + } + + private var opacity: Double { + guard let k = engine.keyEstimate else { return 0.55 } + return 0.6 + 0.4 * min(1, k.confidence / 0.1) + } +} + private struct StatusPill: View { @ObservedObject var engine: AudioEngine diff --git a/recorder/engine/Sources/recorder/CaptureEngine.swift b/recorder/engine/Sources/recorder/CaptureEngine.swift index e3b5c49c02d9040fb35bf729764f650303a40475..d81d80dda9c7438f19f527d2c0446cedb33f7916 100644 --- a/recorder/engine/Sources/recorder/CaptureEngine.swift +++ b/recorder/engine/Sources/recorder/CaptureEngine.swift @@ -276,15 +276,21 @@ final class CaptureEngine { fallbackDir: safeRoot) } - private func makeAudioWriter(name: String, kind: String) throws -> StreamWriter { + private func makeAudioWriter(name: String, kind: String, channels: Int = 2) throws -> StreamWriter { // AAC in .m4a: ~256 kbps stereo, transparent for voice/desktop and tiny next // to the uncompressed PCM we used to write. Audio is the irreplaceable // stream and costs ~115 MB/hour, so it records to the safe (internal) disk // rather than the removable scratch drive. + // + // `channels` must match the source: system audio is genuinely stereo, but a + // mono mic forced into a 2-channel file lands entirely in channel 0 (left) + // and leaves the right dead silent — the file then plays only in the left + // ear on headphones. Writing a true mono file lets AVFoundation upmix it to + // both channels on playback. let settings: [String: Any] = [ AVFormatIDKey: kAudioFormatMPEG4AAC, AVSampleRateKey: 48_000, - AVNumberOfChannelsKey: 2, + AVNumberOfChannelsKey: channels, AVEncoderBitRateKey: 256_000, ] return try StreamWriter( @@ -293,6 +299,14 @@ final class CaptureEngine { fallbackDir: safeRoot) } + /// Channel count of an audio capture device's active format (1 for a typical + /// built-in or USB mic), clamped to at least 1. Used to size the mic writer. + private static func channelCount(of device: AVCaptureDevice) -> Int { + guard let asbd = CMAudioFormatDescriptionGetStreamBasicDescription( + device.activeFormat.formatDescription) else { return 1 } + return max(1, Int(asbd.pointee.mChannelsPerFrame)) + } + // MARK: AVCapture (mic + camera) private func startAVCapture(audioUID: String? = nil, videoUID: String? = nil) throws { @@ -307,7 +321,11 @@ final class CaptureEngine { guard session.canAddInput(input) else { throw RecorderError("cannot add mic input") } session.addInput(input) - let writer = try makeAudioWriter(name: "mic", kind: "mic") + // Match the file's channel count to the device so a mono mic records as a + // true mono file (which plays in both ears) rather than a stereo file with + // a dead right channel. Falls back to mono if the format can't be read. + let micChannels = Self.channelCount(of: device) + let writer = try makeAudioWriter(name: "mic", kind: "mic", channels: micChannels) writer.deviceUID = device.uniqueID sinks.append(writer) diff --git a/sequencer/CLAUDE.md b/sequencer/CLAUDE.md index 3c24098b91c0839ad454364fda60dcfba1d206a5..3a0f69b0a1db32b6da3dbc60a735d6c368d4f1a0 100644 --- a/sequencer/CLAUDE.md +++ b/sequencer/CLAUDE.md @@ -5,11 +5,14 @@ Sequencer ("Clover Sequencer") is a native macOS app, not a video editor. It hel ## Build & run ```sh -swift build # compile -./run.sh # build, kill running instance, copy binary into Sequencer.app, relaunch +swift build # compile (debug — for the CLI harnesses below) +./run.sh # build RELEASE, kill running instance, copy into Sequencer.app, relaunch +./run.sh --debug # same but ship the debug build (crash-chasing only) ``` -`run.sh` is the standard dev loop — it copies `.build/debug/Sequencer` into `Sequencer.app/Contents/MacOS/Sequencer` and reopens the app bundle (needed for a proper app identity/menu bar, not just a bare executable). +`run.sh` is the standard dev loop — it copies the built binary into `Sequencer.app/Contents/MacOS/Sequencer` and reopens the app bundle (needed for a proper app identity/menu bar, not just a bare executable). **It ships the `-c release` build by default**: timeline drawing is ~4× slower under `-Onone`, and shipping debug binaries is how the app spent months feeling slower than it was. Judge any perf perception against release. + +Launching the binary directly with `SEQ_DRAWPROF=1` in the environment (stderr to a file — NSLog is invisible under `open`) prints one `[drawprof]` line per second with draws/sec and mean/max ms per `TimelineView.draw` — the GUI-side counterpart to `--perftest`. There is no test target in Package.swift. Verification instead happens through two CLI-flag-driven harnesses baked into `main.swift`: @@ -17,11 +20,27 @@ There is no test target in Package.swift. Verification instead happens through t swift run Sequencer --selftest # headless pipeline check (see Selftest.swift) swift run Sequencer --uitest # offscreen TimelineView harness (see UITest.swift) swift run Sequencer --perftest # offscreen draw-timing harness (see PerfTest.swift) +swift run Sequencer --cachetest [capGB] # cache-budget invariant check (see Cachetest.swift) ``` - `--selftest` exercises the real media pipeline against a file you pass in: ffprobe, filmstrip/waveform generation, chunk-proxy building, AVFoundation playability of the built proxy, and prints sample Fusion Lua output. Useful when touching `MediaPipeline.swift` or `ChunkedProxy.swift`. - `--uitest` hosts the real `TimelineView` in an offscreen window and drives it with synthetic `NSEvent`s (move, trim, slip, stretch, box select, split, links, storyboard split, comp parsing, fades, plus file-format migration/round-trip — ~100 assertions, PASS/FAIL printer). Useful when touching timeline gesture code. -- `--perftest` loads a real `.sq` into an offscreen `TimelineView` and times `draw(_:)` while simulating a horizontal pan, at several zoom levels, in both full-detail and simplified (mid-scroll) modes, printing ms/frame and a per-section breakdown (`DrawProf`). Use it when touching timeline **drawing** — it's how the per-clip draw cost was found. The lesson baked into `TimelineView`: never build/tint an `NSImage(systemSymbolName:)` per clip/element per frame (SF-symbol tinting does a `lockFocus` composite) — bake once via `bakedSymbol(...)`. Clip labels and filmstrips are also gated to at-rest / wide-enough via the `isScrolling` simplified path. +- `--perftest` loads a real `.sq` into an offscreen `TimelineView` and times `draw(_:)` at several zoom levels, printing **median and p90** ms/frame (medians resist the I/O spikes async thumbnail loads inject) plus a per-section `DrawProf` breakdown and tile blit/render counts. Each zoom is measured **direct vs tiled, in the same process** (thermal drift between runs otherwise swamps real deltas — this machine varies 5× with battery/heat), under two pan patterns: `scroll` (8 px/frame, a real gesture) and `jump` (half-project teleports, cache-hostile). Use it when touching timeline **drawing**. Lessons baked into `TimelineView`: never build/tint an `NSImage(systemSymbolName:)` per clip per frame — bake once via `bakedSymbol(...)`; never draw geometry millions of points wide (clamp to the cull window; CG "clips" it but pays anyway); and detail is a function of **on-screen clip width** (`lodMinWidth`), never of whether the user is scrolling. +- `--cachetest` loads a real `.sq` (headless), jumps the playhead across the timeline, and continuously asserts the cache-budget invariant (`ledger ≤ cap`, disk ≤ cap + slack) while demand builds and evictions run — plus coverage of the chunk under the playhead at each stop. Use it when touching the budget/eviction logic in `MediaPipeline.swift` or `ChunkedProxy.swift`. Run it against a project whose full proxy set exceeds the cap (pass a small `capGB`) — that's the regime it exists to test. Don't run it while the GUI app is open (two processes would fight over one cache). + +Cache/proxy debugging levers: `SEQ_BUILDLOG=1` logs every chunk-build START and admission failure (`[cache] start/admission blocked` — success completions are otherwise silent, so this is how you chase "why isn't chunk X building"). The viewer logs every "Loading Media…" spinner occurrence as a `[miss]` line with a cause diagnosis from `ChunkManager.missDiagnosis` (chunk missing/building/queued-where, composition-lag, player-late, budget-starved) and, on recovery, its duration. **All `[miss]` lines and important `[cache]` events (rescues, build/stitch failures, admission blocks, orphan reaping) also append to `~/Library/Logs/Sequencer.log`** (`SeqLog`, 5 MB rotation) — no Console attach or special launch needed; a user-reported spinner sighting is answerable from that file. `sequencer --layertest […]` opens bare AVPlayerLayer tiles playing the real stitched composition(s) (`SEQ_LAYERTEST_T=` sets the seek) — the isolation tool that cracked the black-viewer bug. + +Two hard-won playback failure modes (2026-07-11): (1) **an `AVAssetTrack` does not retain its `AVURLAsset`** — stitching a track whose asset has been deallocated fails with -11800/-12780, and under `try?` that silently left a black GAP in the composition for a chunk that was healthy on disk (`LoadedPart.asset` exists precisely to prevent this; `comp insert FAILED` in the log means it's back). (2) **Orphaned ffmpeg encoders** (app killed mid-build — run.sh pkills on every rebuild) hold VideoToolbox decode sessions from a machine-wide pool; a dozen accumulated orphans make every AVPlayer render black with zero errors anywhere (items ready, seeks land, `isReadyForDisplay` true). Defenses: `MediaPipeline.reapOrphans` kills stale ffmpegs referencing our cache at launch, `terminateChildren` runs on quit and SIGTERM/SIGINT, and run.sh sweeps strays. + +Bundle id: `net.paperclover.Sequencer` (defaults domain too; renamed from com.clover.Sequencer 2026-07-12). Document UTI: `net.paperclover.sequencer.project` — keep `ProjectDocumentController.projectType` and build.sh's Info.plist in sync. + +`ProjectModel.laneRefs` (and `hasStoryboard`) scan every clip — O(clips). Never call them per-clip in a draw or per-frame path: `laneRect→laneRefs` per drawn clip was quadratic and pegged a core at 98% on big projects. `TimelineView` uses its `laneRows` cache (invalidated in `redraw()`/`ctx.didSet`); route new hot paths through that. + +Two hard-won main-thread rules (2026-07-12): (1) **`URL(fileURLWithPath:)` without `isDirectory:` stats the path** — `MediaItem.url`/`displayName` did this per clip label per timeline draw against NAS paths, freezing the UI whenever the volume was cold (292/337 draw samples in `stat()`). Always pass `isDirectory:` for known-file paths; `displayName` uses `(path as NSString).lastPathComponent` (no I/O). (2) **Nothing between `thread_suspend` and `thread_resume` may allocate or lock** — the hang sampler's `Array.append` needed the malloc lock the suspended main thread held → permanent app freeze (the "unusably laggy on startup" report). `sampleMainStack` uses preallocated buffers; keep it allocation-free. Related: the SIGTERM/SIGINT DispatchSources live on a background queue, not main — a wedged main thread must not make the app unkillable. + +Microhangs/stutter: `HangMonitor` (a watchdog thread pinging the main queue every 20 ms) logs every main-thread stall >100 ms as a `[hang]` line in `~/Library/Logs/Sequencer.log` — duration, whether playback was running (`during playback (rate …)`), and a **symbolicated stack of the main thread sampled mid-stall** (mach `thread_suspend` + arm64 frame-pointer walk + `swift_demangle`), so the culprit is named without Instruments. `SEQ_NOHANGWATCH=1` disables it; `SEQ_HANGTEST=1` injects a deliberate 0.4 s spin ~2 s after launch to verify the pipeline end-to-end. User-reported stutter during long sessions is answerable from that file. + +RAM: the `maxRAMGB` default (Settings → Global, default 2) budgets `FrameCache` — decoded exact stand-in frames for cut boundaries and not-yet-presentable moments (warmed ahead by `VideoTrackPlayer.warmBoundaryFrames`, on demand by the viewer; `SEQ_NOWARM=1` disables decodes) — and scales the video players' forward buffer (`ramGB/2` seconds, capped at 8). Requires `ffmpeg`/`ffprobe` on PATH for anything touching media (probing, filmstrips, proxies). @@ -48,7 +67,7 @@ Non-undoable session/UI state (track hide/focus, pane heights, laneScale, snappi ### Per-document architecture (`DocumentContext.swift`, `Document.swift`, `WindowController.swift`) -Each open project is a `ProjectDocument: NSDocument` owning a `DocumentContext` — the per-document service bag. Everything per-project is an instance on it: `ctx.store`, `ctx.playback`, `ctx.players`, `ctx.chunks`, `ctx.comps`, `ctx.boards`, `ctx.session`. Each service holds an `unowned var ctx` back-reference, so service-to-service calls go through `ctx.*`. Views reach their state through a stored `var ctx` injected at construction (`SequencerWindowController` sets `timeline/viewer/transport`'s `ctx`; the grid injects its cells; the storyboard editor is re-targeted per `open(clipId:ctx:)`). **Do not reach for a global `.shared` for per-project state** — only `MediaPipeline` (content-addressed media cache) and `Theme` are genuinely global. `DocumentContext.current` resolves the front document's context for app-level actions (Settings/Export/cache eviction); `DocumentContext.headless` backs the `--uitest`/`--selftest` harnesses. +Each open project is a `ProjectDocument: NSDocument` owning a `DocumentContext` — the per-document service bag. Everything per-project is an instance on it: `ctx.store`, `ctx.playback`, `ctx.players`, `ctx.chunks`, `ctx.comps`, `ctx.boards`, `ctx.session`. Each service holds an `unowned var ctx` back-reference, so service-to-service calls go through `ctx.*`. **Because `ctx` is `unowned`, any service that lands an async callback (off-main chunk builds, the 60 Hz clock) must stop touching `ctx` once the document is closing** — `ProjectDocument.close()` calls `DocumentContext.shutdown()`, which flips `ChunkManager.stop()` / `PlaybackController.stop()` (both guard their ctx-touching entry points) *before* the context deallocs, so a build finishing after close can't trap on a dangling reference. Add the same guard to any new service that schedules deferred work. Views reach their state through a stored `var ctx` injected at construction (`SequencerWindowController` sets `timeline/viewer/transport`'s `ctx`; the grid injects its cells; the storyboard editor is re-targeted per `open(clipId:ctx:)`). **Do not reach for a global `.shared` for per-project state** — only `MediaPipeline` (content-addressed media cache) and `Theme` are genuinely global. `DocumentContext.current` resolves the front document's context for app-level actions (Settings/Export/cache eviction); `DocumentContext.headless` backs the `--uitest`/`--selftest` harnesses. `SequencerWindowController` (one per window) owns the split layout, previews pop-out, and every per-document menu action (`@objc func`s targeting the first responder). `AppDelegate` is now slim: it builds the menu bar and handles app-level actions only (New/Open route to `NSDocumentController`; Save/Save As/Close to `NSDocument`). @@ -59,7 +78,7 @@ Each open project is a `ProjectDocument: NSDocument` owning a `DocumentContext` This is the most complex subsystem — a three-stage pipeline. `ChunkManager` and `PlaybackController`/`PlayerManager` are per-document (`ctx.chunks`, `ctx.playback`, `ctx.players`); `MediaPipeline` is the one global (a shared content-addressed cache): 1. **`MediaPipeline`** (`MediaPipeline.swift`) — wraps ffprobe/ffmpeg. Probes media, generates filmstrips (thumbnail strips) and waveforms async, LRU content-addressed cache (default 50GB). Cache key = `SHA256(path|size|mtime)`, so moved/remounted files with identical content still hit cache. -2. **`ChunkManager`** (`ChunkedProxy.swift`) — demand-driven **30-second ProRes Proxy chunks** instead of whole-file transcodes. **Build order is the whole game** (`updateDemand` → `nextJob` → `pump`): every playback tick recomputes a best-first `demand` list from the playhead, playback direction, and visibility — for each video clip in a look-ahead window it scores the proxy chunks its source range needs (coverage before sharpening, visible/focused tracks before hidden, nearer the playhead in the playback direction before farther) so the frame you're about to see is always built first. Chunks outside the window fall to the whole-project background fill (`ensure`). Adaptive quality (4 resolution/fps tiers) steps down/up based on measured encode wall-time vs. realtime ratio and distinguishes network vs. encode bottlenecks; **imminent** coverage (uncovered, just ahead, visible) builds at that adaptive realtime quality so it lands in time, everything else at the full preview-quality target. Composes ready chunks + original-file fallback into a **per-media** `AVComposition` (`composition(for:)`), versioned so playback only swaps on a strict upgrade. Per-media (not per-track) is deliberate: only the clip under the playhead needs building, so the current frame is ready fast — a whole-track composition would have to assemble the entire timeline before it could show anything, which is far too slow for a big multicut project. +2. **`ChunkManager`** (`ChunkedProxy.swift`) — demand-driven **30-second ProRes Proxy chunks** instead of whole-file transcodes. **Build order is the whole game** (`updateDemand` → `nextJob` → `pump`): every playback tick recomputes a best-first `demand` list from the playhead, playback direction, and visibility — for each video clip in a look-ahead window it scores the proxy chunks its source range needs (coverage before sharpening, visible/focused tracks before hidden, nearer the playhead in the playback direction before farther) so the frame you're about to see is always built first. Chunks outside the window fall to the whole-project background fill (`ensure`). Adaptive quality (4 resolution/fps tiers) steps down/up based on measured encode wall-time vs. realtime ratio and distinguishes network vs. encode bottlenecks; **imminent** coverage (uncovered, just ahead, visible) builds at that adaptive realtime quality so it lands in time, everything else at the full preview-quality target. **Rescue slices**: when the playhead lands INSIDE an uncovered chunk, a short (~10 s) slice starting at the playhead builds first (`rNNNNNN.mov`, one quality rung lower) — read-bound NAS sources scale with encoded seconds, so this lands in a few seconds where quality drops can't help — then the full chunk builds right behind it and deletes the slice. Slice state (`MediaState.partial`) is session-only; stale slice files are purged by the launch reconcile. In queries: `isCovered` is true inside the slice's range, `builtWidths` reports a slice as width 1 (so the full build reads as a strict upgrade and the player adopts it), demand scoring still counts the chunk uncovered (so the full build stays queued). Trim/slip/ripple drags call `noteGestureExposure` mid-gesture so newly exposed source ranges build before mouse-up; committed edits warm the *edited edge* (`Store.noteEditLocus`). Composes ready chunks + rescue slices + original-file fallback into a **per-media** `AVComposition` (`composition(for:)`, chunk assets loaded concurrently), versioned so playback only swaps on a strict upgrade. Per-media (not per-track) is deliberate: only the clip under the playhead needs building, so the current frame is ready fast — a whole-track composition would have to assemble the entire timeline before it could show anything, which is far too slow for a big multicut project. 3. **`PlaybackController`** + `PlayerManager` (`PlaybackController.swift`) — master clock anchored to `CACurrentMediaTime()`; all players chase this one authoritative time at 60Hz. Supports reverse and J/K/L shuttle speeds (±1/2/4/8/16/32/64). Each video track is a **`VideoTrackPlayer`: two `AVPlayer`s (front/back) for gapless cuts.** The front shows the clip under the playhead (`currentTime` == source time, chasing at `rate × clip.speed`); as a cut to a *different media* approaches (~1.5 s out) the back is prerolled to the next clip's first frame — loaded, decoded, parked, muted — and at the cut the roles flip. The viewer's cell holds two `AVPlayerLayer`s (one bound to each sub-player, stable) and just toggles which is visible, so the flip is a layer swap with no `replaceCurrentItem` black-frame gap. Same-media continuations don't buffer (the front keeps its item and seeks). `tick()` runs `sync()` (which does the flip) *before* posting `.playheadChanged`, so the viewer reflects the flip on the same frame. Standalone `.audio` clips get one `AVPlayer` per clip (looser sync tolerance since originals live on NAS); a clip's player is **prerolled ~1 s before its cut and kept running (silent — its fade gain is 0 until reached)**, so crossing an audio cut is a seamless volume handover rather than a cold start — see `audioLookahead`/`audioLinger` in `syncAudio`. Item swaps hold the fresh item silent (`rate = 0`) until the immediately-following `syncTime` seeks it to the live position, so a swap never blips wrong content from t=0. `.projectChanged` forces a hard resync only while **paused** — during playback the 60 Hz tick already tracks, so edits away from the playhead don't stutter. Seeks coalesce while one is in-flight. When working on playback bugs, the mental model is: `MediaPipeline` produces cached derived assets → `ChunkManager` decides what to build (playhead/visibility-ordered) and assembles per-media compositions → `PlaybackController` drives the players (a double-buffered pair per video track, one per audio clip) against those compositions on a shared clock. Two levers make cuts gapless: the **build order** gets the next clip's proxy ready ahead of time, and the **double buffer** prerolls it and flips without an item swap. @@ -71,11 +90,21 @@ When working on playback bugs, the mental model is: `MediaPipeline` produces cac ### Storyboard (`Storyboard.swift`, `StoryboardEditor.swift`) -Each `Board` has two layers: a shape (vector) layer stored in the model (`BoardShape` — rect/oval/triangle/star/n-gon/text/image-ref) and a raster (drawing) layer stored as a PNG on disk keyed by board ID. `revision`/version counters invalidate the composite cache independent of model-mutation equality (raster edits don't go through `Store.mutate`). Panels are "start-only" in the model — see `normalizeStoryboards()` above. +Each `Board` has two layers: a shape (vector) layer stored in the model (`BoardShape` — rect/oval/triangle/star/n-gon/text/image-ref) and a raster (drawing) layer stored as a PNG on disk keyed by board ID. `revision`/version counters invalidate the composite cache independent of model-mutation equality (raster pixels live in `BoardStore`, out of the value-type model). **Drawing IS undoable and shares ONE timeline with model edits:** `BoardStore.beginStroke`/`endStroke` capture the pre-stroke image and register it via `Store.recordRasterEdit`, so the `Store` undo stack holds both `.model(Snapshot)` and `.raster(boardId, image)` entries in the order they happened — ⌘Z/⌘⇧Z step through drawing and model edits together, from any window, and drawing redoes like everything else (undo/redo of a raster entry swaps the current drawing for the stored one via `BoardStore.rasterSnapshot`/`applyRaster`). Panels are "start-only" in the model — see `normalizeStoryboards()` above. ### Views -- `TimelineView.swift` — largest file (~2700 lines); all editing gestures (move/trim/slip/stretch/box-select/split/blade) live here, each gesture wrapped in one `Store` undo step. +- `TimelineView.swift` — largest file (~3500 lines); all editing gestures (move/trim/slip/stretch/box-select/split/blade) live here, each gesture wrapped in one `Store` undo step. + + **Timeline rendering** is built around one rule: frame cost is proportional to what *changed*, not what's visible, and the look never degrades — a 4,740-clip project draws pixel-identically to a 10-clip one. Three mechanisms: + 1. **Pixel-width LOD** (`drawClip`, `lodMinWidth`): a clip narrower than ~3 px physically can't show corners/labels/filmstrips, so it draws as flat rects (body + strip + audio center line + fade-handle slivers) — no bezier/clip-state chrome. This replaced the old `lightScroll` shed-detail-mid-scroll mode, which visibly dropped thumbnails yet saved almost nothing. + 2. **Scene tile cache** (`blitLaneTiles`/`tileImage`): lane clip content rasterizes into per-(lane × 512 pt slice) `CGImage` tiles in *timeline space* (origin-independent), rendered by the same `drawLaneClips` code as the direct path — pixel-equivalent by construction. Pan/playback frames are blits + chrome (~15 ms at fit-all on a throttled M4; was ~500 ms). Pin-to-view-edge labels are an overlay (`drawPinnedLabels`), never baked. Origin/scrollY/lane heights are quantized to the device-pixel grid so blits never resample. Tiles are BGRA in the window's colorspace (an `NSBitmapImageRep` source costs a per-blit swizzle+conversion). Renders are budgeted (`tileRendersPerFrame`) with pixel-identical direct fallback + a scheduled fill pass; big origin jumps render zero tiles that frame. Edit gestures bypass tiles entirely. `SEQ_NOTILES=1` forces the direct path. + 3. **Targeted invalidation**: `.projectChanged`/`.viewOptionsChanged` → scene regroup + tile flush; `.selectionChanged` → link-mate cache + tile flush only; `.mediaStatusChanged` flushes tiles **only when posted with `userInfo["scene"]`** (filmstrip/waveform/board raster landed — `MediaPipeline`/`BoardStore` posts carry it) — proxy-chunk churn during playback repaints without re-rendering. Keep that convention when adding posts. + 4. **Parallel cold frames** (`drawLanesDirectParallel`): zoom-in-flight, teleport jumps, and live edit gestures can't use tiles, so those frames rasterize every lane concurrently into persistent per-lane buffers (same `drawLaneClips` code, composited on main; the storyboard lane stays on main — `BoardStore` isn't locked). Anything `drawLaneClips` touches must therefore be **thread-safe under concurrent reads**: the scene caches are read-only during a draw; `MediaPipeline`'s thumb/waveform/strip-info bookkeeping, `bakedSymbol`, and the CTLine title cache are lock-guarded; `DrawProf` only records on the main thread. `NSGraphicsContext.current` is thread-local, but `concurrentPerform` runs one iteration on the calling thread — save/restore it, never nil it. + + Image assets that the timeline blits per frame (filmstrip thumbs, waveforms) are decoded once into the display's raster format via `MediaPipeline.displayImage` (BGRA premultiplied, screen colorspace) and returned as `CGImage` — a file-format NSImage costs a per-draw swizzle + colorspace conversion. Misses are negatively cached (`thumbMissing`/`waveformMissing`) so a dense timeline doesn't re-dispatch hundreds of no-op loads per frame; the miss sets clear when fresh strips/waveforms land. + + Model-side lookups that draw or hit-test against clips go through the scene caches (`clipsByLane` sorted per lane + binary-searched `visibleIndexRange`, `overlapsByLane`, `overlapIdsByLane`, `cachedTimelineDuration`) — never `project.clips.filter(...)` per frame/event; `Model.overlaps()` relies on its sorted early-`break` to stay near-linear. - `ViewerGridView.swift` — multicam grid, one cell per visible track plus a Fusion comps cell. - `TransportBar.swift`, `ExportDialog.swift`, `ColorPicker.swift`, `Theme.swift` (light/dark, follows system appearance, no manual toggle), `Tools.swift` (tool enum + radial quick-picker). diff --git a/sequencer/Deltarune Ch5.sq/project.json b/sequencer/Deltarune Ch5.sq/project.json deleted file mode 100644 index 474767f697c486ce06c41b116df1df51bc3ae133..0000000000000000000000000000000000000000 --- a/sequencer/Deltarune Ch5.sq/project.json +++ /dev/null @@ -1,11285 +0,0 @@ -{ - "formatVersion" : 2, - "project" : { - "boardHeight" : 1080, - "boardWidth" : 1920, - "clips" : [ - { - "duration" : 8.779661016949152, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0856AA33-6A7E-4E96-BFB1-7B40DCC5C3E2", - "kind" : "video", - "linkId" : "E4929B8D-EE5B-4B9D-850D-B43F8A803E7A", - "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0.8026453438086703, - "start" : 0, - "track" : "v0" - }, - { - "duration" : 8.779661016949152, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F03A433A-DD0D-43A4-B2E0-77976661B8D5", - "kind" : "audio", - "linkId" : "E4929B8D-EE5B-4B9D-850D-B43F8A803E7A", - "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 0.9509002194553415, - "start" : 0, - "track" : "v1" - }, - { - "duration" : 8.779661016949152, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "DDC04C1A-94A8-4F4D-9568-73F07B3E0304", - "kind" : "audio", - "linkId" : "E4929B8D-EE5B-4B9D-850D-B43F8A803E7A", - "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1.2881355932203389, - "start" : 0, - "track" : "v2" - }, - { - "duration" : 8.779661016949152, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "856D7E40-8EF3-4242-AB37-B26CBFD4B3F7", - "kind" : "video", - "linkId" : "E4929B8D-EE5B-4B9D-850D-B43F8A803E7A", - "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0.8603196774655755, - "start" : 0, - "track" : "v3" - }, - { - "duration" : 8.779661016949152, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "ACBD774E-FC95-41F4-B5FB-037577828AD9", - "kind" : "video", - "linkId" : "E4929B8D-EE5B-4B9D-850D-B43F8A803E7A", - "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0.8603078854665893, - "start" : 0, - "track" : "v4" - }, - { - "duration" : 9.288135593220339, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "21E55168-059F-4CFC-AF14-FF43F7E7A9F8", - "kind" : "video", - "linkId" : "7ADB32D0-2D02-491A-8BF7-11F6A47A354E", - "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 11.582306360757823, - "start" : 8.779661016949152, - "track" : "v0" - }, - { - "duration" : 9.288135593220339, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FA932A03-4AEE-428C-9593-EB3CFA09BEBE", - "kind" : "audio", - "linkId" : "7ADB32D0-2D02-491A-8BF7-11F6A47A354E", - "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 11.730561236404494, - "start" : 8.779661016949152, - "track" : "v1" - }, - { - "duration" : 9.288135593220339, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BDB5ABE0-71D2-457D-A7D4-21ADCC3B5743", - "kind" : "audio", - "linkId" : "7ADB32D0-2D02-491A-8BF7-11F6A47A354E", - "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 12.067796610169491, - "start" : 8.779661016949152, - "track" : "v2" - }, - { - "duration" : 9.288135593220339, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2A859930-C3F8-4E02-BD27-4CF36CF2138E", - "kind" : "video", - "linkId" : "7ADB32D0-2D02-491A-8BF7-11F6A47A354E", - "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 11.639980694414728, - "start" : 8.779661016949152, - "track" : "v3" - }, - { - "duration" : 9.288135593220339, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "197B76CD-FB1C-4DB5-BB64-B260CC9CC769", - "kind" : "video", - "linkId" : "7ADB32D0-2D02-491A-8BF7-11F6A47A354E", - "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 11.639968902415742, - "start" : 8.779661016949152, - "track" : "v4" - }, - { - "duration" : 1.322033898305083, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "30FF8D20-D84E-4A9C-8303-F75C6D488959", - "kind" : "video", - "linkId" : "DE8705CE-9B40-487A-A252-50C469C56589", - "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 86.02298432685951, - "start" : 18.067796610169495, - "track" : "v0" - }, - { - "duration" : 1.322033898305083, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "28F8C52E-77E0-4886-9A6F-49E8524EFDB0", - "kind" : "audio", - "linkId" : "DE8705CE-9B40-487A-A252-50C469C56589", - "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 86.17123920250619, - "start" : 18.067796610169495, - "track" : "v1" - }, - { - "duration" : 1.322033898305083, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "385FB864-B457-4585-A1E2-B07F0B398682", - "kind" : "audio", - "linkId" : "DE8705CE-9B40-487A-A252-50C469C56589", - "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 86.50847457627118, - "start" : 18.067796610169495, - "track" : "v2" - }, - { - "duration" : 1.322033898305083, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4703FBC9-8E06-4DE3-A460-19695096A67B", - "kind" : "video", - "linkId" : "DE8705CE-9B40-487A-A252-50C469C56589", - "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 86.08065866051642, - "start" : 18.067796610169495, - "track" : "v3" - }, - { - "duration" : 1.322033898305083, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "14297849-4D4F-41D8-A59A-15B5B1D60DF2", - "kind" : "video", - "linkId" : "DE8705CE-9B40-487A-A252-50C469C56589", - "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 86.08064686851743, - "start" : 18.067796610169495, - "track" : "v4" - }, - { - "duration" : 1.1525423728813529, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5EF423FA-AD82-4C89-8385-335325345015", - "kind" : "video", - "linkId" : "66157B6F-FF8D-490F-B510-26417942AA98", - "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 113.98908602177477, - "start" : 19.389830508474578, - "track" : "v0" - }, - { - "duration" : 1.1525423728813529, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "95F0F7BD-FA8B-48FE-A975-859CBDE2F60A", - "kind" : "audio", - "linkId" : "66157B6F-FF8D-490F-B510-26417942AA98", - "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 114.13734089742144, - "start" : 19.389830508474578, - "track" : "v1" - }, - { - "duration" : 1.1525423728813529, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "236F5693-66F0-4DF2-9C6B-0FC10DC039A0", - "kind" : "audio", - "linkId" : "66157B6F-FF8D-490F-B510-26417942AA98", - "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 114.47457627118644, - "start" : 19.389830508474578, - "track" : "v2" - }, - { - "duration" : 1.1525423728813529, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "08DF237F-434A-4ADB-B77C-F165BA3B9BAE", - "kind" : "video", - "linkId" : "66157B6F-FF8D-490F-B510-26417942AA98", - "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 114.04676035543167, - "start" : 19.389830508474578, - "track" : "v3" - }, - { - "duration" : 1.1525423728813529, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6CEFB0C1-9FA6-4727-B291-AE95CBF63C1E", - "kind" : "video", - "linkId" : "66157B6F-FF8D-490F-B510-26417942AA98", - "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 114.04674856343269, - "start" : 19.389830508474578, - "track" : "v4" - }, - { - "duration" : 5.220338983050851, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "CBC234E0-371D-4FDA-99B5-0BB7DE1737EB", - "kind" : "video", - "linkId" : "CEA52F0A-38E4-4F7B-AB96-117218501790", - "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 397.9212894116052, - "start" : 20.54237288135596, - "track" : "v0" - }, - { - "duration" : 5.220338983050851, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A4F89301-A2DE-4597-AD9E-DC51F3507355", - "kind" : "audio", - "linkId" : "CEA52F0A-38E4-4F7B-AB96-117218501790", - "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 398.06954428725186, - "start" : 20.54237288135596, - "track" : "v1" - }, - { - "duration" : 5.220338983050851, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F00A71B2-649B-4BF6-9C0A-80270331C353", - "kind" : "audio", - "linkId" : "CEA52F0A-38E4-4F7B-AB96-117218501790", - "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 398.40677966101686, - "start" : 20.54237288135596, - "track" : "v2" - }, - { - "duration" : 5.220338983050851, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "31A4F820-D7C2-4CAB-8A20-F52F615C6114", - "kind" : "video", - "linkId" : "CEA52F0A-38E4-4F7B-AB96-117218501790", - "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 397.9789637452621, - "start" : 20.54237288135596, - "track" : "v3" - }, - { - "duration" : 5.220338983050851, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4B6EF408-0BAE-4495-885B-A0281AFD0B46", - "kind" : "video", - "linkId" : "CEA52F0A-38E4-4F7B-AB96-117218501790", - "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 397.9789519532631, - "start" : 20.54237288135596, - "track" : "v4" - }, - { - "duration" : 14.57627118644065, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B386A5EC-7B75-421C-941E-9A2E8FC4C303", - "kind" : "video", - "linkId" : "3688DE28-A2CB-4ED5-9666-9772A7FAF5FD", - "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 422.73484873363907, - "start" : 25.76271186440681, - "track" : "v0" - }, - { - "duration" : 14.57627118644065, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "78D95643-9E92-4665-A49E-1640916F222F", - "kind" : "audio", - "linkId" : "3688DE28-A2CB-4ED5-9666-9772A7FAF5FD", - "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 422.88310360928574, - "start" : 25.76271186440681, - "track" : "v1" - }, - { - "duration" : 14.57627118644065, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3EC46637-04E5-407D-90BD-BB290FB532C6", - "kind" : "audio", - "linkId" : "3688DE28-A2CB-4ED5-9666-9772A7FAF5FD", - "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 423.22033898305074, - "start" : 25.76271186440681, - "track" : "v2" - }, - { - "duration" : 14.57627118644065, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "54D04272-5607-4E88-BBAB-A50A9FEAA953", - "kind" : "video", - "linkId" : "3688DE28-A2CB-4ED5-9666-9772A7FAF5FD", - "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 422.792523067296, - "start" : 25.76271186440681, - "track" : "v3" - }, - { - "duration" : 14.57627118644065, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7B34B91A-3E53-4B21-A7C7-22172D7442EA", - "kind" : "video", - "linkId" : "3688DE28-A2CB-4ED5-9666-9772A7FAF5FD", - "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 422.792511275297, - "start" : 25.76271186440681, - "track" : "v4" - }, - { - "duration" : 10.644067796610166, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3EDF6E35-1C81-4706-878F-E685962438E7", - "kind" : "video", - "linkId" : "E6380C0A-8BCB-4E01-A3FF-4B6CE609EBA7", - "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 447.0399334794017, - "start" : 40.33898305084746, - "track" : "v0" - }, - { - "duration" : 10.644067796610166, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5E3BFA57-DA8E-47B2-969D-026F31AD2A3C", - "kind" : "audio", - "linkId" : "E6380C0A-8BCB-4E01-A3FF-4B6CE609EBA7", - "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 447.1881883550484, - "start" : 40.33898305084746, - "track" : "v1" - }, - { - "duration" : 10.644067796610166, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8E2AD73C-7F9E-4F46-8D04-EF9087EE690A", - "kind" : "audio", - "linkId" : "E6380C0A-8BCB-4E01-A3FF-4B6CE609EBA7", - "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 447.5254237288134, - "start" : 40.33898305084746, - "track" : "v2" - }, - { - "duration" : 10.644067796610166, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F54DE68B-6E96-4BA9-BEB6-81E4B984892C", - "kind" : "video", - "linkId" : "E6380C0A-8BCB-4E01-A3FF-4B6CE609EBA7", - "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 447.09760781305863, - "start" : 40.33898305084746, - "track" : "v3" - }, - { - "duration" : 10.644067796610166, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C77A294D-6278-4BAD-8BC5-F7BC3C372A00", - "kind" : "video", - "linkId" : "E6380C0A-8BCB-4E01-A3FF-4B6CE609EBA7", - "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 447.09759602105964, - "start" : 40.33898305084746, - "track" : "v4" - }, - { - "duration" : 9.694915254237287, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BA4DCFDD-E47E-4485-86C7-5D2F91DAD82A", - "kind" : "video", - "linkId" : "6CC9888B-E11F-41E5-BAC2-A15C7D015557", - "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 495.4128148353339, - "start" : 50.983050847457626, - "track" : "v0" - }, - { - "duration" : 9.694915254237287, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F7D2760F-4578-4319-9C36-40A5E0C0554A", - "kind" : "audio", - "linkId" : "6CC9888B-E11F-41E5-BAC2-A15C7D015557", - "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 495.5610697109806, - "start" : 50.983050847457626, - "track" : "v1" - }, - { - "duration" : 9.694915254237287, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "344797CD-1399-4A2F-B5D4-ED0DAFF921B2", - "kind" : "audio", - "linkId" : "6CC9888B-E11F-41E5-BAC2-A15C7D015557", - "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 495.8983050847456, - "start" : 50.983050847457626, - "track" : "v2" - }, - { - "duration" : 9.694915254237287, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "436EE031-3821-460A-B1E6-362C6AC7DD15", - "kind" : "video", - "linkId" : "6CC9888B-E11F-41E5-BAC2-A15C7D015557", - "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 495.4704891689908, - "start" : 50.983050847457626, - "track" : "v3" - }, - { - "duration" : 9.694915254237287, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "DA7C94D5-26B6-465A-958E-DA029F1CEC3F", - "kind" : "video", - "linkId" : "6CC9888B-E11F-41E5-BAC2-A15C7D015557", - "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 495.4704773769918, - "start" : 50.983050847457626, - "track" : "v4" - }, - { - "duration" : 12.440677966101696, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EF8B18B2-7BEB-4520-9C17-903C1A223C06", - "kind" : "video", - "linkId" : "8D7F9D29-68E0-4E62-B7D7-B149575B3269", - "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 511.0399334794017, - "start" : 60.67796610169491, - "track" : "v0" - }, - { - "duration" : 12.440677966101696, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "65C8245D-4AA2-4534-BDAF-3D2841DC3F70", - "kind" : "audio", - "linkId" : "8D7F9D29-68E0-4E62-B7D7-B149575B3269", - "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 511.1881883550484, - "start" : 60.67796610169491, - "track" : "v1" - }, - { - "duration" : 12.440677966101696, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8ECF7044-2C74-43E6-9219-036DB453AA8E", - "kind" : "audio", - "linkId" : "8D7F9D29-68E0-4E62-B7D7-B149575B3269", - "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 511.5254237288134, - "start" : 60.67796610169491, - "track" : "v2" - }, - { - "duration" : 12.440677966101696, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E0691830-C2E3-4692-A443-C96E79034416", - "kind" : "video", - "linkId" : "8D7F9D29-68E0-4E62-B7D7-B149575B3269", - "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 511.09760781305863, - "start" : 60.67796610169491, - "track" : "v3" - }, - { - "duration" : 12.440677966101696, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1DF28AA1-85FC-4AA6-8BCC-86CF38B06C1B", - "kind" : "video", - "linkId" : "8D7F9D29-68E0-4E62-B7D7-B149575B3269", - "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 511.09759602105964, - "start" : 60.67796610169491, - "track" : "v4" - }, - { - "duration" : 2.0677966101694807, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4937BF52-96D7-4AF0-B222-22E27B797A5C", - "kind" : "video", - "linkId" : "E5D93442-8B82-4040-8E5F-BD717FD75859", - "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 539.9212894116051, - "start" : 73.11864406779661, - "track" : "v0" - }, - { - "duration" : 2.0677966101694807, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "16FFB49E-AD0E-430F-BC1E-0E649D37B469", - "kind" : "audio", - "linkId" : "E5D93442-8B82-4040-8E5F-BD717FD75859", - "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 540.0695442872518, - "start" : 73.11864406779661, - "track" : "v1" - }, - { - "duration" : 2.0677966101694807, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3772DE62-3FC2-491B-9DBC-922F6DDC5BB1", - "kind" : "audio", - "linkId" : "E5D93442-8B82-4040-8E5F-BD717FD75859", - "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 540.4067796610168, - "start" : 73.11864406779661, - "track" : "v2" - }, - { - "duration" : 2.0677966101694807, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4A62F7EC-EFF6-4CE8-82FA-024FC1AE5421", - "kind" : "video", - "linkId" : "E5D93442-8B82-4040-8E5F-BD717FD75859", - "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 539.978963745262, - "start" : 73.11864406779661, - "track" : "v3" - }, - { - "duration" : 2.0677966101694807, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7E5889B3-52E4-48AD-8E76-DCAC57E2F26C", - "kind" : "video", - "linkId" : "E5D93442-8B82-4040-8E5F-BD717FD75859", - "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 539.978951953263, - "start" : 73.11864406779661, - "track" : "v4" - }, - { - "duration" : 4.542372881355931, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "779C851A-2F3C-4B84-8EDB-99C3D657B311", - "kind" : "video", - "linkId" : "DCA0DA82-D677-4F94-8ECD-EC2EE408A444", - "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 552.0229843268594, - "start" : 75.18644067796609, - "track" : "v0" - }, - { - "duration" : 4.542372881355931, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6897606B-DA0A-4825-B6DB-325E170FC9B9", - "kind" : "audio", - "linkId" : "DCA0DA82-D677-4F94-8ECD-EC2EE408A444", - "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 552.1712392025061, - "start" : 75.18644067796609, - "track" : "v1" - }, - { - "duration" : 4.542372881355931, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "07934764-AD42-4089-80B6-B244015447CF", - "kind" : "audio", - "linkId" : "DCA0DA82-D677-4F94-8ECD-EC2EE408A444", - "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 552.508474576271, - "start" : 75.18644067796609, - "track" : "v2" - }, - { - "duration" : 4.542372881355931, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EE2670C2-1AAA-40FC-A411-8CE33185992E", - "kind" : "video", - "linkId" : "DCA0DA82-D677-4F94-8ECD-EC2EE408A444", - "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 552.0806586605163, - "start" : 75.18644067796609, - "track" : "v3" - }, - { - "duration" : 4.542372881355931, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "83DC8C97-0117-495D-AB6F-488575028DF6", - "kind" : "video", - "linkId" : "DCA0DA82-D677-4F94-8ECD-EC2EE408A444", - "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 552.0806468685173, - "start" : 75.18644067796609, - "track" : "v4" - }, - { - "duration" : 2.542372881355945, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "75DD8465-3E74-4576-A287-6F0B7A25885F", - "kind" : "video", - "linkId" : "B3514666-D79A-43F4-96AF-77F14857D9AE", - "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 565.1077300895713, - "start" : 79.76271186440677, - "track" : "v0" - }, - { - "duration" : 2.542372881355945, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B5BC5C0D-5C8F-4616-954E-F0544734C4CC", - "kind" : "audio", - "linkId" : "B3514666-D79A-43F4-96AF-77F14857D9AE", - "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 565.255984965218, - "start" : 79.76271186440677, - "track" : "v1" - }, - { - "duration" : 2.542372881355945, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "743266F1-8759-428D-8500-328A33B14EE7", - "kind" : "audio", - "linkId" : "B3514666-D79A-43F4-96AF-77F14857D9AE", - "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 565.593220338983, - "start" : 79.76271186440677, - "track" : "v2" - }, - { - "duration" : 2.542372881355945, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "99C8EB5C-633C-407A-B29D-0F563C4A2A11", - "kind" : "video", - "linkId" : "B3514666-D79A-43F4-96AF-77F14857D9AE", - "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 565.1654044232282, - "start" : 79.76271186440677, - "track" : "v3" - }, - { - "duration" : 2.542372881355945, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F5A5A67A-1C13-4920-94A3-D63379305184", - "kind" : "video", - "linkId" : "B3514666-D79A-43F4-96AF-77F14857D9AE", - "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 565.1653926312292, - "start" : 79.76271186440677, - "track" : "v4" - }, - { - "duration" : 7.457627118644069, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C4F9A637-30B0-4050-B75E-6C87C85699A8", - "kind" : "video", - "linkId" : "E6C60A60-B509-4E76-A1F1-797378C6C7EF", - "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 792.0229843268595, - "start" : 82.30508474576271, - "track" : "v0" - }, - { - "duration" : 7.457627118644069, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D0EF66D9-DF73-4099-BAA1-CD7E3DC03441", - "kind" : "audio", - "linkId" : "E6C60A60-B509-4E76-A1F1-797378C6C7EF", - "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 792.1712392025062, - "start" : 82.30508474576271, - "track" : "v1" - }, - { - "duration" : 7.457627118644069, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FACBA48B-CB80-4E8C-AAF6-55705F37FDB8", - "kind" : "audio", - "linkId" : "E6C60A60-B509-4E76-A1F1-797378C6C7EF", - "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 792.5084745762712, - "start" : 82.30508474576271, - "track" : "v2" - }, - { - "duration" : 7.457627118644069, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "24F56A9C-9D34-412D-AAFA-816D98E1BF4C", - "kind" : "video", - "linkId" : "E6C60A60-B509-4E76-A1F1-797378C6C7EF", - "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 792.0806586605164, - "start" : 82.30508474576271, - "track" : "v3" - }, - { - "duration" : 7.457627118644069, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E8BDB4FD-1478-46C9-B2B9-4A995785C804", - "kind" : "video", - "linkId" : "E6C60A60-B509-4E76-A1F1-797378C6C7EF", - "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 792.0806468685174, - "start" : 82.30508474576271, - "track" : "v4" - }, - { - "duration" : 5.7966101694915295, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "9F719822-EE9F-4D98-AE6F-95832502501D", - "kind" : "video", - "linkId" : "33DE57F6-49E9-47DA-971D-A7E3824D9B47", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 6.203389830508485, - "start" : 89.76271186440678, - "track" : "v0" - }, - { - "duration" : 5.7966101694915295, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4D903E13-8AAE-4EED-9E78-EB2D6201EE84", - "kind" : "audio", - "linkId" : "33DE57F6-49E9-47DA-971D-A7E3824D9B47", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 6.1195182891674165, - "start" : 89.76271186440678, - "track" : "v1" - }, - { - "duration" : 5.7966101694915295, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0A6A0000-76C4-4F4F-A5EC-B9DB5AA10EE5", - "kind" : "audio", - "linkId" : "33DE57F6-49E9-47DA-971D-A7E3824D9B47", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 6.166194592398341, - "start" : 89.76271186440678, - "track" : "v2" - }, - { - "duration" : 5.7966101694915295, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "91FC4279-5D23-44A0-BA93-5BC8CFB0775C", - "kind" : "video", - "linkId" : "33DE57F6-49E9-47DA-971D-A7E3824D9B47", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 6.029587456170219, - "start" : 89.76271186440678, - "track" : "v3" - }, - { - "duration" : 5.7966101694915295, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "51B251E0-3404-4CAC-B18A-37A29B9BF96B", - "kind" : "video", - "linkId" : "33DE57F6-49E9-47DA-971D-A7E3824D9B47", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 6.029582872171403, - "start" : 89.76271186440678, - "track" : "v4" - }, - { - "duration" : 18.847457627118715, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E8DC76EC-C4D2-46AE-853A-A60F4F0D2E07", - "kind" : "video", - "linkId" : "C5B94050-9128-423F-8A3C-F610A15ECBE8", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 153.08474576271186, - "start" : 116.81355932203384, - "track" : "v0" - }, - { - "duration" : 18.847457627118715, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4440B25F-48FC-49CC-9A78-207D16DDCCB7", - "kind" : "audio", - "linkId" : "C5B94050-9128-423F-8A3C-F610A15ECBE8", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 153.0008742213708, - "start" : 116.81355932203384, - "track" : "v1" - }, - { - "duration" : 18.847457627118715, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6FEFD05B-C857-4EBE-9EE7-F0EAAA08569F", - "kind" : "audio", - "linkId" : "C5B94050-9128-423F-8A3C-F610A15ECBE8", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 153.04755052460172, - "start" : 116.81355932203384, - "track" : "v2" - }, - { - "duration" : 18.847457627118715, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E1C8762B-5477-4769-BD47-2DE3F3E7F08A", - "kind" : "video", - "linkId" : "C5B94050-9128-423F-8A3C-F610A15ECBE8", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 152.9109433883736, - "start" : 116.81355932203384, - "track" : "v3" - }, - { - "duration" : 18.847457627118715, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6C90A7B0-6891-4C26-9ACE-E78062706047", - "kind" : "video", - "linkId" : "C5B94050-9128-423F-8A3C-F610A15ECBE8", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 152.91093880437478, - "start" : 116.81355932203384, - "track" : "v4" - }, - { - "duration" : 13.220338983050851, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BE66E066-F241-46FE-8D9A-606DBA9554B7", - "kind" : "video", - "linkId" : "17FC9151-B113-420C-8754-353E10218637", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 81.01694915254235, - "start" : 103.59322033898304, - "track" : "v0" - }, - { - "duration" : 13.220338983050851, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D10CE445-F3A3-46E4-B38D-B77AAB60F5FB", - "kind" : "audio", - "linkId" : "17FC9151-B113-420C-8754-353E10218637", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 80.93307761120128, - "start" : 103.59322033898304, - "track" : "v1" - }, - { - "duration" : 13.220338983050851, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4F4DB6E2-2CC1-4696-8CE6-9B7119E8F034", - "kind" : "audio", - "linkId" : "17FC9151-B113-420C-8754-353E10218637", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 80.97975391443221, - "start" : 103.59322033898304, - "track" : "v2" - }, - { - "duration" : 13.220338983050851, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "27A9D3E7-68B7-40B3-B332-244F8C0C1BF6", - "kind" : "video", - "linkId" : "17FC9151-B113-420C-8754-353E10218637", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 80.84314677820409, - "start" : 103.59322033898304, - "track" : "v3" - }, - { - "duration" : 13.220338983050851, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5DB078BA-6259-4E90-9C12-E9642D920CED", - "kind" : "video", - "linkId" : "17FC9151-B113-420C-8754-353E10218637", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 80.84314219420527, - "start" : 103.59322033898304, - "track" : "v4" - }, - { - "duration" : 2.6779661016949063, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5FE10E54-0DF5-4AE4-BD39-77E27BADC0B3", - "kind" : "video", - "linkId" : "557C472E-4AC3-4428-8BD4-7817D0701CAB", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 22.40677966101694, - "start" : 95.55932203389831, - "track" : "v0" - }, - { - "duration" : 2.6779661016949063, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "981061A0-98AF-4B09-9FEF-6DE942F56A18", - "kind" : "audio", - "linkId" : "557C472E-4AC3-4428-8BD4-7817D0701CAB", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 22.322908119675873, - "start" : 95.55932203389831, - "track" : "v1" - }, - { - "duration" : 2.6779661016949063, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "AD29D0F7-2863-42EA-BF7F-AB91267B34D1", - "kind" : "audio", - "linkId" : "557C472E-4AC3-4428-8BD4-7817D0701CAB", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 22.369584422906797, - "start" : 95.55932203389831, - "track" : "v2" - }, - { - "duration" : 2.6779661016949063, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "665388F4-40AF-450A-9C85-EE082E1EE31F", - "kind" : "video", - "linkId" : "557C472E-4AC3-4428-8BD4-7817D0701CAB", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 22.232977286678675, - "start" : 95.55932203389831, - "track" : "v3" - }, - { - "duration" : 2.6779661016949063, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3A784722-971F-44D4-B835-69C262B69DC7", - "kind" : "video", - "linkId" : "557C472E-4AC3-4428-8BD4-7817D0701CAB", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 22.23297270267986, - "start" : 95.55932203389831, - "track" : "v4" - }, - { - "duration" : 5.355932203389827, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3101D12D-3956-4472-A689-AC25FAF6B2B3", - "kind" : "video", - "linkId" : "DD4C0E3A-C905-48F3-BA08-017781831163", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 29.423728813559308, - "start" : 98.23728813559322, - "track" : "v0" - }, - { - "duration" : 5.355932203389827, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8AE89603-E830-41CB-98AA-FCE4DDB65200", - "kind" : "audio", - "linkId" : "DD4C0E3A-C905-48F3-BA08-017781831163", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 29.33985727221824, - "start" : 98.23728813559322, - "track" : "v1" - }, - { - "duration" : 5.355932203389827, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "30FEEB7F-94C6-4D85-98F3-9D56EFDEA6DB", - "kind" : "audio", - "linkId" : "DD4C0E3A-C905-48F3-BA08-017781831163", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 29.386533575449164, - "start" : 98.23728813559322, - "track" : "v2" - }, - { - "duration" : 5.355932203389827, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "986FD34B-99B0-48E4-8AA8-EA9747E0F300", - "kind" : "video", - "linkId" : "DD4C0E3A-C905-48F3-BA08-017781831163", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 29.249926439221042, - "start" : 98.23728813559322, - "track" : "v3" - }, - { - "duration" : 5.355932203389827, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "847F33A9-CA10-4FA4-95E4-3B250E43146A", - "kind" : "video", - "linkId" : "DD4C0E3A-C905-48F3-BA08-017781831163", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 29.249921855222226, - "start" : 98.23728813559322, - "track" : "v4" - }, - { - "duration" : 7.423728813559308, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "11BBE731-6722-4267-9D42-9A793553DE06", - "kind" : "video", - "linkId" : "DA913126-892F-46B7-9A7A-51A2A1951521", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 193.55932203389838, - "start" : 135.66101694915255, - "track" : "v0" - }, - { - "duration" : 7.423728813559308, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "22421728-439D-4470-95A1-EC2EF0D21C81", - "kind" : "audio", - "linkId" : "DA913126-892F-46B7-9A7A-51A2A1951521", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 193.47545049255731, - "start" : 135.66101694915255, - "track" : "v1" - }, - { - "duration" : 7.423728813559308, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "AE02280F-55B2-4037-9D75-5CC083936FD1", - "kind" : "audio", - "linkId" : "DA913126-892F-46B7-9A7A-51A2A1951521", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 193.52212679578824, - "start" : 135.66101694915255, - "track" : "v2" - }, - { - "duration" : 7.423728813559308, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E2A5436F-D353-42DF-9A48-073C9FE6246F", - "kind" : "video", - "linkId" : "DA913126-892F-46B7-9A7A-51A2A1951521", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 193.38551965956012, - "start" : 135.66101694915255, - "track" : "v3" - }, - { - "duration" : 7.423728813559308, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A1DA8F0A-B592-4DBD-9926-213A33E42749", - "kind" : "video", - "linkId" : "DA913126-892F-46B7-9A7A-51A2A1951521", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 193.3855150755613, - "start" : 135.66101694915255, - "track" : "v4" - }, - { - "duration" : 2.3389830508474745, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0F625D10-4206-4D6D-8623-4F14529073E3", - "kind" : "video", - "linkId" : "E458F570-26D5-47DD-87C2-E11FF928A2CA", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 214.9152542372882, - "start" : 143.08474576271186, - "track" : "v0" - }, - { - "duration" : 2.3389830508474745, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F69F71DF-6EEA-48C1-A1F0-F8EE77EE669C", - "kind" : "audio", - "linkId" : "E458F570-26D5-47DD-87C2-E11FF928A2CA", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 214.83138269594713, - "start" : 143.08474576271186, - "track" : "v1" - }, - { - "duration" : 2.3389830508474745, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "63619F12-6728-45D4-9664-77B370607EEB", - "kind" : "audio", - "linkId" : "E458F570-26D5-47DD-87C2-E11FF928A2CA", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 214.87805899917805, - "start" : 143.08474576271186, - "track" : "v2" - }, - { - "duration" : 2.3389830508474745, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4F7963B1-6583-494B-A319-8F2BF093C517", - "kind" : "video", - "linkId" : "E458F570-26D5-47DD-87C2-E11FF928A2CA", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 214.74145186294993, - "start" : 143.08474576271186, - "track" : "v3" - }, - { - "duration" : 2.3389830508474745, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "95B72825-D2B7-4D97-8C1F-63E1EB598DF9", - "kind" : "video", - "linkId" : "E458F570-26D5-47DD-87C2-E11FF928A2CA", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 214.7414472789511, - "start" : 143.08474576271186, - "track" : "v4" - }, - { - "duration" : 6.135593220338961, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "112CE6B8-BFDF-497C-9C69-CCE7802DB06C", - "kind" : "video", - "linkId" : "A28DCAA3-B061-4E38-8372-878AD7EF2060", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 226.33898305084753, - "start" : 145.42372881355934, - "track" : "v0" - }, - { - "duration" : 6.135593220338961, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4040BC36-A9F4-4ED3-A653-1978DDDAF7C5", - "kind" : "audio", - "linkId" : "A28DCAA3-B061-4E38-8372-878AD7EF2060", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 226.25511150950646, - "start" : 145.42372881355934, - "track" : "v1" - }, - { - "duration" : 6.135593220338961, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EFD116A1-EB5D-4CE3-B95D-14B73ECDE548", - "kind" : "audio", - "linkId" : "A28DCAA3-B061-4E38-8372-878AD7EF2060", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 226.3017878127374, - "start" : 145.42372881355934, - "track" : "v2" - }, - { - "duration" : 6.135593220338961, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0AE31757-C7B4-49BB-AB63-0641A6D83BC3", - "kind" : "video", - "linkId" : "A28DCAA3-B061-4E38-8372-878AD7EF2060", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 226.16518067650927, - "start" : 145.42372881355934, - "track" : "v3" - }, - { - "duration" : 6.135593220338961, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "9E1DFB7E-F165-4D38-AC11-812E55244554", - "kind" : "video", - "linkId" : "A28DCAA3-B061-4E38-8372-878AD7EF2060", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 226.16517609251045, - "start" : 145.42372881355934, - "track" : "v4" - }, - { - "duration" : 8.508474576271198, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6C78F572-76AD-4191-822E-E127F9D580E4", - "kind" : "video", - "linkId" : "CBF9981A-DB5A-459B-96C0-CFDCEB644C78", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 233.8305084745763, - "start" : 151.5593220338983, - "track" : "v0" - }, - { - "duration" : 8.508474576271198, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "443C9738-2CD2-45FF-89ED-93D209FFBFCA", - "kind" : "audio", - "linkId" : "CBF9981A-DB5A-459B-96C0-CFDCEB644C78", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 233.74663693323524, - "start" : 151.5593220338983, - "track" : "v1" - }, - { - "duration" : 8.508474576271198, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B28457F9-9054-461C-99E1-73E68152E679", - "kind" : "audio", - "linkId" : "CBF9981A-DB5A-459B-96C0-CFDCEB644C78", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 233.79331323646616, - "start" : 151.5593220338983, - "track" : "v2" - }, - { - "duration" : 8.508474576271198, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3D057A10-3381-4125-B50C-BC1998039A9D", - "kind" : "video", - "linkId" : "CBF9981A-DB5A-459B-96C0-CFDCEB644C78", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 233.65670610023804, - "start" : 151.5593220338983, - "track" : "v3" - }, - { - "duration" : 8.508474576271198, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BDECC22C-1053-4191-B2F2-2EDBC4F3B5B5", - "kind" : "video", - "linkId" : "CBF9981A-DB5A-459B-96C0-CFDCEB644C78", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 233.65670151623922, - "start" : 151.5593220338983, - "track" : "v4" - }, - { - "duration" : 9.050847457627128, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "62014848-FAC6-4BBC-8E7A-14F0B28CC0E6", - "kind" : "video", - "linkId" : "4288CB77-E89F-42B1-927E-DEADACE56BA4", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 279.6271186440679, - "start" : 160.0677966101695, - "track" : "v0" - }, - { - "duration" : 9.050847457627128, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "59D15847-5750-41D0-84C8-63D08E771477", - "kind" : "audio", - "linkId" : "4288CB77-E89F-42B1-927E-DEADACE56BA4", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 279.5432471027268, - "start" : 160.0677966101695, - "track" : "v1" - }, - { - "duration" : 9.050847457627128, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3404389F-7945-4513-A69B-1C428F1D90BE", - "kind" : "audio", - "linkId" : "4288CB77-E89F-42B1-927E-DEADACE56BA4", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 279.58992340595773, - "start" : 160.0677966101695, - "track" : "v2" - }, - { - "duration" : 9.050847457627128, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "91B4872C-9861-4274-ADDC-E13CB1843662", - "kind" : "video", - "linkId" : "4288CB77-E89F-42B1-927E-DEADACE56BA4", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 279.4533162697296, - "start" : 160.0677966101695, - "track" : "v3" - }, - { - "duration" : 9.050847457627128, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BA59E5A7-F140-4986-8E5B-E8994D035D5D", - "kind" : "video", - "linkId" : "4288CB77-E89F-42B1-927E-DEADACE56BA4", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 279.4533116857308, - "start" : 160.0677966101695, - "track" : "v4" - }, - { - "duration" : 20.983050847457605, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D9CE7BC0-0A5C-4C90-A15A-7694E9E749B7", - "kind" : "video", - "linkId" : "1B8AA32F-8AAC-4699-91CD-75C5E92F7BA0", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 341.42372881355936, - "start" : 169.11864406779662, - "track" : "v0" - }, - { - "duration" : 20.983050847457605, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6F77C3F0-E5FF-426F-99E1-AA28B988BB0A", - "kind" : "audio", - "linkId" : "1B8AA32F-8AAC-4699-91CD-75C5E92F7BA0", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 341.3398572722183, - "start" : 169.11864406779662, - "track" : "v1" - }, - { - "duration" : 20.983050847457605, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "DAF8FD62-8F36-4F73-94EE-5380D1E30401", - "kind" : "audio", - "linkId" : "1B8AA32F-8AAC-4699-91CD-75C5E92F7BA0", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 341.3865335754492, - "start" : 169.11864406779662, - "track" : "v2" - }, - { - "duration" : 20.983050847457605, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "74FC2FC5-A8A4-469B-9482-73979D2C373D", - "kind" : "video", - "linkId" : "1B8AA32F-8AAC-4699-91CD-75C5E92F7BA0", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 341.2499264392211, - "start" : 169.11864406779662, - "track" : "v3" - }, - { - "duration" : 20.983050847457605, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FFA5E7DE-E5C3-4ED5-9004-3201CD4A30DA", - "kind" : "video", - "linkId" : "1B8AA32F-8AAC-4699-91CD-75C5E92F7BA0", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 341.2499218552223, - "start" : 169.11864406779662, - "track" : "v4" - }, - { - "duration" : 3.1525423728813564, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F7F6AF02-0DE4-4485-8590-753751DA656D", - "kind" : "video", - "linkId" : "1B2E5F19-8476-4AC4-B136-E5222F541298", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 516.9830508474577, - "start" : 190.10169491525423, - "track" : "v0" - }, - { - "duration" : 3.1525423728813564, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0F29059A-7B58-4133-AA5B-F26BDD11FCEA", - "kind" : "audio", - "linkId" : "1B2E5F19-8476-4AC4-B136-E5222F541298", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 516.8991793061166, - "start" : 190.10169491525423, - "track" : "v1" - }, - { - "duration" : 3.1525423728813564, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "67E73C14-2908-4A77-83E5-6A8F38F25BF4", - "kind" : "audio", - "linkId" : "1B2E5F19-8476-4AC4-B136-E5222F541298", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 516.9458556093475, - "start" : 190.10169491525423, - "track" : "v2" - }, - { - "duration" : 3.1525423728813564, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "DACD3C89-6F61-4E5A-BFF4-ABCB5013028B", - "kind" : "video", - "linkId" : "1B2E5F19-8476-4AC4-B136-E5222F541298", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 516.8092484731194, - "start" : 190.10169491525423, - "track" : "v3" - }, - { - "duration" : 3.1525423728813564, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D85A8D17-5419-4FE8-A767-7886ACAD06D2", - "kind" : "video", - "linkId" : "1B2E5F19-8476-4AC4-B136-E5222F541298", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 516.8092438891206, - "start" : 190.10169491525423, - "track" : "v4" - }, - { - "duration" : 3.0508474576271283, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "22E40845-33D9-4113-9E6A-72ABDBAF556B", - "kind" : "video", - "linkId" : "3CC5F7F4-7A10-4825-BA22-58F0E5FA39EC", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 526.0677966101696, - "start" : 193.25423728813558, - "track" : "v0" - }, - { - "duration" : 3.0508474576271283, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "80104D35-A942-4197-BE85-9E999B164BD5", - "kind" : "audio", - "linkId" : "3CC5F7F4-7A10-4825-BA22-58F0E5FA39EC", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 525.9839250688285, - "start" : 193.25423728813558, - "track" : "v1" - }, - { - "duration" : 3.0508474576271283, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "98A4488E-8AFB-4DAF-AA4A-B6171B5BE4B0", - "kind" : "audio", - "linkId" : "3CC5F7F4-7A10-4825-BA22-58F0E5FA39EC", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 526.0306013720594, - "start" : 193.25423728813558, - "track" : "v2" - }, - { - "duration" : 3.0508474576271283, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "31F9AFDE-F19F-4D52-8468-6E4FD6E73843", - "kind" : "video", - "linkId" : "3CC5F7F4-7A10-4825-BA22-58F0E5FA39EC", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 525.8939942358313, - "start" : 193.25423728813558, - "track" : "v3" - }, - { - "duration" : 3.0508474576271283, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "58C86E52-F741-4EAF-93E1-9AD9B238BEBC", - "kind" : "video", - "linkId" : "3CC5F7F4-7A10-4825-BA22-58F0E5FA39EC", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 525.8939896518325, - "start" : 193.25423728813558, - "track" : "v4" - }, - { - "duration" : 12.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "729D86A6-9A2B-4F18-97A2-9E4B8842705A", - "kind" : "video", - "linkId" : "96ED86B5-C0C4-427A-AB87-51061D1B6C02", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 547.3898305084747, - "start" : 196.3050847457627, - "track" : "v0" - }, - { - "duration" : 12.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "537BA6C3-FE42-41F7-AFD1-CCD1498FD164", - "kind" : "audio", - "linkId" : "96ED86B5-C0C4-427A-AB87-51061D1B6C02", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 547.3059589671336, - "start" : 196.3050847457627, - "track" : "v1" - }, - { - "duration" : 12.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "DF50C8A8-7061-47D8-9E3E-F76059F64F17", - "kind" : "audio", - "linkId" : "96ED86B5-C0C4-427A-AB87-51061D1B6C02", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 547.3526352703645, - "start" : 196.3050847457627, - "track" : "v2" - }, - { - "duration" : 12.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "396149DF-84B8-447F-8A4F-6BC523F5F5C2", - "kind" : "video", - "linkId" : "96ED86B5-C0C4-427A-AB87-51061D1B6C02", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 547.2160281341364, - "start" : 196.3050847457627, - "track" : "v3" - }, - { - "duration" : 12.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8BAC1614-CB12-4DC2-9F56-99B2DD91B028", - "kind" : "video", - "linkId" : "96ED86B5-C0C4-427A-AB87-51061D1B6C02", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 547.2160235501376, - "start" : 196.3050847457627, - "track" : "v4" - }, - { - "duration" : 4.203389830508456, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C677D610-A145-44E9-A5DF-4F24EF105083", - "kind" : "video", - "linkId" : "61201D64-66C9-4C57-8602-97A96701E0BF", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 566.3389830508476, - "start" : 208.40677966101697, - "track" : "v0" - }, - { - "duration" : 4.203389830508456, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2D9D8A03-3523-4FF8-A897-637F158C3C79", - "kind" : "audio", - "linkId" : "61201D64-66C9-4C57-8602-97A96701E0BF", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 566.2551115095065, - "start" : 208.40677966101697, - "track" : "v1" - }, - { - "duration" : 4.203389830508456, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "61480E1E-954B-45A2-9C3A-FF0F0BB690FC", - "kind" : "audio", - "linkId" : "61201D64-66C9-4C57-8602-97A96701E0BF", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 566.3017878127374, - "start" : 208.40677966101697, - "track" : "v2" - }, - { - "duration" : 4.203389830508456, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0501FE38-6C08-467C-9E1C-E41CA0498523", - "kind" : "video", - "linkId" : "61201D64-66C9-4C57-8602-97A96701E0BF", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 566.1651806765093, - "start" : 208.40677966101697, - "track" : "v3" - }, - { - "duration" : 4.203389830508456, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "74D8A914-BC66-4C84-BFFA-A0CA6639D704", - "kind" : "video", - "linkId" : "61201D64-66C9-4C57-8602-97A96701E0BF", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 566.1651760925105, - "start" : 208.40677966101697, - "track" : "v4" - }, - { - "duration" : 6.406779661016941, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "9B498BC6-78AB-44F5-B23B-25E8F7DDC89B", - "kind" : "video", - "linkId" : "02AB076D-1732-49F9-AA10-410FA9A3E78F", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 571.35593220339, - "start" : 212.61016949152543, - "track" : "v0" - }, - { - "duration" : 6.406779661016941, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8C7D77E2-3FA1-41F1-884C-63C516C4AE33", - "kind" : "audio", - "linkId" : "02AB076D-1732-49F9-AA10-410FA9A3E78F", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 571.272060662049, - "start" : 212.61016949152543, - "track" : "v1" - }, - { - "duration" : 6.406779661016941, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "21A1243A-B2AF-4637-B9E4-3BA86980E892", - "kind" : "audio", - "linkId" : "02AB076D-1732-49F9-AA10-410FA9A3E78F", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 571.3187369652799, - "start" : 212.61016949152543, - "track" : "v2" - }, - { - "duration" : 6.406779661016941, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F361620E-6686-4423-BFBD-736FF93CDEC5", - "kind" : "video", - "linkId" : "02AB076D-1732-49F9-AA10-410FA9A3E78F", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 571.1821298290517, - "start" : 212.61016949152543, - "track" : "v3" - }, - { - "duration" : 6.406779661016941, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2A763F6C-BEF9-435E-8F42-C72F8F08381A", - "kind" : "video", - "linkId" : "02AB076D-1732-49F9-AA10-410FA9A3E78F", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 571.1821252450529, - "start" : 212.61016949152543, - "track" : "v4" - }, - { - "duration" : 4.610169491525454, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "ADAD35C0-6E8C-4D37-B4E4-D2859B73BE27", - "kind" : "video", - "linkId" : "3473CFA9-BD18-42DA-AD05-6C5914B914B6", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 584.813559322034, - "start" : 222.06779661016947, - "track" : "v0" - }, - { - "duration" : 4.610169491525454, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "86415894-FDC6-4F60-A9B8-0954DBA0D01D", - "kind" : "audio", - "linkId" : "3473CFA9-BD18-42DA-AD05-6C5914B914B6", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 584.729687780693, - "start" : 222.06779661016947, - "track" : "v1" - }, - { - "duration" : 4.610169491525454, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "48CD7335-7777-4C44-A35A-17CFAA490549", - "kind" : "audio", - "linkId" : "3473CFA9-BD18-42DA-AD05-6C5914B914B6", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 584.7763640839239, - "start" : 222.06779661016947, - "track" : "v2" - }, - { - "duration" : 4.610169491525454, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "54BD8C49-005E-4485-9DC1-7F916CF8BDD3", - "kind" : "video", - "linkId" : "3473CFA9-BD18-42DA-AD05-6C5914B914B6", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 584.6397569476958, - "start" : 222.06779661016947, - "track" : "v3" - }, - { - "duration" : 4.610169491525454, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "23AEC066-77D9-4BA3-8117-EE7A8161CBBD", - "kind" : "video", - "linkId" : "3473CFA9-BD18-42DA-AD05-6C5914B914B6", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 584.639752363697, - "start" : 222.06779661016947, - "track" : "v4" - }, - { - "duration" : 2.237288135593218, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0991B547-CD7C-4CC3-87EC-AD16FF79CEEC", - "kind" : "video", - "linkId" : "02A86E0B-9E9F-4742-A7B2-44024A6F3E6B", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 577.7627118644069, - "start" : 219.01694915254237, - "track" : "v0" - }, - { - "duration" : 2.237288135593218, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A7485496-22C3-4CB6-8034-93F5ACE90052", - "kind" : "audio", - "linkId" : "02A86E0B-9E9F-4742-A7B2-44024A6F3E6B", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 577.6788403230659, - "start" : 219.01694915254237, - "track" : "v1" - }, - { - "duration" : 2.237288135593218, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A8426C1F-0305-45DF-A0C4-57CB7AD56360", - "kind" : "audio", - "linkId" : "02A86E0B-9E9F-4742-A7B2-44024A6F3E6B", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 577.7255166262968, - "start" : 219.01694915254237, - "track" : "v2" - }, - { - "duration" : 2.237288135593218, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F0053E8C-8FC8-47E2-8431-73026DBACEC8", - "kind" : "video", - "linkId" : "02A86E0B-9E9F-4742-A7B2-44024A6F3E6B", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 577.5889094900687, - "start" : 219.01694915254237, - "track" : "v3" - }, - { - "duration" : 2.237288135593218, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EF04E82D-AA3C-41CA-B856-4A6914526EC1", - "kind" : "video", - "linkId" : "02A86E0B-9E9F-4742-A7B2-44024A6F3E6B", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 577.5889049060698, - "start" : 219.01694915254237, - "track" : "v4" - }, - { - "duration" : 0.33898305084744607, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "CABADC3E-FA59-4C65-87F2-13BFBE96E557", - "kind" : "video", - "linkId" : "00C998B7-CF37-4D26-B09F-6C36B9B32D82", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 582.4067796610171, - "start" : 221.25423728813558, - "track" : "v0" - }, - { - "duration" : 0.33898305084744607, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FD19E6CE-504A-42B5-96A0-1CD953096F29", - "kind" : "audio", - "linkId" : "00C998B7-CF37-4D26-B09F-6C36B9B32D82", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 582.3229081196761, - "start" : 221.25423728813558, - "track" : "v1" - }, - { - "duration" : 0.33898305084744607, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A112502F-0EDC-4D49-9AB8-9ACA09DFCDE5", - "kind" : "audio", - "linkId" : "00C998B7-CF37-4D26-B09F-6C36B9B32D82", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 582.369584422907, - "start" : 221.25423728813558, - "track" : "v2" - }, - { - "duration" : 0.33898305084744607, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F01A73DE-9729-484B-89E7-719D0391276E", - "kind" : "video", - "linkId" : "00C998B7-CF37-4D26-B09F-6C36B9B32D82", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 582.2329772866789, - "start" : 221.25423728813558, - "track" : "v3" - }, - { - "duration" : 0.33898305084744607, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "627B2BC3-48D0-4F6C-A129-90620B0EB3FD", - "kind" : "video", - "linkId" : "00C998B7-CF37-4D26-B09F-6C36B9B32D82", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 582.2329727026801, - "start" : 221.25423728813558, - "track" : "v4" - }, - { - "duration" : 0.47457627118643586, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "DC2DEF3C-5F2B-476E-AF27-8A22D6D9AED7", - "kind" : "video", - "linkId" : "699AF368-3D93-47C1-8338-C3D28F300CB5", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 583.1864406779663, - "start" : 221.59322033898303, - "track" : "v0" - }, - { - "duration" : 0.47457627118643586, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F714792D-CAB4-45F1-9BD8-36A1DF081EB8", - "kind" : "audio", - "linkId" : "699AF368-3D93-47C1-8338-C3D28F300CB5", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 583.1025691366252, - "start" : 221.59322033898303, - "track" : "v1" - }, - { - "duration" : 0.47457627118643586, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BA43BF71-4C54-4370-863D-D0B445C4A4DF", - "kind" : "audio", - "linkId" : "699AF368-3D93-47C1-8338-C3D28F300CB5", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 583.1492454398561, - "start" : 221.59322033898303, - "track" : "v2" - }, - { - "duration" : 0.47457627118643586, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4BAC786B-BDD8-4676-9B29-2A7AC95177F6", - "kind" : "video", - "linkId" : "699AF368-3D93-47C1-8338-C3D28F300CB5", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 583.012638303628, - "start" : 221.59322033898303, - "track" : "v3" - }, - { - "duration" : 0.47457627118643586, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "987F92AC-8229-449F-BA8F-BB01CA7CADC3", - "kind" : "video", - "linkId" : "699AF368-3D93-47C1-8338-C3D28F300CB5", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 583.0126337196292, - "start" : 221.59322033898303, - "track" : "v4" - }, - { - "duration" : 4.813559322033882, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8B6B0536-EAC5-4272-9106-03B9E89E36AB", - "kind" : "video", - "linkId" : "0F2B0909-5C81-4A61-BBD8-6CD3ECC9563D", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 589.6949152542375, - "start" : 226.67796610169492, - "track" : "v0" - }, - { - "duration" : 4.813559322033882, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "DD50D9F8-4C08-47DA-AB75-382FE215D3C0", - "kind" : "audio", - "linkId" : "0F2B0909-5C81-4A61-BBD8-6CD3ECC9563D", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 589.6110437128964, - "start" : 226.67796610169492, - "track" : "v1" - }, - { - "duration" : 4.813559322033882, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A2113A02-7099-45C4-990B-263F71229BAA", - "kind" : "audio", - "linkId" : "0F2B0909-5C81-4A61-BBD8-6CD3ECC9563D", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 589.6577200161273, - "start" : 226.67796610169492, - "track" : "v2" - }, - { - "duration" : 4.813559322033882, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "72B5DADE-2448-445A-A7AB-6BA636E753FD", - "kind" : "video", - "linkId" : "0F2B0909-5C81-4A61-BBD8-6CD3ECC9563D", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 589.5211128798992, - "start" : 226.67796610169492, - "track" : "v3" - }, - { - "duration" : 4.813559322033882, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8CC675CB-9ABE-45FF-B384-C7AC4D78F091", - "kind" : "video", - "linkId" : "0F2B0909-5C81-4A61-BBD8-6CD3ECC9563D", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 589.5211082959004, - "start" : 226.67796610169492, - "track" : "v4" - }, - { - "duration" : 4.81355932203391, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EF0E9A54-F218-4203-8874-E267C94F96ED", - "kind" : "video", - "linkId" : "C9B743A1-0C1E-4522-9FAE-C8D05A31C193", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 594.5084745762713, - "start" : 231.4915254237288, - "track" : "v0" - }, - { - "duration" : 4.81355932203391, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3E1A7EAE-EA47-443C-A5DF-51295361CA28", - "kind" : "audio", - "linkId" : "C9B743A1-0C1E-4522-9FAE-C8D05A31C193", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 594.4246030349302, - "start" : 231.4915254237288, - "track" : "v1" - }, - { - "duration" : 4.81355932203391, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "63928E72-332C-4FF0-AB20-7C4D9674F55F", - "kind" : "audio", - "linkId" : "C9B743A1-0C1E-4522-9FAE-C8D05A31C193", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 594.4712793381611, - "start" : 231.4915254237288, - "track" : "v2" - }, - { - "duration" : 4.81355932203391, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "AF037655-401A-4032-866C-0E9B1BB6EAC8", - "kind" : "video", - "linkId" : "C9B743A1-0C1E-4522-9FAE-C8D05A31C193", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 594.334672201933, - "start" : 231.4915254237288, - "track" : "v3" - }, - { - "duration" : 4.81355932203391, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "AF0F661D-9868-49AB-9276-E20CC57DBC28", - "kind" : "video", - "linkId" : "C9B743A1-0C1E-4522-9FAE-C8D05A31C193", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 594.3346676179342, - "start" : 231.4915254237288, - "track" : "v4" - }, - { - "duration" : 2.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EBA9BB70-0CEA-4170-9DBC-65B5D2FE9AD5", - "kind" : "video", - "linkId" : "4AAA7ABF-1577-4C2E-BF38-8705136C6450", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 616.8813559322035, - "start" : 236.3050847457627, - "track" : "v0" - }, - { - "duration" : 2.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A8D715D7-2371-400B-A53A-90BFA9CA14E1", - "kind" : "audio", - "linkId" : "4AAA7ABF-1577-4C2E-BF38-8705136C6450", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 616.7974843908625, - "start" : 236.3050847457627, - "track" : "v1" - }, - { - "duration" : 2.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1EC0AD88-9036-49C3-99E3-32F0047EB028", - "kind" : "audio", - "linkId" : "4AAA7ABF-1577-4C2E-BF38-8705136C6450", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 616.8441606940934, - "start" : 236.3050847457627, - "track" : "v2" - }, - { - "duration" : 2.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A1C1C0D1-CF81-42A4-8930-B00C4C02387B", - "kind" : "video", - "linkId" : "4AAA7ABF-1577-4C2E-BF38-8705136C6450", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 616.7075535578653, - "start" : 236.3050847457627, - "track" : "v3" - }, - { - "duration" : 2.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "9694A094-9FA8-4451-BBC8-3A505816AB5C", - "kind" : "video", - "linkId" : "4AAA7ABF-1577-4C2E-BF38-8705136C6450", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 616.7075489738664, - "start" : 236.3050847457627, - "track" : "v4" - }, - { - "duration" : 2.2033898305084563, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2DFE5FB3-850C-408B-888E-1B117520E619", - "kind" : "video", - "linkId" : "20A4E34D-C55C-439E-9743-C91BA434CB07", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 623.1864406779663, - "start" : 238.4406779661017, - "track" : "v0" - }, - { - "duration" : 2.2033898305084563, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1C5AFA4B-AB07-45EB-9012-D6817BCA669F", - "kind" : "audio", - "linkId" : "20A4E34D-C55C-439E-9743-C91BA434CB07", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 623.1025691366252, - "start" : 238.4406779661017, - "track" : "v1" - }, - { - "duration" : 2.2033898305084563, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FA4004F4-5AA3-4F29-999B-41460017B48F", - "kind" : "audio", - "linkId" : "20A4E34D-C55C-439E-9743-C91BA434CB07", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 623.1492454398561, - "start" : 238.4406779661017, - "track" : "v2" - }, - { - "duration" : 2.2033898305084563, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F4539CE1-8350-4649-A8D5-75DEC9A85285", - "kind" : "video", - "linkId" : "20A4E34D-C55C-439E-9743-C91BA434CB07", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 623.012638303628, - "start" : 238.4406779661017, - "track" : "v3" - }, - { - "duration" : 2.2033898305084563, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B9D12715-E647-49E2-A68A-393FDA9865DF", - "kind" : "video", - "linkId" : "20A4E34D-C55C-439E-9743-C91BA434CB07", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 623.0126337196292, - "start" : 238.4406779661017, - "track" : "v4" - }, - { - "duration" : 6.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "92455958-0ACB-484E-B1D9-19C02F73901A", - "kind" : "video", - "linkId" : "FB7680B4-9AF3-40EC-B7FB-010031D66837", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 627.491525423729, - "start" : 240.64406779661016, - "track" : "v0" - }, - { - "duration" : 6.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "DB167161-A9FA-4F64-92F8-79A205627364", - "kind" : "audio", - "linkId" : "FB7680B4-9AF3-40EC-B7FB-010031D66837", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 627.4076538823879, - "start" : 240.64406779661016, - "track" : "v1" - }, - { - "duration" : 6.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D53DC51B-6ED3-43B5-90CF-F263E6A8EF0F", - "kind" : "audio", - "linkId" : "FB7680B4-9AF3-40EC-B7FB-010031D66837", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 627.4543301856188, - "start" : 240.64406779661016, - "track" : "v2" - }, - { - "duration" : 6.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0F96EE3E-4FE2-417F-9760-F24B131C949C", - "kind" : "video", - "linkId" : "FB7680B4-9AF3-40EC-B7FB-010031D66837", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 627.3177230493907, - "start" : 240.64406779661016, - "track" : "v3" - }, - { - "duration" : 6.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "473DAC6E-6B6F-4395-AA04-1D2A9448459F", - "kind" : "video", - "linkId" : "FB7680B4-9AF3-40EC-B7FB-010031D66837", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 627.3177184653919, - "start" : 240.64406779661016, - "track" : "v4" - }, - { - "duration" : 5.288135593220346, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "04295A05-E2E5-4A94-A70F-1C9DCC3ACF86", - "kind" : "video", - "linkId" : "EDE7F931-7884-4F1E-8EFB-E8B00DCB5A07", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 641.220338983051, - "start" : 246.91525423728814, - "track" : "v0" - }, - { - "duration" : 5.288135593220346, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A4446EF1-6B91-4D7B-9285-3C4E42A475ED", - "kind" : "audio", - "linkId" : "EDE7F931-7884-4F1E-8EFB-E8B00DCB5A07", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 641.1364674417099, - "start" : 246.91525423728814, - "track" : "v1" - }, - { - "duration" : 5.288135593220346, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "37E22D5C-E179-4CD6-9C69-C6B281846229", - "kind" : "audio", - "linkId" : "EDE7F931-7884-4F1E-8EFB-E8B00DCB5A07", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 641.1831437449408, - "start" : 246.91525423728814, - "track" : "v2" - }, - { - "duration" : 5.288135593220346, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0D2B5334-E420-4746-B09A-110DA7EE6BA5", - "kind" : "video", - "linkId" : "EDE7F931-7884-4F1E-8EFB-E8B00DCB5A07", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 641.0465366087127, - "start" : 246.91525423728814, - "track" : "v3" - }, - { - "duration" : 5.288135593220346, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "95AA48BC-4C4B-4F4B-B29F-FB2E01943DE6", - "kind" : "video", - "linkId" : "EDE7F931-7884-4F1E-8EFB-E8B00DCB5A07", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 641.0465320247139, - "start" : 246.91525423728814, - "track" : "v4" - }, - { - "duration" : 3.3898305084745743, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "36A8E6B0-4985-4EBD-BD50-2621E6D840E5", - "kind" : "video", - "linkId" : "E1D1EEBE-B0FA-47B6-B02B-0D25E3E7C904", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 649.4237288135594, - "start" : 252.20338983050848, - "track" : "v0" - }, - { - "duration" : 3.3898305084745743, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "CDDA95CC-536D-4225-AAC3-470904596C1D", - "kind" : "audio", - "linkId" : "E1D1EEBE-B0FA-47B6-B02B-0D25E3E7C904", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 649.3398572722183, - "start" : 252.20338983050848, - "track" : "v1" - }, - { - "duration" : 3.3898305084745743, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "79AC6946-AD60-4D7C-9B74-BAF2C5043ED9", - "kind" : "audio", - "linkId" : "E1D1EEBE-B0FA-47B6-B02B-0D25E3E7C904", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 649.3865335754492, - "start" : 252.20338983050848, - "track" : "v2" - }, - { - "duration" : 3.3898305084745743, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "266B1043-1A29-4485-85C8-9921E2D077FE", - "kind" : "video", - "linkId" : "E1D1EEBE-B0FA-47B6-B02B-0D25E3E7C904", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 649.2499264392211, - "start" : 252.20338983050848, - "track" : "v3" - }, - { - "duration" : 3.3898305084745743, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F9D93C0A-48FD-4149-B214-E2648C026BB8", - "kind" : "video", - "linkId" : "E1D1EEBE-B0FA-47B6-B02B-0D25E3E7C904", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 649.2499218552223, - "start" : 252.20338983050848, - "track" : "v4" - }, - { - "duration" : 8.9491525423729, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E13CDB86-D0E7-4219-891E-396AF72F7200", - "kind" : "video", - "linkId" : "E0DD6E6F-D7C7-4743-ADC2-D8787163B0C3", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 664.1016949152543, - "start" : 255.59322033898306, - "track" : "v0" - }, - { - "duration" : 8.9491525423729, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "CF093561-83EC-4803-9CD7-E1D07429E06C", - "kind" : "audio", - "linkId" : "E0DD6E6F-D7C7-4743-ADC2-D8787163B0C3", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 664.0178233739132, - "start" : 255.59322033898306, - "track" : "v1" - }, - { - "duration" : 8.9491525423729, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D7A74DDC-3F2A-4E02-B350-45455C959B82", - "kind" : "audio", - "linkId" : "E0DD6E6F-D7C7-4743-ADC2-D8787163B0C3", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 664.0644996771441, - "start" : 255.59322033898306, - "track" : "v2" - }, - { - "duration" : 8.9491525423729, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "95E67BFD-C59E-4F99-92B5-027C3BC13CC3", - "kind" : "video", - "linkId" : "E0DD6E6F-D7C7-4743-ADC2-D8787163B0C3", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 663.927892540916, - "start" : 255.59322033898306, - "track" : "v3" - }, - { - "duration" : 8.9491525423729, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "AE9DDFED-5BE9-44A5-8349-623A8C1F1587", - "kind" : "video", - "linkId" : "E0DD6E6F-D7C7-4743-ADC2-D8787163B0C3", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 663.9278879569172, - "start" : 255.59322033898306, - "track" : "v4" - }, - { - "duration" : 2.4067796610169125, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6929AB44-D977-4A42-9DD4-69D4E759E4B7", - "kind" : "video", - "linkId" : "B1E3CEAB-46C9-4EF7-8BC4-F413D2FDD063", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 676.7796610169491, - "start" : 264.54237288135596, - "track" : "v0" - }, - { - "duration" : 2.4067796610169125, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F6BA54DA-3AAD-48D5-A132-C946F6FF5BBE", - "kind" : "audio", - "linkId" : "B1E3CEAB-46C9-4EF7-8BC4-F413D2FDD063", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 676.6957894756081, - "start" : 264.54237288135596, - "track" : "v1" - }, - { - "duration" : 2.4067796610169125, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "95E0BF0A-76D3-4341-87C0-4C3855C6AD49", - "kind" : "audio", - "linkId" : "B1E3CEAB-46C9-4EF7-8BC4-F413D2FDD063", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 676.742465778839, - "start" : 264.54237288135596, - "track" : "v2" - }, - { - "duration" : 2.4067796610169125, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "56B8138F-5F33-4EC1-A202-59403546D6C5", - "kind" : "video", - "linkId" : "B1E3CEAB-46C9-4EF7-8BC4-F413D2FDD063", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 676.6058586426109, - "start" : 264.54237288135596, - "track" : "v3" - }, - { - "duration" : 2.4067796610169125, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7C722B8F-E78E-45E3-8A2E-F30FF2127283", - "kind" : "video", - "linkId" : "B1E3CEAB-46C9-4EF7-8BC4-F413D2FDD063", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 676.6058540586121, - "start" : 264.54237288135596, - "track" : "v4" - }, - { - "duration" : 4.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "74A4A856-856E-45BF-BF4B-60A6E5627D38", - "kind" : "video", - "linkId" : "4DA91130-EF31-48CE-9353-1BCA2F4D362F", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 680.5084745762712, - "start" : 266.9491525423729, - "track" : "v0" - }, - { - "duration" : 4.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3DBBE8FE-AD87-457B-8670-E95DE832B26C", - "kind" : "audio", - "linkId" : "4DA91130-EF31-48CE-9353-1BCA2F4D362F", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 680.4246030349301, - "start" : 266.9491525423729, - "track" : "v1" - }, - { - "duration" : 4.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5E2EC015-CDDC-45F6-BEF4-76E842BBC389", - "kind" : "audio", - "linkId" : "4DA91130-EF31-48CE-9353-1BCA2F4D362F", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 680.471279338161, - "start" : 266.9491525423729, - "track" : "v2" - }, - { - "duration" : 4.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1275A44B-DBDA-421C-9443-3923A7274D8D", - "kind" : "video", - "linkId" : "4DA91130-EF31-48CE-9353-1BCA2F4D362F", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 680.3346722019329, - "start" : 266.9491525423729, - "track" : "v3" - }, - { - "duration" : 4.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "15AE2513-D3F9-47C3-B4F7-61A351DAB004", - "kind" : "video", - "linkId" : "4DA91130-EF31-48CE-9353-1BCA2F4D362F", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 680.3346676179341, - "start" : 266.9491525423729, - "track" : "v4" - }, - { - "duration" : 1.1186440677965948, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2077C05E-DE10-4841-AB24-8F92DE0579D1", - "kind" : "video", - "linkId" : "870FE72E-0830-4984-8A17-C8265B573862", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 698.1016949152543, - "start" : 271.22033898305085, - "track" : "v0" - }, - { - "duration" : 1.1186440677965948, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1832EFCE-19F8-4EBA-B21A-88826F9AE7BF", - "kind" : "audio", - "linkId" : "870FE72E-0830-4984-8A17-C8265B573862", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 698.0178233739132, - "start" : 271.22033898305085, - "track" : "v1" - }, - { - "duration" : 1.1186440677965948, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4823B8FA-4845-43E7-A4AD-95170EAE1A77", - "kind" : "audio", - "linkId" : "870FE72E-0830-4984-8A17-C8265B573862", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 698.0644996771441, - "start" : 271.22033898305085, - "track" : "v2" - }, - { - "duration" : 1.1186440677965948, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "AB47B68B-4217-46FF-A1EA-D21122BC749F", - "kind" : "video", - "linkId" : "870FE72E-0830-4984-8A17-C8265B573862", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 697.927892540916, - "start" : 271.22033898305085, - "track" : "v3" - }, - { - "duration" : 1.1186440677965948, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "948310E4-F6E1-4C7B-8DFC-4B1EFE91D9F3", - "kind" : "video", - "linkId" : "870FE72E-0830-4984-8A17-C8265B573862", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 697.9278879569172, - "start" : 271.22033898305085, - "track" : "v4" - }, - { - "duration" : 3.9661016949152668, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1841DCD7-E1AC-46F4-8BA9-65F76CE7F997", - "kind" : "video", - "linkId" : "8E3E2F7E-8CEA-4F62-AC48-EAD5610BBF7A", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 706.1016949152543, - "start" : 272.33898305084745, - "track" : "v0" - }, - { - "duration" : 3.9661016949152668, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "9F70CFD3-7AB7-4A6D-87AF-B53BEDECA24A", - "kind" : "audio", - "linkId" : "8E3E2F7E-8CEA-4F62-AC48-EAD5610BBF7A", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 706.0178233739132, - "start" : 272.33898305084745, - "track" : "v1" - }, - { - "duration" : 3.9661016949152668, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6296E4FA-F3A8-4E9F-B5A6-21C104C11316", - "kind" : "audio", - "linkId" : "8E3E2F7E-8CEA-4F62-AC48-EAD5610BBF7A", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 706.0644996771441, - "start" : 272.33898305084745, - "track" : "v2" - }, - { - "duration" : 3.9661016949152668, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7DBC5BCC-4F01-497F-97A9-FD8E5FED80ED", - "kind" : "video", - "linkId" : "8E3E2F7E-8CEA-4F62-AC48-EAD5610BBF7A", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 705.927892540916, - "start" : 272.33898305084745, - "track" : "v3" - }, - { - "duration" : 3.9661016949152668, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D22E7D29-E8F2-49FC-AE39-95140062ED4C", - "kind" : "video", - "linkId" : "8E3E2F7E-8CEA-4F62-AC48-EAD5610BBF7A", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 705.9278879569172, - "start" : 272.33898305084745, - "track" : "v4" - }, - { - "duration" : 7.2542372881355845, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "ADE6D5E2-A342-4F16-9FAB-2F40C2D1BCDD", - "kind" : "video", - "linkId" : "7AAFBE23-17BF-4AC0-B4E6-3D003AF75DF9", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 710.0677966101696, - "start" : 276.3050847457627, - "track" : "v0" - }, - { - "duration" : 7.2542372881355845, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BFEEDB13-FCE4-473E-BA8B-BAA795862399", - "kind" : "audio", - "linkId" : "7AAFBE23-17BF-4AC0-B4E6-3D003AF75DF9", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 709.9839250688285, - "start" : 276.3050847457627, - "track" : "v1" - }, - { - "duration" : 7.2542372881355845, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BC83D6FA-84FA-483B-8699-D1219DF29357", - "kind" : "audio", - "linkId" : "7AAFBE23-17BF-4AC0-B4E6-3D003AF75DF9", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 710.0306013720594, - "start" : 276.3050847457627, - "track" : "v2" - }, - { - "duration" : 7.2542372881355845, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "58221FDA-00C6-45A1-9267-2AC4500070FE", - "kind" : "video", - "linkId" : "7AAFBE23-17BF-4AC0-B4E6-3D003AF75DF9", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 709.8939942358313, - "start" : 276.3050847457627, - "track" : "v3" - }, - { - "duration" : 7.2542372881355845, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BC23C71D-81CD-40A8-8D4C-5728293351FB", - "kind" : "video", - "linkId" : "7AAFBE23-17BF-4AC0-B4E6-3D003AF75DF9", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 709.8939896518325, - "start" : 276.3050847457627, - "track" : "v4" - }, - { - "duration" : 7.830508474576277, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5B448404-52D6-4D2E-83B3-DDABD2847728", - "kind" : "video", - "linkId" : "B6345D38-3912-4C07-BC43-685A4C767720", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 721.6271186440679, - "start" : 283.5593220338983, - "track" : "v0" - }, - { - "duration" : 7.830508474576277, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EE73D2CB-10F9-47A3-B0A9-B077DC6C6B40", - "kind" : "audio", - "linkId" : "B6345D38-3912-4C07-BC43-685A4C767720", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 721.5432471027268, - "start" : 283.5593220338983, - "track" : "v1" - }, - { - "duration" : 7.830508474576277, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BD06C153-DAD3-449C-B108-1A8130000941", - "kind" : "audio", - "linkId" : "B6345D38-3912-4C07-BC43-685A4C767720", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 721.5899234059577, - "start" : 283.5593220338983, - "track" : "v2" - }, - { - "duration" : 7.830508474576277, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "9EFE24ED-F389-4DB3-945B-BF2F18D9915B", - "kind" : "video", - "linkId" : "B6345D38-3912-4C07-BC43-685A4C767720", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 721.4533162697296, - "start" : 283.5593220338983, - "track" : "v3" - }, - { - "duration" : 7.830508474576277, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "26D01EC2-D5AF-4693-809B-C57786F0F277", - "kind" : "video", - "linkId" : "B6345D38-3912-4C07-BC43-685A4C767720", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 721.4533116857308, - "start" : 283.5593220338983, - "track" : "v4" - }, - { - "duration" : 6.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6ACBC2E5-88A0-48BD-ABE1-16D4214541B8", - "kind" : "video", - "linkId" : "9DBD3CAA-D3B8-40E4-9DAB-5C8025F8248C", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 736.3389830508476, - "start" : 291.3898305084746, - "track" : "v0" - }, - { - "duration" : 6.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EBF24B46-FE81-45AB-97D4-6FD5E677B893", - "kind" : "audio", - "linkId" : "9DBD3CAA-D3B8-40E4-9DAB-5C8025F8248C", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 736.2551115095065, - "start" : 291.3898305084746, - "track" : "v1" - }, - { - "duration" : 6.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "DF4C7EBF-43A7-4CDE-837E-4995C5DE3EE7", - "kind" : "audio", - "linkId" : "9DBD3CAA-D3B8-40E4-9DAB-5C8025F8248C", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 736.3017878127374, - "start" : 291.3898305084746, - "track" : "v2" - }, - { - "duration" : 6.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F39E9B23-7810-4DA0-B17A-6D487A801159", - "kind" : "video", - "linkId" : "9DBD3CAA-D3B8-40E4-9DAB-5C8025F8248C", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 736.1651806765093, - "start" : 291.3898305084746, - "track" : "v3" - }, - { - "duration" : 6.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D58A628D-522C-4267-BA46-9E4D55FE030E", - "kind" : "video", - "linkId" : "9DBD3CAA-D3B8-40E4-9DAB-5C8025F8248C", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 736.1651760925105, - "start" : 291.3898305084746, - "track" : "v4" - }, - { - "duration" : 9.288135593220318, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8A9513E7-2C9C-4EB6-84AE-EDFF80DCF552", - "kind" : "video", - "linkId" : "2D759F83-E902-4851-A3CB-8D0088A77D2D", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 747.2881355932204, - "start" : 297.66101694915255, - "track" : "v0" - }, - { - "duration" : 9.288135593220318, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D7CDE17B-5946-4F71-B09B-6983C216681E", - "kind" : "audio", - "linkId" : "2D759F83-E902-4851-A3CB-8D0088A77D2D", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 747.2042640518794, - "start" : 297.66101694915255, - "track" : "v1" - }, - { - "duration" : 9.288135593220318, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "CC586FEC-2BAB-4053-AC79-1FA59BD9BF45", - "kind" : "audio", - "linkId" : "2D759F83-E902-4851-A3CB-8D0088A77D2D", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 747.2509403551103, - "start" : 297.66101694915255, - "track" : "v2" - }, - { - "duration" : 9.288135593220318, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "926C01C2-ADAD-4B46-BE04-CD6B4618CCC3", - "kind" : "video", - "linkId" : "2D759F83-E902-4851-A3CB-8D0088A77D2D", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 747.1143332188822, - "start" : 297.66101694915255, - "track" : "v3" - }, - { - "duration" : 9.288135593220318, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0AB6F4F2-89CA-4D4B-A52E-765F89C40923", - "kind" : "video", - "linkId" : "2D759F83-E902-4851-A3CB-8D0088A77D2D", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 747.1143286348833, - "start" : 297.66101694915255, - "track" : "v4" - }, - { - "duration" : 4.372881355932236, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E84D7CB3-1292-4241-AAD4-AA71BA97E643", - "kind" : "video", - "linkId" : "1F9BC1C6-8DE8-4750-ADF1-DBC052261B98", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 763.2203389830509, - "start" : 306.9491525423729, - "track" : "v0" - }, - { - "duration" : 4.372881355932236, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "48214E82-E8A7-45D1-A218-C6F71276D005", - "kind" : "audio", - "linkId" : "1F9BC1C6-8DE8-4750-ADF1-DBC052261B98", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 763.1364674417098, - "start" : 306.9491525423729, - "track" : "v1" - }, - { - "duration" : 4.372881355932236, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5D3BAE7E-FE37-43FE-9E8C-945DB4037BD3", - "kind" : "audio", - "linkId" : "1F9BC1C6-8DE8-4750-ADF1-DBC052261B98", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 763.1831437449407, - "start" : 306.9491525423729, - "track" : "v2" - }, - { - "duration" : 4.372881355932236, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6B9196EE-5BAF-4BC6-A372-D785A26AB9B6", - "kind" : "video", - "linkId" : "1F9BC1C6-8DE8-4750-ADF1-DBC052261B98", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 763.0465366087126, - "start" : 306.9491525423729, - "track" : "v3" - }, - { - "duration" : 4.372881355932236, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "92FFAC2A-5A18-4CD5-A8E8-38A3237085FB", - "kind" : "video", - "linkId" : "1F9BC1C6-8DE8-4750-ADF1-DBC052261B98", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 763.0465320247138, - "start" : 306.9491525423729, - "track" : "v4" - }, - { - "duration" : 6.203389830508456, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B4E35468-D60F-41C5-9DEB-00DF0D9AB8F0", - "kind" : "video", - "linkId" : "5C054F95-5ECF-452C-8B05-51B9272EC65B", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 771.2203389830509, - "start" : 311.3220338983051, - "track" : "v0" - }, - { - "duration" : 6.203389830508456, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EE8E5E05-B61B-4960-ABF2-9C3490F03B65", - "kind" : "audio", - "linkId" : "5C054F95-5ECF-452C-8B05-51B9272EC65B", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 771.1364674417098, - "start" : 311.3220338983051, - "track" : "v1" - }, - { - "duration" : 6.203389830508456, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "41C6BBFA-FF1F-4336-8A02-CC2A7AD8D711", - "kind" : "audio", - "linkId" : "5C054F95-5ECF-452C-8B05-51B9272EC65B", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 771.1831437449407, - "start" : 311.3220338983051, - "track" : "v2" - }, - { - "duration" : 6.203389830508456, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "01FAD5BD-4079-4B78-964E-021AB45FD327", - "kind" : "video", - "linkId" : "5C054F95-5ECF-452C-8B05-51B9272EC65B", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 771.0465366087126, - "start" : 311.3220338983051, - "track" : "v3" - }, - { - "duration" : 6.203389830508456, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D05C5F7B-1439-48C3-A549-332D2965E644", - "kind" : "video", - "linkId" : "5C054F95-5ECF-452C-8B05-51B9272EC65B", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 771.0465320247138, - "start" : 311.3220338983051, - "track" : "v4" - }, - { - "duration" : 4.033898305084733, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "9ADC6EF4-01E6-44A2-A4E7-CE979C4216A6", - "kind" : "video", - "linkId" : "E7791304-08F7-4757-B49E-97BD758DF635", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 780.271186440678, - "start" : 317.52542372881356, - "track" : "v0" - }, - { - "duration" : 4.033898305084733, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C83F690F-75F2-4F0E-9F44-2B3B64FC4D7D", - "kind" : "audio", - "linkId" : "E7791304-08F7-4757-B49E-97BD758DF635", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 780.1873148993369, - "start" : 317.52542372881356, - "track" : "v1" - }, - { - "duration" : 4.033898305084733, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7F4D1569-EB2B-470A-A39D-4E27BB9631D3", - "kind" : "audio", - "linkId" : "E7791304-08F7-4757-B49E-97BD758DF635", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 780.2339912025678, - "start" : 317.52542372881356, - "track" : "v2" - }, - { - "duration" : 4.033898305084733, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F566D86E-6BCB-450B-B165-E742669968FE", - "kind" : "video", - "linkId" : "E7791304-08F7-4757-B49E-97BD758DF635", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 780.0973840663397, - "start" : 317.52542372881356, - "track" : "v3" - }, - { - "duration" : 4.033898305084733, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "397C4077-BE8E-46D8-9A26-4A85FB164ADD", - "kind" : "video", - "linkId" : "E7791304-08F7-4757-B49E-97BD758DF635", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 780.0973794823409, - "start" : 317.52542372881356, - "track" : "v4" - }, - { - "duration" : 2.474576271186436, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3EA1AE5B-B71F-4DD3-BAB2-258343BC38E6", - "kind" : "video", - "linkId" : "9683EB9F-EC01-480F-8BE7-2201B247DE7E", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 790.7118644067798, - "start" : 321.5593220338983, - "track" : "v0" - }, - { - "duration" : 2.474576271186436, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "84190D3F-8FAE-46C1-8402-B9BE98C961DD", - "kind" : "audio", - "linkId" : "9683EB9F-EC01-480F-8BE7-2201B247DE7E", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 790.6279928654387, - "start" : 321.5593220338983, - "track" : "v1" - }, - { - "duration" : 2.474576271186436, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BA726F72-FB82-4612-8EBA-361CFF5A177B", - "kind" : "audio", - "linkId" : "9683EB9F-EC01-480F-8BE7-2201B247DE7E", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 790.6746691686697, - "start" : 321.5593220338983, - "track" : "v2" - }, - { - "duration" : 2.474576271186436, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A2BD0CC2-F087-4B62-B210-2422E7CDA62E", - "kind" : "video", - "linkId" : "9683EB9F-EC01-480F-8BE7-2201B247DE7E", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 790.5380620324415, - "start" : 321.5593220338983, - "track" : "v3" - }, - { - "duration" : 2.474576271186436, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "CB9AED26-D860-4411-9D20-1B63F56F0BEC", - "kind" : "video", - "linkId" : "9683EB9F-EC01-480F-8BE7-2201B247DE7E", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 790.5380574484427, - "start" : 321.5593220338983, - "track" : "v4" - }, - { - "duration" : 3.525423728813564, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5650BD3B-4BF5-4533-B873-6A89B355DBBD", - "kind" : "video", - "linkId" : "CABF546D-96A6-4629-8BD2-A6CED6EBD7E4", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 794.7118644067798, - "start" : 324.03389830508473, - "track" : "v0" - }, - { - "duration" : 3.525423728813564, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3BA60039-3255-435B-90E4-738D94B13821", - "kind" : "audio", - "linkId" : "CABF546D-96A6-4629-8BD2-A6CED6EBD7E4", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 794.6279928654387, - "start" : 324.03389830508473, - "track" : "v1" - }, - { - "duration" : 3.525423728813564, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1A07AF09-C783-4547-BB45-5671AD0A75E7", - "kind" : "audio", - "linkId" : "CABF546D-96A6-4629-8BD2-A6CED6EBD7E4", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 794.6746691686697, - "start" : 324.03389830508473, - "track" : "v2" - }, - { - "duration" : 3.525423728813564, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A656775F-4507-44FD-ADCB-E23AE9572BD6", - "kind" : "video", - "linkId" : "CABF546D-96A6-4629-8BD2-A6CED6EBD7E4", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 794.5380620324415, - "start" : 324.03389830508473, - "track" : "v3" - }, - { - "duration" : 3.525423728813564, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F287E2E4-9B24-4928-9DF4-BE2A88C6F6E1", - "kind" : "video", - "linkId" : "CABF546D-96A6-4629-8BD2-A6CED6EBD7E4", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 794.5380574484427, - "start" : 324.03389830508473, - "track" : "v4" - }, - { - "duration" : 11.694915254237287, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "10BDB7F1-A97E-46B4-AA17-D5AA082F4FE5", - "kind" : "video", - "linkId" : "FE61ADB3-0C95-4A1A-B7F6-D3B4874BD0EA", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 812.6440677966104, - "start" : 327.5593220338983, - "track" : "v0" - }, - { - "duration" : 11.694915254237287, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1A175E00-C9B8-4071-9395-B96E6BBC0766", - "kind" : "audio", - "linkId" : "FE61ADB3-0C95-4A1A-B7F6-D3B4874BD0EA", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 812.5601962552694, - "start" : 327.5593220338983, - "track" : "v1" - }, - { - "duration" : 11.694915254237287, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "9CDBFDAC-83C2-4F7B-AC48-618437D267F1", - "kind" : "audio", - "linkId" : "FE61ADB3-0C95-4A1A-B7F6-D3B4874BD0EA", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 812.6068725585003, - "start" : 327.5593220338983, - "track" : "v2" - }, - { - "duration" : 11.694915254237287, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F8DB026C-68E2-4110-8310-A4E77894383C", - "kind" : "video", - "linkId" : "FE61ADB3-0C95-4A1A-B7F6-D3B4874BD0EA", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 812.4702654222722, - "start" : 327.5593220338983, - "track" : "v3" - }, - { - "duration" : 11.694915254237287, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2C6F6969-72A9-49A9-8A8D-A15D2D15A434", - "kind" : "video", - "linkId" : "FE61ADB3-0C95-4A1A-B7F6-D3B4874BD0EA", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 812.4702608382734, - "start" : 327.5593220338983, - "track" : "v4" - }, - { - "duration" : 1.3898305084745743, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5D76FCCF-E20B-4E2B-B2BF-06D04D2EC873", - "kind" : "video", - "linkId" : "5A0236C5-2C98-4C5E-A570-96D211E7B3C6", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 980.813559322034, - "start" : 339.2542372881356, - "track" : "v0" - }, - { - "duration" : 1.3898305084745743, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B834F191-B8D9-4288-A76C-5BC67BBEAA7E", - "kind" : "audio", - "linkId" : "5A0236C5-2C98-4C5E-A570-96D211E7B3C6", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 980.729687780693, - "start" : 339.2542372881356, - "track" : "v1" - }, - { - "duration" : 1.3898305084745743, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EC711F21-C8A3-40C5-B230-A451B6878977", - "kind" : "audio", - "linkId" : "5A0236C5-2C98-4C5E-A570-96D211E7B3C6", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 980.7763640839239, - "start" : 339.2542372881356, - "track" : "v2" - }, - { - "duration" : 1.3898305084745743, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3BFC58C2-D801-45E4-8F01-5F94BDB9ED96", - "kind" : "video", - "linkId" : "5A0236C5-2C98-4C5E-A570-96D211E7B3C6", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 980.6397569476958, - "start" : 339.2542372881356, - "track" : "v3" - }, - { - "duration" : 1.3898305084745743, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "28885AEF-445D-47A2-88D3-550BBF692ADE", - "kind" : "video", - "linkId" : "5A0236C5-2C98-4C5E-A570-96D211E7B3C6", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 980.639752363697, - "start" : 339.2542372881356, - "track" : "v4" - }, - { - "duration" : 0.6440677966101589, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EA534368-94B4-475D-8041-45696947070E", - "kind" : "video", - "linkId" : "B31B5062-B2A8-445B-A891-F2D2104E86E3", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 983.1186440677968, - "start" : 340.64406779661016, - "track" : "v0" - }, - { - "duration" : 0.6440677966101589, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F6F6F9A9-8B8B-443A-B401-32205A74C093", - "kind" : "audio", - "linkId" : "B31B5062-B2A8-445B-A891-F2D2104E86E3", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 983.0347725264558, - "start" : 340.64406779661016, - "track" : "v1" - }, - { - "duration" : 0.6440677966101589, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1176FB5D-D780-4273-9FCA-9695A04A100F", - "kind" : "audio", - "linkId" : "B31B5062-B2A8-445B-A891-F2D2104E86E3", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 983.0814488296867, - "start" : 340.64406779661016, - "track" : "v2" - }, - { - "duration" : 0.6440677966101589, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5DA60AD6-AC7E-4579-86CD-50F79A14150D", - "kind" : "video", - "linkId" : "B31B5062-B2A8-445B-A891-F2D2104E86E3", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 982.9448416934586, - "start" : 340.64406779661016, - "track" : "v3" - }, - { - "duration" : 0.6440677966101589, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3DBD1518-ACEA-4CC3-9B85-FB6C1BFDDDE4", - "kind" : "video", - "linkId" : "B31B5062-B2A8-445B-A891-F2D2104E86E3", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 982.9448371094597, - "start" : 340.64406779661016, - "track" : "v4" - }, - { - "duration" : 3.6271186440678207, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "832B3ED1-5BC4-43EE-8FE6-717D93008C5A", - "kind" : "video", - "linkId" : "596C8323-127D-44C1-B319-7DF1FB8B1171", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1037.8305084745766, - "start" : 341.2881355932203, - "track" : "v0" - }, - { - "duration" : 3.6271186440678207, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "00209D2F-08A1-4B49-96F2-91A8C5EF454D", - "kind" : "audio", - "linkId" : "596C8323-127D-44C1-B319-7DF1FB8B1171", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1037.7466369332355, - "start" : 341.2881355932203, - "track" : "v1" - }, - { - "duration" : 3.6271186440678207, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "43AEF5F0-29EF-45AD-A7B6-893C574685CB", - "kind" : "audio", - "linkId" : "596C8323-127D-44C1-B319-7DF1FB8B1171", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1037.7933132364665, - "start" : 341.2881355932203, - "track" : "v2" - }, - { - "duration" : 3.6271186440678207, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "32BC4FE2-4816-49E7-80A0-2DA08DCC56A0", - "kind" : "video", - "linkId" : "596C8323-127D-44C1-B319-7DF1FB8B1171", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1037.6567061002384, - "start" : 341.2881355932203, - "track" : "v3" - }, - { - "duration" : 3.6271186440678207, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7C5CF291-4A7F-4BBD-809E-A9094B0B00E0", - "kind" : "video", - "linkId" : "596C8323-127D-44C1-B319-7DF1FB8B1171", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1037.6567015162395, - "start" : 341.2881355932203, - "track" : "v4" - }, - { - "duration" : 1.355932203389841, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0905362C-DE86-4A3A-B256-9FD740D3A19D", - "kind" : "video", - "linkId" : "E5D49AA9-6CD5-438D-8B96-6ADA7002033B", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1122.203389830509, - "start" : 344.91525423728814, - "track" : "v0" - }, - { - "duration" : 1.355932203389841, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "48C76EE1-E83E-45E8-A767-47E35F66576F", - "kind" : "audio", - "linkId" : "E5D49AA9-6CD5-438D-8B96-6ADA7002033B", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1122.119518289168, - "start" : 344.91525423728814, - "track" : "v1" - }, - { - "duration" : 1.355932203389841, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2F82F324-00A9-48EC-BF8C-DAA85B8C9D73", - "kind" : "audio", - "linkId" : "E5D49AA9-6CD5-438D-8B96-6ADA7002033B", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1122.1661945923988, - "start" : 344.91525423728814, - "track" : "v2" - }, - { - "duration" : 1.355932203389841, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5B27AC9E-88CF-4AB2-927D-A79412E95F09", - "kind" : "video", - "linkId" : "E5D49AA9-6CD5-438D-8B96-6ADA7002033B", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1122.0295874561707, - "start" : 344.91525423728814, - "track" : "v3" - }, - { - "duration" : 1.355932203389841, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6CC1FC2E-3809-4A37-87B2-3255B1430A3C", - "kind" : "video", - "linkId" : "E5D49AA9-6CD5-438D-8B96-6ADA7002033B", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1122.0295828721719, - "start" : 344.91525423728814, - "track" : "v4" - }, - { - "duration" : 5.0847457627118615, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F27B9C51-75DE-4E4F-8808-3BA889863CF9", - "kind" : "video", - "linkId" : "81F5C3C3-3A90-4A30-8056-87972464F37F", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1129.254237288136, - "start" : 346.271186440678, - "track" : "v0" - }, - { - "duration" : 5.0847457627118615, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D6CB9F93-3A96-4AD3-8448-52E45B33A28D", - "kind" : "audio", - "linkId" : "81F5C3C3-3A90-4A30-8056-87972464F37F", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1129.170365746795, - "start" : 346.271186440678, - "track" : "v1" - }, - { - "duration" : 5.0847457627118615, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "34239B0B-3D34-4D29-9C48-D18A775FCDDB", - "kind" : "audio", - "linkId" : "81F5C3C3-3A90-4A30-8056-87972464F37F", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1129.2170420500258, - "start" : 346.271186440678, - "track" : "v2" - }, - { - "duration" : 5.0847457627118615, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C0EC3471-CC80-448C-BA8B-6F565ED9EE06", - "kind" : "video", - "linkId" : "81F5C3C3-3A90-4A30-8056-87972464F37F", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1129.0804349137977, - "start" : 346.271186440678, - "track" : "v3" - }, - { - "duration" : 5.0847457627118615, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "89A2AE13-B7C0-4138-B475-2D7ED7D1BEEA", - "kind" : "video", - "linkId" : "81F5C3C3-3A90-4A30-8056-87972464F37F", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1129.080430329799, - "start" : 346.271186440678, - "track" : "v4" - }, - { - "duration" : 4.711864406779625, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2E892202-DFC8-4722-848D-8ED956667E0D", - "kind" : "video", - "linkId" : "CD0169DB-72F4-4E7A-B27D-1D1C0A2437D9", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1278.2372881355936, - "start" : 351.35593220338984, - "track" : "v0" - }, - { - "duration" : 4.711864406779625, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "43D45A86-49A1-449A-9E77-900F5D0ACC61", - "kind" : "audio", - "linkId" : "CD0169DB-72F4-4E7A-B27D-1D1C0A2437D9", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1278.1534165942526, - "start" : 351.35593220338984, - "track" : "v1" - }, - { - "duration" : 4.711864406779625, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "9B4179AA-3FE3-4B93-B578-86F7BDFBAC59", - "kind" : "audio", - "linkId" : "CD0169DB-72F4-4E7A-B27D-1D1C0A2437D9", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1278.2000928974835, - "start" : 351.35593220338984, - "track" : "v2" - }, - { - "duration" : 4.711864406779625, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "84160650-3975-445E-9CCE-FE9A4B354DE4", - "kind" : "video", - "linkId" : "CD0169DB-72F4-4E7A-B27D-1D1C0A2437D9", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1278.0634857612554, - "start" : 351.35593220338984, - "track" : "v3" - }, - { - "duration" : 4.711864406779625, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EEE0A850-F637-47A2-A351-78A5E35E2E71", - "kind" : "video", - "linkId" : "CD0169DB-72F4-4E7A-B27D-1D1C0A2437D9", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1278.0634811772566, - "start" : 351.35593220338984, - "track" : "v4" - }, - { - "duration" : 12.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "40E92BD1-0CD5-4846-B649-57DEE758C62A", - "kind" : "video", - "linkId" : "7D4C2DFB-52DA-48A5-B7A5-8C50DE99C254", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1283.4237288135596, - "start" : 356.06779661016947, - "track" : "v0" - }, - { - "duration" : 12.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A9E11104-5BA1-47B5-A29B-0DB9D895655D", - "kind" : "audio", - "linkId" : "7D4C2DFB-52DA-48A5-B7A5-8C50DE99C254", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1283.3398572722185, - "start" : 356.06779661016947, - "track" : "v1" - }, - { - "duration" : 12.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EB6EE9AD-7386-4D10-BFBF-E253B17F3431", - "kind" : "audio", - "linkId" : "7D4C2DFB-52DA-48A5-B7A5-8C50DE99C254", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1283.3865335754494, - "start" : 356.06779661016947, - "track" : "v2" - }, - { - "duration" : 12.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "438EA227-D2C5-4532-9A3A-252007721608", - "kind" : "video", - "linkId" : "7D4C2DFB-52DA-48A5-B7A5-8C50DE99C254", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1283.2499264392213, - "start" : 356.06779661016947, - "track" : "v3" - }, - { - "duration" : 12.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E7E7A787-8710-4FC3-8D04-8FB4A9808A63", - "kind" : "video", - "linkId" : "7D4C2DFB-52DA-48A5-B7A5-8C50DE99C254", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1283.2499218552225, - "start" : 356.06779661016947, - "track" : "v4" - }, - { - "duration" : 3.0508474576271283, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "65671782-CE0B-49E2-AF2D-C5D2D41FFA24", - "kind" : "video", - "linkId" : "D50E12ED-A0DF-4078-95DE-6D5332CDFB62", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1304.271186440678, - "start" : 368.33898305084745, - "track" : "v0" - }, - { - "duration" : 3.0508474576271283, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B250A8E7-5DB5-463B-BC7E-2A985C554A48", - "kind" : "audio", - "linkId" : "D50E12ED-A0DF-4078-95DE-6D5332CDFB62", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1304.187314899337, - "start" : 368.33898305084745, - "track" : "v1" - }, - { - "duration" : 3.0508474576271283, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "538F899C-5E20-49EC-8416-74178778FE07", - "kind" : "audio", - "linkId" : "D50E12ED-A0DF-4078-95DE-6D5332CDFB62", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1304.233991202568, - "start" : 368.33898305084745, - "track" : "v2" - }, - { - "duration" : 3.0508474576271283, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "AB06969E-2217-4DDA-B6A6-0537DB89EDD0", - "kind" : "video", - "linkId" : "D50E12ED-A0DF-4078-95DE-6D5332CDFB62", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1304.0973840663398, - "start" : 368.33898305084745, - "track" : "v3" - }, - { - "duration" : 3.0508474576271283, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "84921634-8C3A-4FDC-9737-68D94154D037", - "kind" : "video", - "linkId" : "D50E12ED-A0DF-4078-95DE-6D5332CDFB62", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1304.097379482341, - "start" : 368.33898305084745, - "track" : "v4" - }, - { - "duration" : 6.610169491525426, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B2E2127E-A4B7-4682-BC09-FE4FBF53EC66", - "kind" : "video", - "linkId" : "4A3BE2AA-0118-46FB-9311-40602A8063DF", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1309.6610169491526, - "start" : 371.3898305084746, - "track" : "v0" - }, - { - "duration" : 6.610169491525426, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A2F9926B-983E-4F53-99C7-92BB9B023122", - "kind" : "audio", - "linkId" : "4A3BE2AA-0118-46FB-9311-40602A8063DF", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1309.5771454078115, - "start" : 371.3898305084746, - "track" : "v1" - }, - { - "duration" : 6.610169491525426, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D13D2198-79F9-4915-A5E0-AC690ADBC371", - "kind" : "audio", - "linkId" : "4A3BE2AA-0118-46FB-9311-40602A8063DF", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1309.6238217110424, - "start" : 371.3898305084746, - "track" : "v2" - }, - { - "duration" : 6.610169491525426, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "53651C3A-4919-49F0-ADED-34CDF520F07E", - "kind" : "video", - "linkId" : "4A3BE2AA-0118-46FB-9311-40602A8063DF", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1309.4872145748143, - "start" : 371.3898305084746, - "track" : "v3" - }, - { - "duration" : 6.610169491525426, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "00F70279-9C4E-40AB-8D35-B686234D9CD2", - "kind" : "video", - "linkId" : "4A3BE2AA-0118-46FB-9311-40602A8063DF", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1309.4872099908155, - "start" : 371.3898305084746, - "track" : "v4" - }, - { - "duration" : 6.372881355932179, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3EB22ECC-5EAD-4107-9F24-6864C9464EAC", - "kind" : "video", - "linkId" : "346B7025-BD91-4339-98B4-DBE997BC1A27", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1319.1186440677966, - "start" : 378, - "track" : "v0" - }, - { - "duration" : 6.372881355932179, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2B2021EE-7138-45C6-A0CF-0D8BBAC1116A", - "kind" : "audio", - "linkId" : "346B7025-BD91-4339-98B4-DBE997BC1A27", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1319.0347725264555, - "start" : 378, - "track" : "v1" - }, - { - "duration" : 6.372881355932179, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4A8A3D03-F2FB-4651-A609-318676737F28", - "kind" : "audio", - "linkId" : "346B7025-BD91-4339-98B4-DBE997BC1A27", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1319.0814488296865, - "start" : 378, - "track" : "v2" - }, - { - "duration" : 6.372881355932179, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "196CB0CE-61ED-4852-A694-AB8B592CF510", - "kind" : "video", - "linkId" : "346B7025-BD91-4339-98B4-DBE997BC1A27", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1318.9448416934583, - "start" : 378, - "track" : "v3" - }, - { - "duration" : 6.372881355932179, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "AB9E730D-3DB3-430E-92B1-8528951D9146", - "kind" : "video", - "linkId" : "346B7025-BD91-4339-98B4-DBE997BC1A27", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1318.9448371094595, - "start" : 378, - "track" : "v4" - }, - { - "duration" : 10.983050847457662, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "59AB16D3-EBCD-438B-8B8C-88413E1E8D67", - "kind" : "video", - "linkId" : "8A7CFF6A-2FE9-4C3F-813E-9DDC903D1C26", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1327.1186440677966, - "start" : 384.3728813559322, - "track" : "v0" - }, - { - "duration" : 10.983050847457662, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "77501D06-4929-403D-8FCB-6E79E9A296A4", - "kind" : "audio", - "linkId" : "8A7CFF6A-2FE9-4C3F-813E-9DDC903D1C26", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1327.0347725264555, - "start" : 384.3728813559322, - "track" : "v1" - }, - { - "duration" : 10.983050847457662, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A8B5D5C7-311F-4D71-BD19-7B1BAC7B047E", - "kind" : "audio", - "linkId" : "8A7CFF6A-2FE9-4C3F-813E-9DDC903D1C26", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1327.0814488296865, - "start" : 384.3728813559322, - "track" : "v2" - }, - { - "duration" : 10.983050847457662, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F9DB0D5E-9113-4D5E-81AB-EAE9AF22C7FE", - "kind" : "video", - "linkId" : "8A7CFF6A-2FE9-4C3F-813E-9DDC903D1C26", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1326.9448416934583, - "start" : 384.3728813559322, - "track" : "v3" - }, - { - "duration" : 10.983050847457662, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D4A49349-31F4-41F7-91B5-682C810269F4", - "kind" : "video", - "linkId" : "8A7CFF6A-2FE9-4C3F-813E-9DDC903D1C26", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1326.9448371094595, - "start" : 384.3728813559322, - "track" : "v4" - }, - { - "duration" : 14.169491525423723, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3BEF9E6B-7901-407C-BEA3-7AFAB2A27DF5", - "kind" : "video", - "linkId" : "704CF8B0-3041-4053-9080-715DFE6B2D25", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1389.0169491525423, - "start" : 395.35593220338984, - "track" : "v0" - }, - { - "duration" : 14.169491525423723, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BC1D4D97-B973-4693-9B24-5823A5192FAA", - "kind" : "audio", - "linkId" : "704CF8B0-3041-4053-9080-715DFE6B2D25", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1388.9330776112013, - "start" : 395.35593220338984, - "track" : "v1" - }, - { - "duration" : 14.169491525423723, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "DBBCAF21-55F8-44F0-9F8A-4EA556B8EB57", - "kind" : "audio", - "linkId" : "704CF8B0-3041-4053-9080-715DFE6B2D25", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1388.9797539144322, - "start" : 395.35593220338984, - "track" : "v2" - }, - { - "duration" : 14.169491525423723, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6C7A0EB2-88FC-4404-B48D-AA7FE6269170", - "kind" : "video", - "linkId" : "704CF8B0-3041-4053-9080-715DFE6B2D25", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1388.843146778204, - "start" : 395.35593220338984, - "track" : "v3" - }, - { - "duration" : 14.169491525423723, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "50068A9B-E7CF-4DD9-9917-7155FEEE9EA2", - "kind" : "video", - "linkId" : "704CF8B0-3041-4053-9080-715DFE6B2D25", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1388.8431421942053, - "start" : 395.35593220338984, - "track" : "v4" - }, - { - "duration" : 12.610169491525426, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EA7CF47C-E4D3-437C-B0E5-4275167856C5", - "kind" : "video", - "linkId" : "5E30C5A7-ECB1-41A5-9456-2F737C45E0C1", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1406.5762711864404, - "start" : 409.52542372881356, - "track" : "v0" - }, - { - "duration" : 12.610169491525426, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1E9A4B81-6D6C-4CEF-BCD9-A56D14BC12E5", - "kind" : "audio", - "linkId" : "5E30C5A7-ECB1-41A5-9456-2F737C45E0C1", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1406.4923996450993, - "start" : 409.52542372881356, - "track" : "v1" - }, - { - "duration" : 12.610169491525426, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1506A648-D35B-4619-A082-06FF4E3CBA5B", - "kind" : "audio", - "linkId" : "5E30C5A7-ECB1-41A5-9456-2F737C45E0C1", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1406.5390759483303, - "start" : 409.52542372881356, - "track" : "v2" - }, - { - "duration" : 12.610169491525426, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "9D08D70B-5627-48C7-873D-A8153FA5DA0E", - "kind" : "video", - "linkId" : "5E30C5A7-ECB1-41A5-9456-2F737C45E0C1", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1406.4024688121021, - "start" : 409.52542372881356, - "track" : "v3" - }, - { - "duration" : 12.610169491525426, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A6181FAE-7AFB-4D19-B593-91540501DC66", - "kind" : "video", - "linkId" : "5E30C5A7-ECB1-41A5-9456-2F737C45E0C1", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1406.4024642281033, - "start" : 409.52542372881356, - "track" : "v4" - }, - { - "duration" : 12.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4E3F3515-7F7B-447C-BC9F-E4220A2BA2A2", - "kind" : "video", - "linkId" : "226C31FD-B8DF-4F4A-A67B-DCB285B8B836", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1434.1694915254236, - "start" : 428.271186440678, - "track" : "v0" - }, - { - "duration" : 12.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "635A9B0E-7D5C-4F08-BEB1-0D43CEB95071", - "kind" : "audio", - "linkId" : "226C31FD-B8DF-4F4A-A67B-DCB285B8B836", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1434.0856199840825, - "start" : 428.271186440678, - "track" : "v1" - }, - { - "duration" : 12.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B26C43CA-9142-4FF8-8DE6-5FB3CF02701F", - "kind" : "audio", - "linkId" : "226C31FD-B8DF-4F4A-A67B-DCB285B8B836", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1434.1322962873135, - "start" : 428.271186440678, - "track" : "v2" - }, - { - "duration" : 12.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1D681666-828B-4516-905F-70C0B9927714", - "kind" : "video", - "linkId" : "226C31FD-B8DF-4F4A-A67B-DCB285B8B836", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1433.9956891510853, - "start" : 428.271186440678, - "track" : "v3" - }, - { - "duration" : 12.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "14FFF5E0-4F74-4347-AEC2-714B77823B90", - "kind" : "video", - "linkId" : "226C31FD-B8DF-4F4A-A67B-DCB285B8B836", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1433.9956845670865, - "start" : 428.271186440678, - "track" : "v4" - }, - { - "duration" : 6.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "651982BC-F62C-433E-9CA6-7F53FDD18AD2", - "kind" : "video", - "linkId" : "985611EC-FA7E-4535-BFEC-AECD319001B6", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1428.0338983050847, - "start" : 422.135593220339, - "track" : "v0" - }, - { - "duration" : 6.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "011DD52B-7DF9-4AB2-9143-9249F293A98C", - "kind" : "audio", - "linkId" : "985611EC-FA7E-4535-BFEC-AECD319001B6", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1427.9500267637436, - "start" : 422.135593220339, - "track" : "v1" - }, - { - "duration" : 6.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "9C6C77CE-A577-41A8-AF62-5B1AD22511CB", - "kind" : "audio", - "linkId" : "985611EC-FA7E-4535-BFEC-AECD319001B6", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1427.9967030669745, - "start" : 422.135593220339, - "track" : "v2" - }, - { - "duration" : 6.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "81445B0A-E2DC-4623-B61E-F78297F1AF9B", - "kind" : "video", - "linkId" : "985611EC-FA7E-4535-BFEC-AECD319001B6", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1427.8600959307464, - "start" : 422.135593220339, - "track" : "v3" - }, - { - "duration" : 6.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2FCF79AA-A233-43B0-924A-F22316673337", - "kind" : "video", - "linkId" : "985611EC-FA7E-4535-BFEC-AECD319001B6", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1427.8600913467476, - "start" : 422.135593220339, - "track" : "v4" - }, - { - "duration" : 9.45762711864404, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3226AEDC-71C8-4045-95AB-2938C4E153FF", - "kind" : "video", - "linkId" : "ABBC461D-6EED-494A-8B1A-7677A2E7D027", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1449.559322033898, - "start" : 440.40677966101697, - "track" : "v0" - }, - { - "duration" : 9.45762711864404, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FAF956D0-C01F-4926-93F2-49FA34A04E13", - "kind" : "audio", - "linkId" : "ABBC461D-6EED-494A-8B1A-7677A2E7D027", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1449.475450492557, - "start" : 440.40677966101697, - "track" : "v1" - }, - { - "duration" : 9.45762711864404, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3EB9B48B-CACE-4489-BAFE-59EB0F2E6305", - "kind" : "audio", - "linkId" : "ABBC461D-6EED-494A-8B1A-7677A2E7D027", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1449.522126795788, - "start" : 440.40677966101697, - "track" : "v2" - }, - { - "duration" : 9.45762711864404, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4406B9FA-D633-49BC-BB25-FA2554A8FA6A", - "kind" : "video", - "linkId" : "ABBC461D-6EED-494A-8B1A-7677A2E7D027", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1449.3855196595598, - "start" : 440.40677966101697, - "track" : "v3" - }, - { - "duration" : 9.45762711864404, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "99E4D7B0-8E74-4871-B79C-5EE9AE03E424", - "kind" : "video", - "linkId" : "ABBC461D-6EED-494A-8B1A-7677A2E7D027", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1449.385515075561, - "start" : 440.40677966101697, - "track" : "v4" - }, - { - "duration" : 6.677966101694949, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "559361D5-9DB2-4B63-8E39-79DEB5DA658A", - "kind" : "video", - "linkId" : "F4E6AD2E-D33A-4B9D-9CC7-E85C4DF5F3C7", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1462.64406779661, - "start" : 449.864406779661, - "track" : "v0" - }, - { - "duration" : 6.677966101694949, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "37884883-822C-45CA-B6E1-133027A1718A", - "kind" : "audio", - "linkId" : "F4E6AD2E-D33A-4B9D-9CC7-E85C4DF5F3C7", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1462.560196255269, - "start" : 449.864406779661, - "track" : "v1" - }, - { - "duration" : 6.677966101694949, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "81675B52-F6A6-4C8A-997F-8AF4803A89E7", - "kind" : "audio", - "linkId" : "F4E6AD2E-D33A-4B9D-9CC7-E85C4DF5F3C7", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1462.6068725584998, - "start" : 449.864406779661, - "track" : "v2" - }, - { - "duration" : 6.677966101694949, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4787230C-8C29-4CF7-854E-A3AB00759466", - "kind" : "video", - "linkId" : "F4E6AD2E-D33A-4B9D-9CC7-E85C4DF5F3C7", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1462.4702654222717, - "start" : 449.864406779661, - "track" : "v3" - }, - { - "duration" : 6.677966101694949, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "CDA54502-AAC3-496A-B251-B43F1F744E11", - "kind" : "video", - "linkId" : "F4E6AD2E-D33A-4B9D-9CC7-E85C4DF5F3C7", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1462.470260838273, - "start" : 449.864406779661, - "track" : "v4" - }, - { - "duration" : 2.1016949152541997, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "CEDDFF86-9747-4058-98DC-1EB7A4B1F572", - "kind" : "video", - "linkId" : "EDBE29AE-0AD9-452E-A93A-771CE2BE813D", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1471.6610169491523, - "start" : 456.54237288135596, - "track" : "v0" - }, - { - "duration" : 2.1016949152541997, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "65DBD942-E0B7-428A-B955-F7C74957C7E7", - "kind" : "audio", - "linkId" : "EDBE29AE-0AD9-452E-A93A-771CE2BE813D", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1471.5771454078113, - "start" : 456.54237288135596, - "track" : "v1" - }, - { - "duration" : 2.1016949152541997, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F6898171-8F65-41C6-8518-DFF6DBBABC04", - "kind" : "audio", - "linkId" : "EDBE29AE-0AD9-452E-A93A-771CE2BE813D", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1471.6238217110422, - "start" : 456.54237288135596, - "track" : "v2" - }, - { - "duration" : 2.1016949152541997, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D865CA10-5853-486F-82AD-02B05EE60EDB", - "kind" : "video", - "linkId" : "EDBE29AE-0AD9-452E-A93A-771CE2BE813D", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1471.487214574814, - "start" : 456.54237288135596, - "track" : "v3" - }, - { - "duration" : 2.1016949152541997, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0616F857-62D2-44F6-A078-21F47E1CD066", - "kind" : "video", - "linkId" : "EDBE29AE-0AD9-452E-A93A-771CE2BE813D", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1471.4872099908152, - "start" : 456.54237288135596, - "track" : "v4" - }, - { - "duration" : 8.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A734B468-A8AC-4E1B-BBFF-00BCD0D78390", - "kind" : "video", - "linkId" : "E9457961-D7DC-4D55-8C56-3E6D232AC821", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1478.3389830508472, - "start" : 458.64406779661016, - "track" : "v0" - }, - { - "duration" : 8.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0B521685-7B77-48BB-B145-8FC0FC1B8D14", - "kind" : "audio", - "linkId" : "E9457961-D7DC-4D55-8C56-3E6D232AC821", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1478.2551115095062, - "start" : 458.64406779661016, - "track" : "v1" - }, - { - "duration" : 8.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B79DCE82-D278-4E64-BF3E-DD1383DE9320", - "kind" : "audio", - "linkId" : "E9457961-D7DC-4D55-8C56-3E6D232AC821", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1478.301787812737, - "start" : 458.64406779661016, - "track" : "v2" - }, - { - "duration" : 8.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C1373384-5CBC-48DA-AEEA-B1DC0A1277AB", - "kind" : "video", - "linkId" : "E9457961-D7DC-4D55-8C56-3E6D232AC821", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1478.165180676509, - "start" : 458.64406779661016, - "track" : "v3" - }, - { - "duration" : 8.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8B3DE2A2-8DE0-408E-96C4-669DAE9AD4A9", - "kind" : "video", - "linkId" : "E9457961-D7DC-4D55-8C56-3E6D232AC821", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1478.1651760925101, - "start" : 458.64406779661016, - "track" : "v4" - }, - { - "duration" : 2.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EFA97D54-119E-4829-9D03-E931F198D283", - "kind" : "video", - "linkId" : "8FAC32E3-47A3-49EC-A6FC-51E8FD39E2C8", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1491.2203389830506, - "start" : 466.7457627118644, - "track" : "v0" - }, - { - "duration" : 2.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D10DB1AB-A596-41C4-99D6-2874E01C4666", - "kind" : "audio", - "linkId" : "8FAC32E3-47A3-49EC-A6FC-51E8FD39E2C8", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1491.1364674417096, - "start" : 466.7457627118644, - "track" : "v1" - }, - { - "duration" : 2.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "05FD9DBC-666B-4F86-98C5-B006D88468C8", - "kind" : "audio", - "linkId" : "8FAC32E3-47A3-49EC-A6FC-51E8FD39E2C8", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1491.1831437449405, - "start" : 466.7457627118644, - "track" : "v2" - }, - { - "duration" : 2.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F8379F4E-716C-4AB7-A712-E90386ACAAF5", - "kind" : "video", - "linkId" : "8FAC32E3-47A3-49EC-A6FC-51E8FD39E2C8", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1491.0465366087124, - "start" : 466.7457627118644, - "track" : "v3" - }, - { - "duration" : 2.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0AE01210-BDC5-4A27-A1D9-447FB98CBD8C", - "kind" : "video", - "linkId" : "8FAC32E3-47A3-49EC-A6FC-51E8FD39E2C8", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1491.0465320247135, - "start" : 466.7457627118644, - "track" : "v4" - }, - { - "duration" : 12.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "17C94894-CE9A-414A-99D9-983680DBF3C0", - "kind" : "video", - "linkId" : "BD0AA767-5E48-4364-B00E-41745729A942", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1493.3559322033896, - "start" : 468.8813559322034, - "track" : "v0" - }, - { - "duration" : 12.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "517FEEE5-722F-4AF0-9739-D6C4CBFF13F7", - "kind" : "audio", - "linkId" : "BD0AA767-5E48-4364-B00E-41745729A942", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1493.2720606620485, - "start" : 468.8813559322034, - "track" : "v1" - }, - { - "duration" : 12.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "66CDAA10-44F3-4B54-AE2E-76150ACC9CD0", - "kind" : "audio", - "linkId" : "BD0AA767-5E48-4364-B00E-41745729A942", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1493.3187369652794, - "start" : 468.8813559322034, - "track" : "v2" - }, - { - "duration" : 12.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A6487CCD-4DF5-4DD5-BFE5-2536683FBC19", - "kind" : "video", - "linkId" : "BD0AA767-5E48-4364-B00E-41745729A942", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1493.1821298290513, - "start" : 468.8813559322034, - "track" : "v3" - }, - { - "duration" : 12.13559322033899, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3E4AB4B8-236F-4408-8452-95DFA2ED6598", - "kind" : "video", - "linkId" : "BD0AA767-5E48-4364-B00E-41745729A942", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1493.1821252450525, - "start" : 468.8813559322034, - "track" : "v4" - }, - { - "duration" : 2.7796610169491487, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D2B2B51E-C0F9-43FA-A96C-FF04E0956544", - "kind" : "video", - "linkId" : "F6AD378F-5CCF-4E64-988D-88B2876624B9", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1537.8644067796606, - "start" : 481.0169491525424, - "track" : "v0" - }, - { - "duration" : 2.7796610169491487, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6F365D36-DA75-4DAC-875E-2799D7405B79", - "kind" : "audio", - "linkId" : "F6AD378F-5CCF-4E64-988D-88B2876624B9", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1537.7805352383195, - "start" : 481.0169491525424, - "track" : "v1" - }, - { - "duration" : 2.7796610169491487, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6086A913-26DD-4E7A-8DA8-5B7FF6C55700", - "kind" : "audio", - "linkId" : "F6AD378F-5CCF-4E64-988D-88B2876624B9", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1537.8272115415505, - "start" : 481.0169491525424, - "track" : "v2" - }, - { - "duration" : 2.7796610169491487, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2F29A3B0-A10F-4DB4-B9E2-B76DBFA73359", - "kind" : "video", - "linkId" : "F6AD378F-5CCF-4E64-988D-88B2876624B9", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1537.6906044053223, - "start" : 481.0169491525424, - "track" : "v3" - }, - { - "duration" : 2.7796610169491487, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1684BBB7-47B4-4BFA-B8C1-058C3FAC9CED", - "kind" : "video", - "linkId" : "F6AD378F-5CCF-4E64-988D-88B2876624B9", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1537.6905998213235, - "start" : 481.0169491525424, - "track" : "v4" - }, - { - "duration" : 1.0847457627118615, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A7680D68-6BCD-4F79-A28F-858D96D4A6E2", - "kind" : "video", - "linkId" : "A3087F67-EF84-47FF-B3BE-7F42F50669A0", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1540.6440677966098, - "start" : 483.79661016949154, - "track" : "v0" - }, - { - "duration" : 1.0847457627118615, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3854E595-101A-4295-A1B3-5765E651A0AF", - "kind" : "audio", - "linkId" : "A3087F67-EF84-47FF-B3BE-7F42F50669A0", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1540.5601962552687, - "start" : 483.79661016949154, - "track" : "v1" - }, - { - "duration" : 1.0847457627118615, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D256891E-1C5E-4D56-9CA9-3638EDDF53E4", - "kind" : "audio", - "linkId" : "A3087F67-EF84-47FF-B3BE-7F42F50669A0", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1540.6068725584996, - "start" : 483.79661016949154, - "track" : "v2" - }, - { - "duration" : 1.0847457627118615, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B12D34EF-C8C7-49A2-8C6F-04B3A3068BC7", - "kind" : "video", - "linkId" : "A3087F67-EF84-47FF-B3BE-7F42F50669A0", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1540.4702654222715, - "start" : 483.79661016949154, - "track" : "v3" - }, - { - "duration" : 1.0847457627118615, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D07A239B-1D03-40C0-82F5-BF4EE984E137", - "kind" : "video", - "linkId" : "A3087F67-EF84-47FF-B3BE-7F42F50669A0", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1540.4702608382727, - "start" : 483.79661016949154, - "track" : "v4" - }, - { - "duration" : 3.5932203389830306, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B7BE7F5F-53C9-40B3-9F12-7DF7A2D9685A", - "kind" : "video", - "linkId" : "F9AD2997-81A6-4EF6-BD27-5B43EE87F75A", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1630.745762711864, - "start" : 484.8813559322034, - "track" : "v0" - }, - { - "duration" : 3.5932203389830306, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B0F7D729-54ED-422A-B202-11DF2760B5F3", - "kind" : "audio", - "linkId" : "F9AD2997-81A6-4EF6-BD27-5B43EE87F75A", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1630.661891170523, - "start" : 484.8813559322034, - "track" : "v1" - }, - { - "duration" : 3.5932203389830306, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "905A3ACC-296C-4843-B9D9-B86F50F5BA5F", - "kind" : "audio", - "linkId" : "F9AD2997-81A6-4EF6-BD27-5B43EE87F75A", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1630.7085674737539, - "start" : 484.8813559322034, - "track" : "v2" - }, - { - "duration" : 3.5932203389830306, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0B7D26CE-983C-4362-B1CD-AC3A40378E3B", - "kind" : "video", - "linkId" : "F9AD2997-81A6-4EF6-BD27-5B43EE87F75A", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1630.5719603375258, - "start" : 484.8813559322034, - "track" : "v3" - }, - { - "duration" : 3.5932203389830306, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "AFE6D697-39D5-420E-BD1B-A64E0A13DC41", - "kind" : "video", - "linkId" : "F9AD2997-81A6-4EF6-BD27-5B43EE87F75A", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1630.571955753527, - "start" : 484.8813559322034, - "track" : "v4" - }, - { - "duration" : 5.525423728813564, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FFF0A0F3-132A-4462-A828-0FAEB88BBC4D", - "kind" : "video", - "linkId" : "0DB5CF0D-9192-4B84-94D8-6B5373F51616", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1690.881355932203, - "start" : 488.47457627118644, - "track" : "v0" - }, - { - "duration" : 5.525423728813564, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8C1B2FDC-C8A5-44ED-9F1E-CA222B045C32", - "kind" : "audio", - "linkId" : "0DB5CF0D-9192-4B84-94D8-6B5373F51616", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1690.7974843908619, - "start" : 488.47457627118644, - "track" : "v1" - }, - { - "duration" : 5.525423728813564, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "630C64EC-19AC-4FC2-B36B-5B8934D80D33", - "kind" : "audio", - "linkId" : "0DB5CF0D-9192-4B84-94D8-6B5373F51616", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1690.8441606940928, - "start" : 488.47457627118644, - "track" : "v2" - }, - { - "duration" : 5.525423728813564, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "234A86B0-ECCC-4314-9B52-A58DB3C619BB", - "kind" : "video", - "linkId" : "0DB5CF0D-9192-4B84-94D8-6B5373F51616", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1690.7075535578647, - "start" : 488.47457627118644, - "track" : "v3" - }, - { - "duration" : 5.525423728813564, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "02321509-9B22-45BA-884A-BFB975074D08", - "kind" : "video", - "linkId" : "0DB5CF0D-9192-4B84-94D8-6B5373F51616", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1690.7075489738659, - "start" : 488.47457627118644, - "track" : "v4" - }, - { - "duration" : 3.9661016949152668, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A8D392D7-E60E-4189-B7D6-B1C2B5E6927D", - "kind" : "video", - "linkId" : "A50B7D21-1369-498E-A893-10D9266DA682", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1703.5254237288132, - "start" : 494, - "track" : "v0" - }, - { - "duration" : 3.9661016949152668, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "79B92814-E4D4-422A-B414-FEC1D42BABF8", - "kind" : "audio", - "linkId" : "A50B7D21-1369-498E-A893-10D9266DA682", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1703.441552187472, - "start" : 494, - "track" : "v1" - }, - { - "duration" : 3.9661016949152668, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "154F32D0-7DAB-433B-833A-86D9F7B2B99E", - "kind" : "audio", - "linkId" : "A50B7D21-1369-498E-A893-10D9266DA682", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1703.488228490703, - "start" : 494, - "track" : "v2" - }, - { - "duration" : 3.9661016949152668, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C57B1520-E9CD-43AB-ABB5-AA9F59B97CE5", - "kind" : "video", - "linkId" : "A50B7D21-1369-498E-A893-10D9266DA682", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1703.351621354475, - "start" : 494, - "track" : "v3" - }, - { - "duration" : 3.9661016949152668, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "72ECB107-E625-47A0-91F5-4B07C84F9D01", - "kind" : "video", - "linkId" : "A50B7D21-1369-498E-A893-10D9266DA682", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1703.351616770476, - "start" : 494, - "track" : "v4" - }, - { - "duration" : 3.322033898305051, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "592BF8E8-84BB-4D68-ACAF-C5250A378311", - "kind" : "video", - "linkId" : "2F16524C-A3E5-4EC3-B918-2C0107EB5F71", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1714.4067796610166, - "start" : 497.96610169491527, - "track" : "v0" - }, - { - "duration" : 3.322033898305051, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6A420F63-349C-490A-AEF2-A9A23B8F5911", - "kind" : "audio", - "linkId" : "2F16524C-A3E5-4EC3-B918-2C0107EB5F71", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1714.3229081196755, - "start" : 497.96610169491527, - "track" : "v1" - }, - { - "duration" : 3.322033898305051, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "9BFFE86E-C803-440C-A23A-27946EB8F38E", - "kind" : "audio", - "linkId" : "2F16524C-A3E5-4EC3-B918-2C0107EB5F71", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1714.3695844229064, - "start" : 497.96610169491527, - "track" : "v2" - }, - { - "duration" : 3.322033898305051, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "507C5A42-E790-48A6-8DE6-F867042AD251", - "kind" : "video", - "linkId" : "2F16524C-A3E5-4EC3-B918-2C0107EB5F71", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1714.2329772866783, - "start" : 497.96610169491527, - "track" : "v3" - }, - { - "duration" : 3.322033898305051, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1255A12C-C270-451F-B9E7-2C3827620218", - "kind" : "video", - "linkId" : "2F16524C-A3E5-4EC3-B918-2C0107EB5F71", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1714.2329727026795, - "start" : 497.96610169491527, - "track" : "v4" - }, - { - "duration" : 5.491525423728831, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FAF25F24-F13A-4DE0-9CF8-7D2584531C9E", - "kind" : "video", - "linkId" : "EE183339-F801-4116-A9EF-D64A77C5996D", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1722.372881355932, - "start" : 501.2881355932203, - "track" : "v0" - }, - { - "duration" : 5.491525423728831, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E764AB44-88DC-41E9-A83E-E32A15AD06C5", - "kind" : "audio", - "linkId" : "EE183339-F801-4116-A9EF-D64A77C5996D", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1722.2890098145908, - "start" : 501.2881355932203, - "track" : "v1" - }, - { - "duration" : 5.491525423728831, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A156F3B8-0CDE-4ADF-8CA0-BA16EF901769", - "kind" : "audio", - "linkId" : "EE183339-F801-4116-A9EF-D64A77C5996D", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1722.3356861178218, - "start" : 501.2881355932203, - "track" : "v2" - }, - { - "duration" : 5.491525423728831, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "60A808DE-37D0-4C45-925D-276CA1D59EB7", - "kind" : "video", - "linkId" : "EE183339-F801-4116-A9EF-D64A77C5996D", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1722.1990789815936, - "start" : 501.2881355932203, - "track" : "v3" - }, - { - "duration" : 5.491525423728831, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C8564707-EE02-4CAD-BFA6-0089C454CE2A", - "kind" : "video", - "linkId" : "EE183339-F801-4116-A9EF-D64A77C5996D", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1722.1990743975948, - "start" : 501.2881355932203, - "track" : "v4" - }, - { - "duration" : 7.627118644067764, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BB4F9A82-C969-4E01-BDBE-E481B17343CF", - "kind" : "video", - "linkId" : "DFDDB477-FE81-42F4-A11E-389F9E0A1439", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1729.050847457627, - "start" : 506.77966101694915, - "track" : "v0" - }, - { - "duration" : 7.627118644067764, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D539326D-CFC6-4F6B-A59C-770061337CBD", - "kind" : "audio", - "linkId" : "DFDDB477-FE81-42F4-A11E-389F9E0A1439", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1728.966975916286, - "start" : 506.77966101694915, - "track" : "v1" - }, - { - "duration" : 7.627118644067764, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "28F991A4-9D42-4CBC-BE96-E89FEBFAF96C", - "kind" : "audio", - "linkId" : "DFDDB477-FE81-42F4-A11E-389F9E0A1439", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1729.0136522195169, - "start" : 506.77966101694915, - "track" : "v2" - }, - { - "duration" : 7.627118644067764, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D4D4B878-3981-44A7-9435-B6B607F17D8E", - "kind" : "video", - "linkId" : "DFDDB477-FE81-42F4-A11E-389F9E0A1439", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1728.8770450832887, - "start" : 506.77966101694915, - "track" : "v3" - }, - { - "duration" : 7.627118644067764, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BFB922A9-8931-4826-A5AE-4A8F51627F00", - "kind" : "video", - "linkId" : "DFDDB477-FE81-42F4-A11E-389F9E0A1439", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1728.87704049929, - "start" : 506.77966101694915, - "track" : "v4" - }, - { - "duration" : 7.491525423728831, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "08360F23-5B06-4769-94DC-42A31CD3BD46", - "kind" : "video", - "linkId" : "2C7554F2-E73F-41CD-85CB-697100DCD6C1", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1747.593220338983, - "start" : 514.4067796610169, - "track" : "v0" - }, - { - "duration" : 7.491525423728831, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8BCF92B0-D87A-42BB-A168-F889F41CD1AB", - "kind" : "audio", - "linkId" : "2C7554F2-E73F-41CD-85CB-697100DCD6C1", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1747.509348797642, - "start" : 514.4067796610169, - "track" : "v1" - }, - { - "duration" : 7.491525423728831, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2753AF8E-8B68-4B0D-A289-CCF8ABFA4EA6", - "kind" : "audio", - "linkId" : "2C7554F2-E73F-41CD-85CB-697100DCD6C1", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1747.5560251008728, - "start" : 514.4067796610169, - "track" : "v2" - }, - { - "duration" : 7.491525423728831, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "913D7AB9-49C1-4FEE-B538-D81313C8CDAA", - "kind" : "video", - "linkId" : "2C7554F2-E73F-41CD-85CB-697100DCD6C1", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1747.4194179646447, - "start" : 514.4067796610169, - "track" : "v3" - }, - { - "duration" : 7.491525423728831, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A4078960-1488-4073-A3C0-10A87E8A55C7", - "kind" : "video", - "linkId" : "2C7554F2-E73F-41CD-85CB-697100DCD6C1", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1747.419413380646, - "start" : 514.4067796610169, - "track" : "v4" - }, - { - "duration" : 10.677966101694892, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B0A75210-7D43-4E54-906B-32BFB4856787", - "kind" : "video", - "linkId" : "68BA82C2-1D37-4341-BBD5-0BE799A2C6FA", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1755.084745762712, - "start" : 521.8983050847457, - "track" : "v0" - }, - { - "duration" : 10.677966101694892, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "581E6DD4-D543-4188-9C1B-BC9D2F1F908C", - "kind" : "audio", - "linkId" : "68BA82C2-1D37-4341-BBD5-0BE799A2C6FA", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1755.0008742213709, - "start" : 521.8983050847457, - "track" : "v1" - }, - { - "duration" : 10.677966101694892, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B8BFF2A6-3F9E-4D5E-8DFE-EDF5101E1AE5", - "kind" : "audio", - "linkId" : "68BA82C2-1D37-4341-BBD5-0BE799A2C6FA", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1755.0475505246018, - "start" : 521.8983050847457, - "track" : "v2" - }, - { - "duration" : 10.677966101694892, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6669C09D-E39D-46DE-A862-6BA3B20F558E", - "kind" : "video", - "linkId" : "68BA82C2-1D37-4341-BBD5-0BE799A2C6FA", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1754.9109433883737, - "start" : 521.8983050847457, - "track" : "v3" - }, - { - "duration" : 10.677966101694892, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C6619902-8ED4-46F4-907C-4F6DF342850B", - "kind" : "video", - "linkId" : "68BA82C2-1D37-4341-BBD5-0BE799A2C6FA", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1754.9109388043748, - "start" : 521.8983050847457, - "track" : "v4" - }, - { - "duration" : 3.152542372881385, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1E1F6B29-A2BC-44CA-BC6B-B2DA54F93FE5", - "kind" : "video", - "linkId" : "F2447BC6-DF74-439E-8FD2-0FBAC39A205C", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1766.6101694915255, - "start" : 532.5762711864406, - "track" : "v0" - }, - { - "duration" : 3.152542372881385, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "97A49AEA-492D-4D8E-A171-E4F0831755E7", - "kind" : "audio", - "linkId" : "F2447BC6-DF74-439E-8FD2-0FBAC39A205C", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1766.5262979501845, - "start" : 532.5762711864406, - "track" : "v1" - }, - { - "duration" : 3.152542372881385, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C0542DA4-D2C3-4854-B510-3701574E9CD1", - "kind" : "audio", - "linkId" : "F2447BC6-DF74-439E-8FD2-0FBAC39A205C", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1766.5729742534154, - "start" : 532.5762711864406, - "track" : "v2" - }, - { - "duration" : 3.152542372881385, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "85D95CB3-B6F4-4DFA-9F00-55E80014D2B2", - "kind" : "video", - "linkId" : "F2447BC6-DF74-439E-8FD2-0FBAC39A205C", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1766.4363671171873, - "start" : 532.5762711864406, - "track" : "v3" - }, - { - "duration" : 3.152542372881385, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3AD412AE-C961-4FE7-B65F-3266A0980B36", - "kind" : "video", - "linkId" : "F2447BC6-DF74-439E-8FD2-0FBAC39A205C", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1766.4363625331885, - "start" : 532.5762711864406, - "track" : "v4" - }, - { - "duration" : 5.491525423728831, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1F116DEE-E2FC-4C19-B84E-F62E8C7FB58E", - "kind" : "video", - "linkId" : "CC83DD13-876C-4795-A07C-8A9F20E0FCD4", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1771.898305084746, - "start" : 535.728813559322, - "track" : "v0" - }, - { - "duration" : 5.491525423728831, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "26F01770-1063-4614-838E-9C7408CB8EA7", - "kind" : "audio", - "linkId" : "CC83DD13-876C-4795-A07C-8A9F20E0FCD4", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1771.814433543405, - "start" : 535.728813559322, - "track" : "v1" - }, - { - "duration" : 5.491525423728831, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E27B6F72-E807-4B80-B045-EF94CBEAE518", - "kind" : "audio", - "linkId" : "CC83DD13-876C-4795-A07C-8A9F20E0FCD4", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1771.8611098466358, - "start" : 535.728813559322, - "track" : "v2" - }, - { - "duration" : 5.491525423728831, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "780E91E1-D956-46CE-AD94-132F9146FF16", - "kind" : "video", - "linkId" : "CC83DD13-876C-4795-A07C-8A9F20E0FCD4", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1771.7245027104077, - "start" : 535.728813559322, - "track" : "v3" - }, - { - "duration" : 5.491525423728831, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "502017FB-B15D-472A-92CD-864B01FA9D5A", - "kind" : "video", - "linkId" : "CC83DD13-876C-4795-A07C-8A9F20E0FCD4", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1771.724498126409, - "start" : 535.728813559322, - "track" : "v4" - }, - { - "duration" : 4.0677966101694665, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C08F7E72-3AD4-4B1D-8250-1512128E7940", - "kind" : "video", - "linkId" : "A2118321-9E02-4656-BCE9-0E3980E8372E", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1779.5593220338988, - "start" : 541.2203389830509, - "track" : "v0" - }, - { - "duration" : 4.0677966101694665, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B58ABDEF-6C61-45A8-95FF-6445D0C0EABC", - "kind" : "audio", - "linkId" : "A2118321-9E02-4656-BCE9-0E3980E8372E", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1779.4754504925577, - "start" : 541.2203389830509, - "track" : "v1" - }, - { - "duration" : 4.0677966101694665, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "141D7901-0677-44F5-8296-15BC113C1EBE", - "kind" : "audio", - "linkId" : "A2118321-9E02-4656-BCE9-0E3980E8372E", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1779.5221267957886, - "start" : 541.2203389830509, - "track" : "v2" - }, - { - "duration" : 4.0677966101694665, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BADB3830-CF8B-4208-A04F-439958ACADDA", - "kind" : "video", - "linkId" : "A2118321-9E02-4656-BCE9-0E3980E8372E", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1779.3855196595605, - "start" : 541.2203389830509, - "track" : "v3" - }, - { - "duration" : 4.0677966101694665, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "83BD9C59-2FC5-4F64-9963-A86B647F6F52", - "kind" : "video", - "linkId" : "A2118321-9E02-4656-BCE9-0E3980E8372E", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1779.3855150755617, - "start" : 541.2203389830509, - "track" : "v4" - }, - { - "duration" : 1.661016949152554, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "81042DC7-94FE-4359-959F-C9E29D4F9461", - "kind" : "video", - "linkId" : "CDF46AA7-2655-4344-81E6-6C7AB4F4B303", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1815.5932203389834, - "start" : 545.2881355932203, - "track" : "v0" - }, - { - "duration" : 1.661016949152554, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EB5D9E21-E9FF-430D-B90A-143FFFC161D0", - "kind" : "audio", - "linkId" : "CDF46AA7-2655-4344-81E6-6C7AB4F4B303", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1815.5093487976424, - "start" : 545.2881355932203, - "track" : "v1" - }, - { - "duration" : 1.661016949152554, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E5C8677F-212F-43D4-95D6-F86D218562F5", - "kind" : "audio", - "linkId" : "CDF46AA7-2655-4344-81E6-6C7AB4F4B303", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1815.5560251008733, - "start" : 545.2881355932203, - "track" : "v2" - }, - { - "duration" : 1.661016949152554, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "899CE36B-86FD-4463-ACF9-64EFDF419140", - "kind" : "video", - "linkId" : "CDF46AA7-2655-4344-81E6-6C7AB4F4B303", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1815.4194179646452, - "start" : 545.2881355932203, - "track" : "v3" - }, - { - "duration" : 1.661016949152554, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B0B393DE-7AAD-46D0-8783-D2E08335DC13", - "kind" : "video", - "linkId" : "CDF46AA7-2655-4344-81E6-6C7AB4F4B303", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1815.4194133806463, - "start" : 545.2881355932203, - "track" : "v4" - }, - { - "duration" : 1.1525423728813848, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "18E9C9F7-AA69-4D86-9DD9-5278392186CD", - "kind" : "video", - "linkId" : "2426D580-BCAA-49B5-BF7C-9DCE2A558605", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1817.5593220338988, - "start" : 546.9491525423729, - "track" : "v0" - }, - { - "duration" : 1.1525423728813848, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "098E9E68-8FCE-4C2E-8A24-81EFEECCD127", - "kind" : "audio", - "linkId" : "2426D580-BCAA-49B5-BF7C-9DCE2A558605", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1817.4754504925577, - "start" : 546.9491525423729, - "track" : "v1" - }, - { - "duration" : 1.1525423728813848, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3A68B520-1B13-4837-98C7-4715D6250B50", - "kind" : "audio", - "linkId" : "2426D580-BCAA-49B5-BF7C-9DCE2A558605", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1817.5221267957886, - "start" : 546.9491525423729, - "track" : "v2" - }, - { - "duration" : 1.1525423728813848, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3D51136D-FC09-46D4-A29C-C1E8B7022D04", - "kind" : "video", - "linkId" : "2426D580-BCAA-49B5-BF7C-9DCE2A558605", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1817.3855196595605, - "start" : 546.9491525423729, - "track" : "v3" - }, - { - "duration" : 1.1525423728813848, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "879B5431-78D7-4E34-98CD-811F0D7D8D2B", - "kind" : "video", - "linkId" : "2426D580-BCAA-49B5-BF7C-9DCE2A558605", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1817.3855150755617, - "start" : 546.9491525423729, - "track" : "v4" - }, - { - "duration" : 11.525423728813507, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B6AD6E92-A164-48D3-962D-8D5472AC5ED8", - "kind" : "video", - "linkId" : "5F7B4F1D-CE0A-49F1-A14C-6C7D1809E682", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1823.5593220338988, - "start" : 548.1016949152543, - "track" : "v0" - }, - { - "duration" : 11.525423728813507, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "15310F42-B66D-4DDB-B571-28824E2AA8F2", - "kind" : "audio", - "linkId" : "5F7B4F1D-CE0A-49F1-A14C-6C7D1809E682", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1823.4754504925577, - "start" : 548.1016949152543, - "track" : "v1" - }, - { - "duration" : 11.525423728813507, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "60DB4DB9-D800-414F-8313-D3C0C9CF620C", - "kind" : "audio", - "linkId" : "5F7B4F1D-CE0A-49F1-A14C-6C7D1809E682", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1823.5221267957886, - "start" : 548.1016949152543, - "track" : "v2" - }, - { - "duration" : 11.525423728813507, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "66EBCF41-7ABC-4B4F-B3D6-670B5DE5FF4B", - "kind" : "video", - "linkId" : "5F7B4F1D-CE0A-49F1-A14C-6C7D1809E682", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1823.3855196595605, - "start" : 548.1016949152543, - "track" : "v3" - }, - { - "duration" : 11.525423728813507, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "74B48312-8BD1-4CF9-967A-E7EED8D50E2A", - "kind" : "video", - "linkId" : "5F7B4F1D-CE0A-49F1-A14C-6C7D1809E682", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1823.3855150755617, - "start" : 548.1016949152543, - "track" : "v4" - }, - { - "duration" : 3.6271186440678775, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7E495919-B008-4616-BB9C-17E3BDE688A9", - "kind" : "video", - "linkId" : "9769E036-22A2-4F7C-95DD-F0F9F243A183", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1836.474576271187, - "start" : 559.6271186440678, - "track" : "v0" - }, - { - "duration" : 3.6271186440678775, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7E503A80-A896-4D03-9DC1-896D78C0F03A", - "kind" : "audio", - "linkId" : "9769E036-22A2-4F7C-95DD-F0F9F243A183", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1836.390704729846, - "start" : 559.6271186440678, - "track" : "v1" - }, - { - "duration" : 3.6271186440678775, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "37A7F54D-1185-4016-9C07-149400B154B8", - "kind" : "audio", - "linkId" : "9769E036-22A2-4F7C-95DD-F0F9F243A183", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1836.437381033077, - "start" : 559.6271186440678, - "track" : "v2" - }, - { - "duration" : 3.6271186440678775, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "12B7D82E-9520-4486-82BA-A1EFA3443CC5", - "kind" : "video", - "linkId" : "9769E036-22A2-4F7C-95DD-F0F9F243A183", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1836.3007738968488, - "start" : 559.6271186440678, - "track" : "v3" - }, - { - "duration" : 3.6271186440678775, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "467D66A4-ABC0-4A30-B666-EC09C21670C2", - "kind" : "video", - "linkId" : "9769E036-22A2-4F7C-95DD-F0F9F243A183", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1836.30076931285, - "start" : 559.6271186440678, - "track" : "v4" - }, - { - "duration" : 5.118644067796595, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F7C9CC46-173D-4CEA-B84A-F83B2D94A2CE", - "kind" : "video", - "linkId" : "B85938EF-37B5-4F22-B0B6-0C3F5749452C", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1849.1186440677973, - "start" : 563.2542372881356, - "track" : "v0" - }, - { - "duration" : 5.118644067796595, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "AA8C606C-3D23-4965-B5D1-E17B4413C421", - "kind" : "audio", - "linkId" : "B85938EF-37B5-4F22-B0B6-0C3F5749452C", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1849.0347725264562, - "start" : 563.2542372881356, - "track" : "v1" - }, - { - "duration" : 5.118644067796595, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FB4942C0-5842-419B-A4CA-242596D7CF03", - "kind" : "audio", - "linkId" : "B85938EF-37B5-4F22-B0B6-0C3F5749452C", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1849.0814488296871, - "start" : 563.2542372881356, - "track" : "v2" - }, - { - "duration" : 5.118644067796595, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8D875994-D892-44D9-A74C-9AE0403A1240", - "kind" : "video", - "linkId" : "B85938EF-37B5-4F22-B0B6-0C3F5749452C", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1848.944841693459, - "start" : 563.2542372881356, - "track" : "v3" - }, - { - "duration" : 5.118644067796595, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D7122A1E-0F1A-4EDA-BA83-5032DCB51740", - "kind" : "video", - "linkId" : "B85938EF-37B5-4F22-B0B6-0C3F5749452C", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1848.9448371094602, - "start" : 563.2542372881356, - "track" : "v4" - }, - { - "duration" : 8.2033898305084, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "54B76DE8-2057-470C-97CB-916B91B32B1C", - "kind" : "video", - "linkId" : "65CA1374-003F-413C-9F77-8A22B7EECDC3", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1854.9830508474583, - "start" : 568.3728813559322, - "track" : "v0" - }, - { - "duration" : 8.2033898305084, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "12851101-B365-4F9E-BFB3-FDEABAE1453A", - "kind" : "audio", - "linkId" : "65CA1374-003F-413C-9F77-8A22B7EECDC3", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1854.8991793061173, - "start" : 568.3728813559322, - "track" : "v1" - }, - { - "duration" : 8.2033898305084, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "38DCBA3E-C18E-447C-B319-9E3A3DE4DC21", - "kind" : "audio", - "linkId" : "65CA1374-003F-413C-9F77-8A22B7EECDC3", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1854.9458556093482, - "start" : 568.3728813559322, - "track" : "v2" - }, - { - "duration" : 8.2033898305084, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FE05A76D-4FB5-43E6-9960-DE92DC23FD6C", - "kind" : "video", - "linkId" : "65CA1374-003F-413C-9F77-8A22B7EECDC3", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1854.80924847312, - "start" : 568.3728813559322, - "track" : "v3" - }, - { - "duration" : 8.2033898305084, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "35C17BC8-29A1-44B4-A655-A1AF25C6D713", - "kind" : "video", - "linkId" : "65CA1374-003F-413C-9F77-8A22B7EECDC3", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1854.8092438891213, - "start" : 568.3728813559322, - "track" : "v4" - }, - { - "duration" : 5.559322033898297, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "ED705251-681D-4EEE-81C8-D077FBB072B5", - "kind" : "video", - "linkId" : "FD6E0C12-08F2-474C-B889-10F178952804", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1865.525423728814, - "start" : 576.5762711864406, - "track" : "v0" - }, - { - "duration" : 5.559322033898297, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A57F8B52-605A-4BB2-8B24-0CBC8B78EBAB", - "kind" : "audio", - "linkId" : "FD6E0C12-08F2-474C-B889-10F178952804", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1865.441552187473, - "start" : 576.5762711864406, - "track" : "v1" - }, - { - "duration" : 5.559322033898297, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "93D09BC6-1947-40D5-82BA-E6E1035452E2", - "kind" : "audio", - "linkId" : "FD6E0C12-08F2-474C-B889-10F178952804", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1865.488228490704, - "start" : 576.5762711864406, - "track" : "v2" - }, - { - "duration" : 5.559322033898297, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "48D771B8-EC5B-45CD-842D-CFC4F85FC9D3", - "kind" : "video", - "linkId" : "FD6E0C12-08F2-474C-B889-10F178952804", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1865.3516213544758, - "start" : 576.5762711864406, - "track" : "v3" - }, - { - "duration" : 5.559322033898297, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6F0B9EDA-F628-4403-9AC9-D23443721AA7", - "kind" : "video", - "linkId" : "FD6E0C12-08F2-474C-B889-10F178952804", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1865.351616770477, - "start" : 576.5762711864406, - "track" : "v4" - }, - { - "duration" : 18.881355932203405, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B0DF95C7-AB53-4E4B-B365-DA47C88E233F", - "kind" : "video", - "linkId" : "17C6BBA2-9064-425D-8592-73511CF2CF98", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1938.101694915255, - "start" : 582.1355932203389, - "track" : "v0" - }, - { - "duration" : 18.881355932203405, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "10C2DDB7-13BF-4707-A836-146E1C1DF546", - "kind" : "audio", - "linkId" : "17C6BBA2-9064-425D-8592-73511CF2CF98", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1938.0178233739139, - "start" : 582.1355932203389, - "track" : "v1" - }, - { - "duration" : 18.881355932203405, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D426D540-67E5-4B2C-8355-17C9D9356D6E", - "kind" : "audio", - "linkId" : "17C6BBA2-9064-425D-8592-73511CF2CF98", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1938.0644996771448, - "start" : 582.1355932203389, - "track" : "v2" - }, - { - "duration" : 18.881355932203405, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4270D3EC-C6D9-44CD-BAEF-E087D96DEE47", - "kind" : "video", - "linkId" : "17C6BBA2-9064-425D-8592-73511CF2CF98", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1937.9278925409167, - "start" : 582.1355932203389, - "track" : "v3" - }, - { - "duration" : 18.881355932203405, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4D883F8E-FE22-4FD7-80D8-05BB6DF23E69", - "kind" : "video", - "linkId" : "17C6BBA2-9064-425D-8592-73511CF2CF98", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1937.9278879569179, - "start" : 582.1355932203389, - "track" : "v4" - }, - { - "duration" : 6.237288135593303, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "73E5F742-AA2F-4AD8-89B1-D1CD80D854E7", - "kind" : "video", - "linkId" : "338C9A6C-870B-46F8-8240-2067335F8F8A", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1956.9830508474583, - "start" : 601.0169491525423, - "track" : "v0" - }, - { - "duration" : 6.237288135593303, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "01D1EA2C-B8BB-4B95-8C2F-716EE57867DE", - "kind" : "audio", - "linkId" : "338C9A6C-870B-46F8-8240-2067335F8F8A", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 1956.8991793061173, - "start" : 601.0169491525423, - "track" : "v1" - }, - { - "duration" : 6.237288135593303, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "588AAA62-D950-4167-88E1-3A7E9016735E", - "kind" : "audio", - "linkId" : "338C9A6C-870B-46F8-8240-2067335F8F8A", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1956.9458556093482, - "start" : 601.0169491525423, - "track" : "v2" - }, - { - "duration" : 6.237288135593303, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5F41A84E-E3E3-417E-B7E8-82995513D4E2", - "kind" : "video", - "linkId" : "338C9A6C-870B-46F8-8240-2067335F8F8A", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1956.80924847312, - "start" : 601.0169491525423, - "track" : "v3" - }, - { - "duration" : 6.237288135593303, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "06DE79D5-6984-41FF-8120-821047F987C5", - "kind" : "video", - "linkId" : "338C9A6C-870B-46F8-8240-2067335F8F8A", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 1956.8092438891213, - "start" : 601.0169491525423, - "track" : "v4" - }, - { - "duration" : 10.677966101694892, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F7574C8D-473A-44C7-BB33-CA9C31856531", - "kind" : "video", - "linkId" : "5715D6B2-69B7-4806-A021-82A001C82B8D", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2022.8474576271192, - "start" : 607.2542372881356, - "track" : "v0" - }, - { - "duration" : 10.677966101694892, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6F7C3CE2-9238-415C-A314-90B677ECAE17", - "kind" : "audio", - "linkId" : "5715D6B2-69B7-4806-A021-82A001C82B8D", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2022.7635860857781, - "start" : 607.2542372881356, - "track" : "v1" - }, - { - "duration" : 10.677966101694892, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BC1D7F8D-872D-4749-97D4-9CBF947EB008", - "kind" : "audio", - "linkId" : "5715D6B2-69B7-4806-A021-82A001C82B8D", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2022.810262389009, - "start" : 607.2542372881356, - "track" : "v2" - }, - { - "duration" : 10.677966101694892, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5AC16FB7-ABB0-462A-926D-6657645660DB", - "kind" : "video", - "linkId" : "5715D6B2-69B7-4806-A021-82A001C82B8D", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2022.673655252781, - "start" : 607.2542372881356, - "track" : "v3" - }, - { - "duration" : 10.677966101694892, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7EC1D808-B244-481D-AEED-29FBAD17A539", - "kind" : "video", - "linkId" : "5715D6B2-69B7-4806-A021-82A001C82B8D", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2022.673650668782, - "start" : 607.2542372881356, - "track" : "v4" - }, - { - "duration" : 3.627118644067764, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "60AAA140-4B79-4568-92FB-E8B2070677B4", - "kind" : "video", - "linkId" : "7A62ADBD-0427-472A-880B-89AAE3A2A8F4", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2045.2881355932209, - "start" : 617.9322033898305, - "track" : "v0" - }, - { - "duration" : 3.627118644067764, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "AA1ADCAE-DA36-4FF1-A7CC-6C3B75A693B7", - "kind" : "audio", - "linkId" : "7A62ADBD-0427-472A-880B-89AAE3A2A8F4", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2045.2042640518798, - "start" : 617.9322033898305, - "track" : "v1" - }, - { - "duration" : 3.627118644067764, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C3588EA8-F756-402E-B6EC-A9C9772EB504", - "kind" : "audio", - "linkId" : "7A62ADBD-0427-472A-880B-89AAE3A2A8F4", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2045.2509403551107, - "start" : 617.9322033898305, - "track" : "v2" - }, - { - "duration" : 3.627118644067764, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8A86F818-17D6-4DA6-806A-BD6E61EDD16A", - "kind" : "video", - "linkId" : "7A62ADBD-0427-472A-880B-89AAE3A2A8F4", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2045.1143332188826, - "start" : 617.9322033898305, - "track" : "v3" - }, - { - "duration" : 3.627118644067764, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "DF8175BF-3E2D-4B78-8761-2798CBEEB331", - "kind" : "video", - "linkId" : "7A62ADBD-0427-472A-880B-89AAE3A2A8F4", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2045.1143286348838, - "start" : 617.9322033898305, - "track" : "v4" - }, - { - "duration" : 2.4067796610169125, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0079348D-917B-4B45-A779-BE7BDA967808", - "kind" : "video", - "linkId" : "B5A3F4FE-CCAA-45AE-A6FB-0579F3BF1536", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2050.542372881356, - "start" : 621.5593220338983, - "track" : "v0" - }, - { - "duration" : 2.4067796610169125, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2D8FE96C-46AE-4C66-B4A5-30D8857C61FB", - "kind" : "audio", - "linkId" : "B5A3F4FE-CCAA-45AE-A6FB-0579F3BF1536", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2050.458501340015, - "start" : 621.5593220338983, - "track" : "v1" - }, - { - "duration" : 2.4067796610169125, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B675F446-CEBF-4F4A-B344-F62184FADF3B", - "kind" : "audio", - "linkId" : "B5A3F4FE-CCAA-45AE-A6FB-0579F3BF1536", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2050.505177643246, - "start" : 621.5593220338983, - "track" : "v2" - }, - { - "duration" : 2.4067796610169125, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C21A6286-77CE-4A32-B617-2710BCE8CBD9", - "kind" : "video", - "linkId" : "B5A3F4FE-CCAA-45AE-A6FB-0579F3BF1536", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2050.368570507018, - "start" : 621.5593220338983, - "track" : "v3" - }, - { - "duration" : 2.4067796610169125, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "64279978-B9D7-4E8D-AFED-D7913E1B9532", - "kind" : "video", - "linkId" : "B5A3F4FE-CCAA-45AE-A6FB-0579F3BF1536", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2050.368565923019, - "start" : 621.5593220338983, - "track" : "v4" - }, - { - "duration" : 3.4237288135593644, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A040FD6A-EC96-415F-8D28-7B4691F99BF3", - "kind" : "video", - "linkId" : "0F6F0554-6C53-4419-B9B3-D9C2EE7D2267", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2052.949152542373, - "start" : 623.9661016949152, - "track" : "v0" - }, - { - "duration" : 3.4237288135593644, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6AC76567-F75D-4A33-B204-A10F431BBFDD", - "kind" : "audio", - "linkId" : "0F6F0554-6C53-4419-B9B3-D9C2EE7D2267", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2052.865281001032, - "start" : 623.9661016949152, - "track" : "v1" - }, - { - "duration" : 3.4237288135593644, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FA9C2797-0EB0-4B75-A617-5D4CF590AC43", - "kind" : "audio", - "linkId" : "0F6F0554-6C53-4419-B9B3-D9C2EE7D2267", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2052.911957304263, - "start" : 623.9661016949152, - "track" : "v2" - }, - { - "duration" : 3.4237288135593644, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "429309C1-5820-4F55-97F5-8FBD04302BA5", - "kind" : "video", - "linkId" : "0F6F0554-6C53-4419-B9B3-D9C2EE7D2267", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2052.775350168035, - "start" : 623.9661016949152, - "track" : "v3" - }, - { - "duration" : 3.4237288135593644, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "89C2D014-50A7-43ED-AE56-0AA708FA28ED", - "kind" : "video", - "linkId" : "0F6F0554-6C53-4419-B9B3-D9C2EE7D2267", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2052.775345584036, - "start" : 623.9661016949152, - "track" : "v4" - }, - { - "duration" : 5.69491525423723, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "01E331DE-F3BA-42B2-B95C-4FA7802600C4", - "kind" : "video", - "linkId" : "681599CE-614B-4D54-9956-0A2F59A0C020", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2114.6779661016953, - "start" : 627.3898305084746, - "track" : "v0" - }, - { - "duration" : 5.69491525423723, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "602A3DDB-AB74-45B5-8FAE-A660E6CA2700", - "kind" : "audio", - "linkId" : "681599CE-614B-4D54-9956-0A2F59A0C020", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2114.5940945603543, - "start" : 627.3898305084746, - "track" : "v1" - }, - { - "duration" : 5.69491525423723, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0BA636D9-34A5-47E7-9BC5-0BDD5579680E", - "kind" : "audio", - "linkId" : "681599CE-614B-4D54-9956-0A2F59A0C020", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2114.640770863585, - "start" : 627.3898305084746, - "track" : "v2" - }, - { - "duration" : 5.69491525423723, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4E01905E-2230-4C63-9855-875F6C89C92B", - "kind" : "video", - "linkId" : "681599CE-614B-4D54-9956-0A2F59A0C020", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2114.504163727357, - "start" : 627.3898305084746, - "track" : "v3" - }, - { - "duration" : 5.69491525423723, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "78183D76-36AF-4F77-8481-6336BB1F1340", - "kind" : "video", - "linkId" : "681599CE-614B-4D54-9956-0A2F59A0C020", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2114.5041591433583, - "start" : 627.3898305084746, - "track" : "v4" - }, - { - "duration" : 15.76271186440681, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "581AB06A-CA87-43B3-9083-3BF4CA1B488F", - "kind" : "video", - "linkId" : "54C20EBF-C3C2-46DA-929B-9C51CBA0B7D6", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2230.1694915254243, - "start" : 633.0847457627118, - "track" : "v0" - }, - { - "duration" : 15.76271186440681, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "39BA14C0-0C38-4545-B0B0-C33EB72363EF", - "kind" : "audio", - "linkId" : "54C20EBF-C3C2-46DA-929B-9C51CBA0B7D6", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2230.085619984083, - "start" : 633.0847457627118, - "track" : "v1" - }, - { - "duration" : 15.76271186440681, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2161011F-CC8E-4C61-8661-7A947C751069", - "kind" : "audio", - "linkId" : "54C20EBF-C3C2-46DA-929B-9C51CBA0B7D6", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2230.132296287314, - "start" : 633.0847457627118, - "track" : "v2" - }, - { - "duration" : 15.76271186440681, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7265B315-57C1-47FB-A27C-3AB7B8CA1004", - "kind" : "video", - "linkId" : "54C20EBF-C3C2-46DA-929B-9C51CBA0B7D6", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2229.995689151086, - "start" : 633.0847457627118, - "track" : "v3" - }, - { - "duration" : 15.76271186440681, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EFC80E51-0F05-4B16-8596-E78F73CB045F", - "kind" : "video", - "linkId" : "54C20EBF-C3C2-46DA-929B-9C51CBA0B7D6", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2229.995684567087, - "start" : 633.0847457627118, - "track" : "v4" - }, - { - "duration" : 3.3898305084745743, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8BD87ADB-1AA9-4B14-9370-4DF20E448896", - "kind" : "video", - "linkId" : "C2740056-FAEB-4E91-9E00-112253797C46", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2256.372881355933, - "start" : 648.8474576271186, - "track" : "v0" - }, - { - "duration" : 3.3898305084745743, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7A48AFF3-E08F-437C-AC57-92133246569E", - "kind" : "audio", - "linkId" : "C2740056-FAEB-4E91-9E00-112253797C46", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2256.2890098145917, - "start" : 648.8474576271186, - "track" : "v1" - }, - { - "duration" : 3.3898305084745743, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E427E26F-1FDB-4742-A1E4-015E32255D38", - "kind" : "audio", - "linkId" : "C2740056-FAEB-4E91-9E00-112253797C46", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2256.3356861178227, - "start" : 648.8474576271186, - "track" : "v2" - }, - { - "duration" : 3.3898305084745743, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "107EE1C9-66CC-4E8E-9743-C6900E649621", - "kind" : "video", - "linkId" : "C2740056-FAEB-4E91-9E00-112253797C46", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2256.1990789815945, - "start" : 648.8474576271186, - "track" : "v3" - }, - { - "duration" : 3.3898305084745743, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D7AAC444-EA58-4486-9341-92E3C325D17F", - "kind" : "video", - "linkId" : "C2740056-FAEB-4E91-9E00-112253797C46", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2256.1990743975957, - "start" : 648.8474576271186, - "track" : "v4" - }, - { - "duration" : 15.423728813559364, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4754E275-63E9-4C43-8DBD-3771650BDEB8", - "kind" : "video", - "linkId" : "FF0D729E-3013-4F28-A488-DBD70296587F", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2280.9491525423737, - "start" : 652.2372881355932, - "track" : "v0" - }, - { - "duration" : 15.423728813559364, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A8BCA6E8-0804-4D39-8229-30DF2DFBF320", - "kind" : "audio", - "linkId" : "FF0D729E-3013-4F28-A488-DBD70296587F", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2280.8652810010326, - "start" : 652.2372881355932, - "track" : "v1" - }, - { - "duration" : 15.423728813559364, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2A8D5833-B3BC-4A82-A7E7-6319DFF51DC3", - "kind" : "audio", - "linkId" : "FF0D729E-3013-4F28-A488-DBD70296587F", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2280.9119573042635, - "start" : 652.2372881355932, - "track" : "v2" - }, - { - "duration" : 15.423728813559364, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B8963AE1-23D3-4C35-B241-00A5396F4DD7", - "kind" : "video", - "linkId" : "FF0D729E-3013-4F28-A488-DBD70296587F", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2280.7753501680354, - "start" : 652.2372881355932, - "track" : "v3" - }, - { - "duration" : 15.423728813559364, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1FC80A57-7A45-4C7D-9200-0C1A48A3D6F5", - "kind" : "video", - "linkId" : "FF0D729E-3013-4F28-A488-DBD70296587F", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2280.7753455840366, - "start" : 652.2372881355932, - "track" : "v4" - }, - { - "duration" : 12.745762711864359, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0CA818E8-62B3-474A-8050-49368421A90E", - "kind" : "video", - "linkId" : "C5B9230A-8789-4FD4-B107-6702F012D99A", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2296.372881355933, - "start" : 667.6610169491526, - "track" : "v0" - }, - { - "duration" : 12.745762711864359, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EB3722EC-6C38-40B2-B519-694180267E3A", - "kind" : "audio", - "linkId" : "C5B9230A-8789-4FD4-B107-6702F012D99A", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2296.2890098145917, - "start" : 667.6610169491526, - "track" : "v1" - }, - { - "duration" : 12.745762711864359, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A797E488-873A-4FC3-816A-D61A73E2F1D9", - "kind" : "audio", - "linkId" : "C5B9230A-8789-4FD4-B107-6702F012D99A", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2296.3356861178227, - "start" : 667.6610169491526, - "track" : "v2" - }, - { - "duration" : 12.745762711864359, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "75FC92A1-B149-4AB0-A366-35F476711F59", - "kind" : "video", - "linkId" : "C5B9230A-8789-4FD4-B107-6702F012D99A", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2296.1990789815945, - "start" : 667.6610169491526, - "track" : "v3" - }, - { - "duration" : 12.745762711864359, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5B3388A0-5AC5-481F-8DD8-BED12840C1D9", - "kind" : "video", - "linkId" : "C5B9230A-8789-4FD4-B107-6702F012D99A", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2296.1990743975957, - "start" : 667.6610169491526, - "track" : "v4" - }, - { - "duration" : 4.677966101695006, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5A8649A2-00AB-447F-BC8C-3085E8695184", - "kind" : "video", - "linkId" : "2782DABC-F157-44FA-B15C-F2D70A28D22C", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2313.254237288136, - "start" : 680.4067796610169, - "track" : "v0" - }, - { - "duration" : 4.677966101695006, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EAC07860-D644-40FF-9A4D-08BD56F332C4", - "kind" : "audio", - "linkId" : "2782DABC-F157-44FA-B15C-F2D70A28D22C", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2313.170365746795, - "start" : 680.4067796610169, - "track" : "v1" - }, - { - "duration" : 4.677966101695006, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0950EA6E-C41F-453A-9C62-F24BC14CD76E", - "kind" : "audio", - "linkId" : "2782DABC-F157-44FA-B15C-F2D70A28D22C", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2313.217042050026, - "start" : 680.4067796610169, - "track" : "v2" - }, - { - "duration" : 4.677966101695006, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C0EBCCE6-06A8-4958-8643-35B908EA7C98", - "kind" : "video", - "linkId" : "2782DABC-F157-44FA-B15C-F2D70A28D22C", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2313.080434913798, - "start" : 680.4067796610169, - "track" : "v3" - }, - { - "duration" : 4.677966101695006, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7D753BA0-2851-4A2B-B1AB-52234E029F7C", - "kind" : "video", - "linkId" : "2782DABC-F157-44FA-B15C-F2D70A28D22C", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2313.080430329799, - "start" : 680.4067796610169, - "track" : "v4" - }, - { - "duration" : 19.186440677966175, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B4AB14C4-4491-499A-8505-0934B993F07B", - "kind" : "video", - "linkId" : "028E7FFC-FA84-47CF-8683-266C5E78C890", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2414.6440677966107, - "start" : 685.0847457627119, - "track" : "v0" - }, - { - "duration" : 19.186440677966175, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "36653084-A594-47D9-82C7-05282A42CE6C", - "kind" : "audio", - "linkId" : "028E7FFC-FA84-47CF-8683-266C5E78C890", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2414.5601962552696, - "start" : 685.0847457627119, - "track" : "v1" - }, - { - "duration" : 19.186440677966175, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5A23146F-8246-498B-AC51-C4FE4B6C4DDA", - "kind" : "audio", - "linkId" : "028E7FFC-FA84-47CF-8683-266C5E78C890", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2414.6068725585005, - "start" : 685.0847457627119, - "track" : "v2" - }, - { - "duration" : 19.186440677966175, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C7D8CAF3-5789-4F76-924A-24654359FD7F", - "kind" : "video", - "linkId" : "028E7FFC-FA84-47CF-8683-266C5E78C890", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2414.4702654222724, - "start" : 685.0847457627119, - "track" : "v3" - }, - { - "duration" : 19.186440677966175, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "DFB2B3EE-B824-4672-A351-269C20BF2ADE", - "kind" : "video", - "linkId" : "028E7FFC-FA84-47CF-8683-266C5E78C890", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2414.4702608382736, - "start" : 685.0847457627119, - "track" : "v4" - }, - { - "duration" : 5.694915254237344, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EB2F9D36-DD5F-468C-9DA8-90D56D42E0A9", - "kind" : "video", - "linkId" : "DDA38179-F497-4A55-8BF0-09672B42936C", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2446.1355932203396, - "start" : 704.2711864406781, - "track" : "v0" - }, - { - "duration" : 5.694915254237344, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D07C45CE-44A0-4FEF-8F34-54E4FD3F7D39", - "kind" : "audio", - "linkId" : "DDA38179-F497-4A55-8BF0-09672B42936C", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2446.0517216789985, - "start" : 704.2711864406781, - "track" : "v1" - }, - { - "duration" : 5.694915254237344, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A22E3A44-8F76-4713-9D7F-7948A74D9CCC", - "kind" : "audio", - "linkId" : "DDA38179-F497-4A55-8BF0-09672B42936C", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2446.0983979822295, - "start" : 704.2711864406781, - "track" : "v2" - }, - { - "duration" : 5.694915254237344, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "15C53598-6F8F-4C89-B6EE-8BEAEA74F23A", - "kind" : "video", - "linkId" : "DDA38179-F497-4A55-8BF0-09672B42936C", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2445.9617908460013, - "start" : 704.2711864406781, - "track" : "v3" - }, - { - "duration" : 5.694915254237344, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FB595553-A66D-4B22-8173-3AAE10CB58D0", - "kind" : "video", - "linkId" : "DDA38179-F497-4A55-8BF0-09672B42936C", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2445.9617862620025, - "start" : 704.2711864406781, - "track" : "v4" - }, - { - "duration" : 9.59322033898286, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "815EF8E2-F1E8-4B38-829B-D2C6971FDCD7", - "kind" : "video", - "linkId" : "1D15D7CC-5494-459C-BAF8-DD757B81547D", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2454.1355932203396, - "start" : 709.9661016949154, - "track" : "v0" - }, - { - "duration" : 9.59322033898286, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6599B7E9-8BC7-4339-8544-580C7E40A6B3", - "kind" : "audio", - "linkId" : "1D15D7CC-5494-459C-BAF8-DD757B81547D", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2454.0517216789985, - "start" : 709.9661016949154, - "track" : "v1" - }, - { - "duration" : 9.59322033898286, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "913AD485-580A-4CF5-832E-3A866323A594", - "kind" : "audio", - "linkId" : "1D15D7CC-5494-459C-BAF8-DD757B81547D", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2454.0983979822295, - "start" : 709.9661016949154, - "track" : "v2" - }, - { - "duration" : 9.59322033898286, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3F04DEF9-CD7C-486B-913A-989ECE77DE57", - "kind" : "video", - "linkId" : "1D15D7CC-5494-459C-BAF8-DD757B81547D", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2453.9617908460013, - "start" : 709.9661016949154, - "track" : "v3" - }, - { - "duration" : 9.59322033898286, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "715FA793-E1B3-4C26-B441-31D597B1938B", - "kind" : "video", - "linkId" : "1D15D7CC-5494-459C-BAF8-DD757B81547D", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2453.9617862620025, - "start" : 709.9661016949154, - "track" : "v4" - }, - { - "duration" : 14.474576271186379, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "40CCDD9D-5568-40CA-AEF5-90D94B8580BB", - "kind" : "video", - "linkId" : "F3271449-9FF2-4A35-8582-579E52CA2EFC", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2526.6779661016953, - "start" : 723.7627118644068, - "track" : "v0" - }, - { - "duration" : 14.474576271186379, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C9DD81B8-F688-49A6-8638-2A626AF597C9", - "kind" : "audio", - "linkId" : "F3271449-9FF2-4A35-8582-579E52CA2EFC", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2526.5940945603543, - "start" : 723.7627118644068, - "track" : "v1" - }, - { - "duration" : 14.474576271186379, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BC7A036D-1FA0-4738-A7F4-AD8EC3488B82", - "kind" : "audio", - "linkId" : "F3271449-9FF2-4A35-8582-579E52CA2EFC", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2526.640770863585, - "start" : 723.7627118644068, - "track" : "v2" - }, - { - "duration" : 14.474576271186379, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A34F945B-0445-4497-9DD0-1E68E72ADA27", - "kind" : "video", - "linkId" : "F3271449-9FF2-4A35-8582-579E52CA2EFC", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2526.504163727357, - "start" : 723.7627118644068, - "track" : "v3" - }, - { - "duration" : 14.474576271186379, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E2ECA174-0D6E-4E40-961D-0804BCB7E988", - "kind" : "video", - "linkId" : "F3271449-9FF2-4A35-8582-579E52CA2EFC", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2526.5041591433583, - "start" : 723.7627118644068, - "track" : "v4" - }, - { - "duration" : 12.372881355932236, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "793A00B4-D559-495E-BF6E-23E5FF440C18", - "kind" : "video", - "linkId" : "57734964-46B6-4D2E-8D3E-63DAA6A4B487", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2542.203389830509, - "start" : 738.2372881355932, - "track" : "v0" - }, - { - "duration" : 12.372881355932236, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C4AC4AF2-ACA7-4B5B-B85A-13CC8E338B0C", - "kind" : "audio", - "linkId" : "57734964-46B6-4D2E-8D3E-63DAA6A4B487", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2542.119518289168, - "start" : 738.2372881355932, - "track" : "v1" - }, - { - "duration" : 12.372881355932236, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0A904774-EBBE-42E8-BC32-42DDB4E21185", - "kind" : "audio", - "linkId" : "57734964-46B6-4D2E-8D3E-63DAA6A4B487", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2542.166194592399, - "start" : 738.2372881355932, - "track" : "v2" - }, - { - "duration" : 12.372881355932236, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8CA5857B-2D5B-4E8A-90EB-7BE589C6785D", - "kind" : "video", - "linkId" : "57734964-46B6-4D2E-8D3E-63DAA6A4B487", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2542.0295874561707, - "start" : 738.2372881355932, - "track" : "v3" - }, - { - "duration" : 12.372881355932236, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A1354766-A4E2-423C-A148-D457C645DEED", - "kind" : "video", - "linkId" : "57734964-46B6-4D2E-8D3E-63DAA6A4B487", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2542.029582872172, - "start" : 738.2372881355932, - "track" : "v4" - }, - { - "duration" : 15112.237288, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1EDE863A-2897-4FFD-A49E-B943B438866F", - "kind" : "video", - "linkId" : "FD2B233D-ADAA-43D0-86FE-BD93A20C9129", - "mediaId" : "CC300C55-7090-4A6E-81FA-55CB6644FAC9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0, - "start" : 4193.627118644067, - "track" : "v0" - }, - { - "duration" : 15112.106667, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A3D2A572-1789-4532-9D15-1312C8312A41", - "kind" : "audio", - "linkId" : "FD2B233D-ADAA-43D0-86FE-BD93A20C9129", - "mediaId" : "2613683E-12E6-402E-BF4D-6D9ACD7C1491", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 0, - "start" : 4193.710543352058, - "track" : "v1" - }, - { - "duration" : 15112.213333, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "387BD48F-5D42-4795-A2FE-DA7340E094B2", - "kind" : "audio", - "linkId" : "FD2B233D-ADAA-43D0-86FE-BD93A20C9129", - "mediaId" : "7889E9EC-F67A-491B-81C0-0CF8338965A3", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0, - "start" : 4193.660488258577, - "track" : "v2" - }, - { - "duration" : 15112.066667, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "15742468-AF06-408A-B0E3-DE8D543186D0", - "kind" : "video", - "linkId" : "FD2B233D-ADAA-43D0-86FE-BD93A20C9129", - "mediaId" : "BC4E74C5-4C45-4633-B31A-3CAA58D4B2BF", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0, - "start" : 4193.796015394061, - "track" : "v3" - }, - { - "duration" : 15111.366667, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "14D49BF0-68EB-4A4B-A627-9A55600AD1C2", - "kind" : "video", - "linkId" : "FD2B233D-ADAA-43D0-86FE-BD93A20C9129", - "mediaId" : "98882264-A2ED-477B-A7B8-B63E54EDEACE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0, - "start" : 4193.796020644072, - "track" : "v4" - }, - { - "duration" : 10045.254237, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "82C83010-6BFE-4B26-81BC-CB291FAEB910", - "kind" : "video", - "linkId" : "F0D0664C-0131-446A-9354-CEB5E8E228DE", - "mediaId" : "2D5EED57-87EB-44B4-9623-2CF48314C5F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0, - "start" : 19305.864406779656, - "track" : "v0" - }, - { - "duration" : 10045.162667, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "83C1AFB5-E771-4D3A-8092-30A98A34B026", - "kind" : "audio", - "linkId" : "F0D0664C-0131-446A-9354-CEB5E8E228DE", - "mediaId" : "0ECB1F4F-9065-432F-B0B1-7F47BD31FDCC", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0, - "start" : 19305.9261039463, - "track" : "v1" - }, - { - "duration" : 10045.248, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BD5B3686-815E-483F-8CDF-DC040625A81F", - "kind" : "audio", - "linkId" : "F0D0664C-0131-446A-9354-CEB5E8E228DE", - "mediaId" : "02A5DFB3-9348-4102-BD13-F1856B431A70", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0, - "start" : 19305.87683716514, - "track" : "v2" - }, - { - "duration" : 10045.133333, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7C227E93-B4B2-44BB-81C4-0621CFA85F4E", - "kind" : "video", - "linkId" : "F0D0664C-0131-446A-9354-CEB5E8E228DE", - "mediaId" : "9CA51332-5865-4C3D-9972-E781BDE75ECD", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0, - "start" : 19306.0080157373, - "track" : "v3" - }, - { - "duration" : 10045.166667, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "472EB6D0-EF6A-469B-83F3-955C5C672361", - "kind" : "video", - "linkId" : "F0D0664C-0131-446A-9354-CEB5E8E228DE", - "mediaId" : "0DEB0D7E-0C1E-49DA-9911-7773FB2158DE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0, - "start" : 19306.00801794631, - "track" : "v4" - }, - { - "duration" : 11597.966102, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "CE776B50-2A67-4285-9707-01DA14BBB04E", - "kind" : "video", - "linkId" : "F27C213D-566E-46A3-AF60-6D581EE67217", - "mediaId" : "F06DF483-F96F-4DD1-8602-53609FA888F1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0, - "start" : 29351.18644067796, - "track" : "v0" - }, - { - "duration" : 11598.101333, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7DB3CB28-8BB6-48ED-9B76-4FC6A5B2A106", - "kind" : "audio", - "linkId" : "F27C213D-566E-46A3-AF60-6D581EE67217", - "mediaId" : "59213540-870A-4795-BD44-B3AFBC9F14E3", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0, - "start" : 29351.035369510933, - "track" : "v1" - }, - { - "duration" : 11598.464, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7941E3BC-CC88-41DB-A7BB-7C7519B8B9D6", - "kind" : "audio", - "linkId" : "F27C213D-566E-46A3-AF60-6D581EE67217", - "mediaId" : "F4DB0AF3-2C74-4501-A58A-A519C046E3D6", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0, - "start" : 29350.707797367297, - "track" : "v2" - }, - { - "duration" : 11598.033333, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C6613E59-9E23-4934-B5DC-E224214F74D9", - "kind" : "video", - "linkId" : "F27C213D-566E-46A3-AF60-6D581EE67217", - "mediaId" : "9F95D76B-EAB2-49AA-897A-C3C9A265E0CC", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0, - "start" : 29351.122484093936, - "track" : "v3" - }, - { - "duration" : 11598, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "628E46F4-79BD-44D7-A964-903FBD21DD0F", - "kind" : "video", - "linkId" : "F27C213D-566E-46A3-AF60-6D581EE67217", - "mediaId" : "CC497E03-E3E7-4283-A96C-F076D9F154A1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 0, - "start" : 29351.12248226092, - "track" : "v4" - }, - { - "duration" : 4.203389830508513, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5D2DD96A-1D28-4583-A85D-DDD1286F3EC1", - "kind" : "video", - "linkId" : "B7F43758-3B67-4E4D-8D71-152570BCA4AF", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2491.0847457627124, - "start" : 719.5593220338983, - "track" : "v0" - }, - { - "duration" : 4.203389830508513, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B7181073-3D61-47D2-A83E-A7C19C246D68", - "kind" : "audio", - "linkId" : "B7F43758-3B67-4E4D-8D71-152570BCA4AF", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2491.0008742213713, - "start" : 719.5593220338983, - "track" : "v1" - }, - { - "duration" : 4.203389830508513, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "DF6474FD-D698-4232-9E94-EBE39BBB8545", - "kind" : "audio", - "linkId" : "B7F43758-3B67-4E4D-8D71-152570BCA4AF", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2491.0475505246022, - "start" : 719.5593220338983, - "track" : "v2" - }, - { - "duration" : 4.203389830508513, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D06A7AA2-0BC5-4031-9EDA-7A81673249DF", - "kind" : "video", - "linkId" : "B7F43758-3B67-4E4D-8D71-152570BCA4AF", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2490.910943388374, - "start" : 719.5593220338983, - "track" : "v3" - }, - { - "duration" : 4.203389830508513, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "39DCC5F4-B663-454C-BD55-6E675B931DBA", - "kind" : "video", - "linkId" : "B7F43758-3B67-4E4D-8D71-152570BCA4AF", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2490.9109388043753, - "start" : 719.5593220338983, - "track" : "v4" - }, - { - "duration" : 15.152542372881271, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F4373D19-EA06-4C7F-A620-FC8FD1BD6AE3", - "kind" : "video", - "linkId" : "DF0D7986-17C5-4CB7-B683-DD878723E8C7", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2557.3898305084754, - "start" : 750.6101694915254, - "track" : "v0" - }, - { - "duration" : 15.152542372881271, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "84DE87BB-60F8-41DF-933A-3B41704FE3D2", - "kind" : "audio", - "linkId" : "DF0D7986-17C5-4CB7-B683-DD878723E8C7", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2557.3059589671343, - "start" : 750.6101694915254, - "track" : "v1" - }, - { - "duration" : 15.152542372881271, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F6A29700-D4EE-4106-AA51-21C13AD34CA4", - "kind" : "audio", - "linkId" : "DF0D7986-17C5-4CB7-B683-DD878723E8C7", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2557.352635270365, - "start" : 750.6101694915254, - "track" : "v2" - }, - { - "duration" : 15.152542372881271, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "353D8752-627E-40D9-8D76-BFB4084F392D", - "kind" : "video", - "linkId" : "DF0D7986-17C5-4CB7-B683-DD878723E8C7", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2557.216028134137, - "start" : 750.6101694915254, - "track" : "v3" - }, - { - "duration" : 15.152542372881271, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "901CEC11-981C-4F2C-8857-98E171F95538", - "kind" : "video", - "linkId" : "DF0D7986-17C5-4CB7-B683-DD878723E8C7", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2557.2160235501383, - "start" : 750.6101694915254, - "track" : "v4" - }, - { - "duration" : 5.661016949152554, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E259582D-FC1C-4FA5-BE66-998C83FEC210", - "kind" : "video", - "linkId" : "9A0BE697-0A09-4342-804A-A10695A5765B", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2575.42372881356, - "start" : 765.7627118644068, - "track" : "v0" - }, - { - "duration" : 5.661016949152554, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FB4E9075-64CB-4C65-A7BE-659E4788FCF8", - "kind" : "audio", - "linkId" : "9A0BE697-0A09-4342-804A-A10695A5765B", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2575.339857272219, - "start" : 765.7627118644068, - "track" : "v1" - }, - { - "duration" : 5.661016949152554, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A6714A57-F046-4DE6-805C-A525B611FBD7", - "kind" : "audio", - "linkId" : "9A0BE697-0A09-4342-804A-A10695A5765B", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2575.38653357545, - "start" : 765.7627118644068, - "track" : "v2" - }, - { - "duration" : 5.661016949152554, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "DB4E1C97-4EDB-41E2-966E-057D14890226", - "kind" : "video", - "linkId" : "9A0BE697-0A09-4342-804A-A10695A5765B", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2575.249926439222, - "start" : 765.7627118644068, - "track" : "v3" - }, - { - "duration" : 5.661016949152554, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E9D9C86D-9ED7-4309-8C45-79EE9034CFD1", - "kind" : "video", - "linkId" : "9A0BE697-0A09-4342-804A-A10695A5765B", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2575.249921855223, - "start" : 765.7627118644068, - "track" : "v4" - }, - { - "duration" : 65.15254237288138, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "864CC0F8-B363-4CE4-B255-61F2E32173FE", - "kind" : "video", - "linkId" : "FAD43732-5E8B-4F4A-AF1A-5F977F88D803", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2581.0847457627124, - "start" : 778.0677966101695, - "track" : "v0" - }, - { - "duration" : 65.15254237288138, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F19656B8-AD96-45A9-AB45-41E84163B720", - "kind" : "audio", - "linkId" : "FAD43732-5E8B-4F4A-AF1A-5F977F88D803", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2581.0008742213713, - "start" : 778.0677966101695, - "track" : "v1" - }, - { - "duration" : 65.15254237288138, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D40E7E5C-E58F-4D04-8A61-4C5A0AFE2C2C", - "kind" : "audio", - "linkId" : "FAD43732-5E8B-4F4A-AF1A-5F977F88D803", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2581.0475505246022, - "start" : 778.0677966101695, - "track" : "v2" - }, - { - "duration" : 65.15254237288138, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0AEACF86-70AC-4999-8D26-F9BDE6003C4F", - "kind" : "video", - "linkId" : "FAD43732-5E8B-4F4A-AF1A-5F977F88D803", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2580.910943388374, - "start" : 778.0677966101695, - "track" : "v3" - }, - { - "duration" : 65.15254237288138, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8566FA67-6A70-4CFF-9C03-5DED0AE5F620", - "kind" : "video", - "linkId" : "FAD43732-5E8B-4F4A-AF1A-5F977F88D803", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2580.9109388043753, - "start" : 778.0677966101695, - "track" : "v4" - }, - { - "duration" : 2.135593220338933, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0FAA1CB7-A092-406F-B1FD-828EE4768B07", - "kind" : "video", - "linkId" : "FC57D67A-F7BF-448D-985C-05D71447DD0A", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2646.2372881355936, - "start" : 843.2203389830509, - "track" : "v0" - }, - { - "duration" : 2.135593220338933, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "AC5A9D19-E998-4CC0-B033-5C9AF84326C4", - "kind" : "audio", - "linkId" : "FC57D67A-F7BF-448D-985C-05D71447DD0A", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2646.1534165942526, - "start" : 843.2203389830509, - "track" : "v1" - }, - { - "duration" : 2.135593220338933, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BF2AE534-1A06-4009-B825-8C1570D790C7", - "kind" : "audio", - "linkId" : "FC57D67A-F7BF-448D-985C-05D71447DD0A", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2646.2000928974835, - "start" : 843.2203389830509, - "track" : "v2" - }, - { - "duration" : 2.135593220338933, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "10E9AD27-80E1-4F3C-BD87-414A1B6617A8", - "kind" : "video", - "linkId" : "FC57D67A-F7BF-448D-985C-05D71447DD0A", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2646.0634857612554, - "start" : 843.2203389830509, - "track" : "v3" - }, - { - "duration" : 2.135593220338933, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3EE49ACC-C52A-4ADF-86BE-884CE4259D4B", - "kind" : "video", - "linkId" : "FC57D67A-F7BF-448D-985C-05D71447DD0A", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2646.0634811772566, - "start" : 843.2203389830509, - "track" : "v4" - }, - { - "duration" : 2.1355932203390466, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6F6DB3B3-42CE-4046-9988-087B5847CE7F", - "kind" : "video", - "linkId" : "28EA7312-79F0-4C9C-A37D-04E32716FA29", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2650.203389830509, - "start" : 845.3559322033898, - "track" : "v0" - }, - { - "duration" : 2.1355932203390466, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "96066F14-B10E-4826-890D-95FE249BB166", - "kind" : "audio", - "linkId" : "28EA7312-79F0-4C9C-A37D-04E32716FA29", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2650.119518289168, - "start" : 845.3559322033898, - "track" : "v1" - }, - { - "duration" : 2.1355932203390466, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B07978FC-00CF-41C2-8657-665D128435D0", - "kind" : "audio", - "linkId" : "28EA7312-79F0-4C9C-A37D-04E32716FA29", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2650.166194592399, - "start" : 845.3559322033898, - "track" : "v2" - }, - { - "duration" : 2.1355932203390466, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4FAD2B83-A8E8-4692-9360-C2F908C32F76", - "kind" : "video", - "linkId" : "28EA7312-79F0-4C9C-A37D-04E32716FA29", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2650.0295874561707, - "start" : 845.3559322033898, - "track" : "v3" - }, - { - "duration" : 2.1355932203390466, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "DF197166-8983-45B6-A285-54D45DECCFFB", - "kind" : "video", - "linkId" : "28EA7312-79F0-4C9C-A37D-04E32716FA29", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2650.029582872172, - "start" : 845.3559322033898, - "track" : "v4" - }, - { - "duration" : 0.7118644067796822, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A6D0B507-6A36-4A23-B415-3888A8D66CEE", - "kind" : "video", - "linkId" : "E3D0D877-F5B9-4B99-B94B-44E5E47BEAE8", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2653.118644067797, - "start" : 847.4915254237288, - "track" : "v0" - }, - { - "duration" : 0.7118644067796822, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "AEBE3D73-9EE9-42F3-80FF-AF4E272C5BF5", - "kind" : "audio", - "linkId" : "E3D0D877-F5B9-4B99-B94B-44E5E47BEAE8", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2653.034772526456, - "start" : 847.4915254237288, - "track" : "v1" - }, - { - "duration" : 0.7118644067796822, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A3B22B00-BA4B-4A40-91A3-F3E3C884C9BA", - "kind" : "audio", - "linkId" : "E3D0D877-F5B9-4B99-B94B-44E5E47BEAE8", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2653.081448829687, - "start" : 847.4915254237288, - "track" : "v2" - }, - { - "duration" : 0.7118644067796822, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7B3ADFE8-7F47-4800-B8D6-77B5B07AFA99", - "kind" : "video", - "linkId" : "E3D0D877-F5B9-4B99-B94B-44E5E47BEAE8", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2652.944841693459, - "start" : 847.4915254237288, - "track" : "v3" - }, - { - "duration" : 0.7118644067796822, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BC37902D-B6DE-4EE5-9996-467DEC167330", - "kind" : "video", - "linkId" : "E3D0D877-F5B9-4B99-B94B-44E5E47BEAE8", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2652.94483710946, - "start" : 847.4915254237288, - "track" : "v4" - }, - { - "duration" : 0.7796610169491487, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8C61A3D8-55F3-4CCD-A02E-859B57EDA9CD", - "kind" : "video", - "linkId" : "86F71C9D-4CC8-49AE-85D6-08C4B076CE87", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2658.71186440678, - "start" : 848.2033898305085, - "track" : "v0" - }, - { - "duration" : 0.7796610169491487, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "25E99D83-FDE9-4E70-BBD3-A76D726F2D15", - "kind" : "audio", - "linkId" : "86F71C9D-4CC8-49AE-85D6-08C4B076CE87", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2658.627992865439, - "start" : 848.2033898305085, - "track" : "v1" - }, - { - "duration" : 0.7796610169491487, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5FD3B500-E4B2-4AE8-B37D-DE251B465DBD", - "kind" : "audio", - "linkId" : "86F71C9D-4CC8-49AE-85D6-08C4B076CE87", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2658.67466916867, - "start" : 848.2033898305085, - "track" : "v2" - }, - { - "duration" : 0.7796610169491487, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7BE9FE0B-5C6E-49DC-ACD3-613EE443EF5B", - "kind" : "video", - "linkId" : "86F71C9D-4CC8-49AE-85D6-08C4B076CE87", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2658.5380620324418, - "start" : 848.2033898305085, - "track" : "v3" - }, - { - "duration" : 0.7796610169491487, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E51F2531-CEDB-4175-A047-516CD3142C93", - "kind" : "video", - "linkId" : "86F71C9D-4CC8-49AE-85D6-08C4B076CE87", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2658.538057448443, - "start" : 848.2033898305085, - "track" : "v4" - }, - { - "duration" : 4.881355932203405, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "352DB16B-39D1-475A-8C95-A0D712C61DA1", - "kind" : "video", - "linkId" : "3EABBA0E-4C24-4221-9E58-79D35BC9B08D", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2672.77966101695, - "start" : 848.9830508474577, - "track" : "v0" - }, - { - "duration" : 4.881355932203405, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8904F908-4ED1-49D6-A064-B85CD9585F94", - "kind" : "audio", - "linkId" : "3EABBA0E-4C24-4221-9E58-79D35BC9B08D", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2672.6957894756088, - "start" : 848.9830508474577, - "track" : "v1" - }, - { - "duration" : 4.881355932203405, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F88FC5BF-C07B-4659-96CD-F0C6423F09A3", - "kind" : "audio", - "linkId" : "3EABBA0E-4C24-4221-9E58-79D35BC9B08D", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2672.7424657788397, - "start" : 848.9830508474577, - "track" : "v2" - }, - { - "duration" : 4.881355932203405, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "319CEC14-6215-4445-8F0D-91C308A3420E", - "kind" : "video", - "linkId" : "3EABBA0E-4C24-4221-9E58-79D35BC9B08D", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2672.6058586426116, - "start" : 848.9830508474577, - "track" : "v3" - }, - { - "duration" : 4.881355932203405, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EE17ADAE-984B-467F-8219-EC6C5F5D962B", - "kind" : "video", - "linkId" : "3EABBA0E-4C24-4221-9E58-79D35BC9B08D", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2672.6058540586127, - "start" : 848.9830508474577, - "track" : "v4" - }, - { - "duration" : 4.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FCB1D834-DEAA-473C-A554-9F80416158C2", - "kind" : "video", - "linkId" : "3B4E4A04-CB5C-4738-B65E-F7076495C9D8", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2710.7118644067805, - "start" : 853.8644067796611, - "track" : "v0" - }, - { - "duration" : 4.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "881E7DB1-B037-40A9-9296-A99F279D7B54", - "kind" : "audio", - "linkId" : "3B4E4A04-CB5C-4738-B65E-F7076495C9D8", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2710.6279928654394, - "start" : 853.8644067796611, - "track" : "v1" - }, - { - "duration" : 4.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5D38E6BE-385D-45A3-9AF3-30FF2BB3F39B", - "kind" : "audio", - "linkId" : "3B4E4A04-CB5C-4738-B65E-F7076495C9D8", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2710.6746691686703, - "start" : 853.8644067796611, - "track" : "v2" - }, - { - "duration" : 4.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7B349858-7AE2-48A3-991E-DB25F74D2807", - "kind" : "video", - "linkId" : "3B4E4A04-CB5C-4738-B65E-F7076495C9D8", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2710.538062032442, - "start" : 853.8644067796611, - "track" : "v3" - }, - { - "duration" : 4.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "58E4F08F-1403-43B7-B93B-C6BFA5127F8D", - "kind" : "video", - "linkId" : "3B4E4A04-CB5C-4738-B65E-F7076495C9D8", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2710.5380574484434, - "start" : 853.8644067796611, - "track" : "v4" - }, - { - "duration" : 30.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "52F2190F-1C89-44EA-94D3-8A7085B8172A", - "kind" : "video", - "linkId" : "AE414DA9-138E-4B31-BCEA-EBD522E81175", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2721.322033898306, - "start" : 857.9661016949152, - "track" : "v0" - }, - { - "duration" : 30.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "369AF7CA-F05E-4BAF-B4DA-92398E680470", - "kind" : "audio", - "linkId" : "AE414DA9-138E-4B31-BCEA-EBD522E81175", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2721.238162356965, - "start" : 857.9661016949152, - "track" : "v1" - }, - { - "duration" : 30.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E0C2832B-D290-4F69-B311-6CCB13D81611", - "kind" : "audio", - "linkId" : "AE414DA9-138E-4B31-BCEA-EBD522E81175", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2721.284838660196, - "start" : 857.9661016949152, - "track" : "v2" - }, - { - "duration" : 30.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "435A5598-D5FB-4880-AF72-ACF1883BA634", - "kind" : "video", - "linkId" : "AE414DA9-138E-4B31-BCEA-EBD522E81175", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2721.1482315239678, - "start" : 857.9661016949152, - "track" : "v3" - }, - { - "duration" : 30.27118644067798, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E1A300BD-01C6-44F8-A372-7EDEAAA3407A", - "kind" : "video", - "linkId" : "AE414DA9-138E-4B31-BCEA-EBD522E81175", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2721.148226939969, - "start" : 857.9661016949152, - "track" : "v4" - }, - { - "duration" : 2.8135593220339388, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F79BA8C9-1EEA-4EAD-9408-BEB36A06F6EC", - "kind" : "video", - "linkId" : "4045FA44-3B4D-4BFC-A67F-515E3C553BA0", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2760.915254237289, - "start" : 888.2372881355932, - "track" : "v0" - }, - { - "duration" : 2.8135593220339388, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E4CC2092-0AFA-454F-A056-DC1D4B96EAE3", - "kind" : "audio", - "linkId" : "4045FA44-3B4D-4BFC-A67F-515E3C553BA0", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2760.831382695948, - "start" : 888.2372881355932, - "track" : "v1" - }, - { - "duration" : 2.8135593220339388, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C6569D08-E232-40FD-AD5E-38A850503FDF", - "kind" : "audio", - "linkId" : "4045FA44-3B4D-4BFC-A67F-515E3C553BA0", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2760.878058999179, - "start" : 888.2372881355932, - "track" : "v2" - }, - { - "duration" : 2.8135593220339388, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5D424C43-D60A-4A52-A33B-BBEB380053F2", - "kind" : "video", - "linkId" : "4045FA44-3B4D-4BFC-A67F-515E3C553BA0", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2760.7414518629507, - "start" : 888.2372881355932, - "track" : "v3" - }, - { - "duration" : 2.8135593220339388, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C9ECC1B1-1D5A-4D84-9012-9864635A322A", - "kind" : "video", - "linkId" : "4045FA44-3B4D-4BFC-A67F-515E3C553BA0", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2760.741447278952, - "start" : 888.2372881355932, - "track" : "v4" - }, - { - "duration" : 5.288135593220318, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1B56AEBA-0FE3-40D6-A485-407844047F55", - "kind" : "video", - "linkId" : "F572F9CE-FD52-495A-A477-9D6D62AC1086", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2783.186440677967, - "start" : 891.0508474576271, - "track" : "v0" - }, - { - "duration" : 5.288135593220318, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "47F3B364-BE6C-473E-85C4-AD4A9DADF041", - "kind" : "audio", - "linkId" : "F572F9CE-FD52-495A-A477-9D6D62AC1086", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2783.102569136626, - "start" : 891.0508474576271, - "track" : "v1" - }, - { - "duration" : 5.288135593220318, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E99A647D-A1D7-4937-883D-517D2AB47C45", - "kind" : "audio", - "linkId" : "F572F9CE-FD52-495A-A477-9D6D62AC1086", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2783.1492454398567, - "start" : 891.0508474576271, - "track" : "v2" - }, - { - "duration" : 5.288135593220318, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "68CBC79C-1130-4411-9396-AF8D0501E229", - "kind" : "video", - "linkId" : "F572F9CE-FD52-495A-A477-9D6D62AC1086", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2783.0126383036286, - "start" : 891.0508474576271, - "track" : "v3" - }, - { - "duration" : 5.288135593220318, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B4AEE7C6-2A5A-4B03-B760-5FB897266EB9", - "kind" : "video", - "linkId" : "F572F9CE-FD52-495A-A477-9D6D62AC1086", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2783.01263371963, - "start" : 891.0508474576271, - "track" : "v4" - }, - { - "duration" : 25.96610169491521, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3A767D55-B04B-4029-9717-1C032BD7E834", - "kind" : "video", - "linkId" : "B52B0E53-2FA8-47CD-B126-B2D828808856", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2807.152542372882, - "start" : 896.3389830508474, - "track" : "v0" - }, - { - "duration" : 25.96610169491521, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "CF8D639C-6FA9-4562-BF86-A28192C1D6C3", - "kind" : "audio", - "linkId" : "B52B0E53-2FA8-47CD-B126-B2D828808856", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2807.068670831541, - "start" : 896.3389830508474, - "track" : "v1" - }, - { - "duration" : 25.96610169491521, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "304BEE54-6B93-44B2-BF5C-F5E0F8864DBB", - "kind" : "audio", - "linkId" : "B52B0E53-2FA8-47CD-B126-B2D828808856", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2807.115347134772, - "start" : 896.3389830508474, - "track" : "v2" - }, - { - "duration" : 25.96610169491521, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7DAAE3B6-0D4A-4029-9891-8A2D9DD41EB6", - "kind" : "video", - "linkId" : "B52B0E53-2FA8-47CD-B126-B2D828808856", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2806.978739998544, - "start" : 896.3389830508474, - "track" : "v3" - }, - { - "duration" : 25.96610169491521, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A7EC127B-861B-436B-973A-566A7282AB56", - "kind" : "video", - "linkId" : "B52B0E53-2FA8-47CD-B126-B2D828808856", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2806.978735414545, - "start" : 896.3389830508474, - "track" : "v4" - }, - { - "duration" : 1.9322033898305335, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B9DADEF6-8878-42FB-BD66-05228CB67D3F", - "kind" : "video", - "linkId" : "6C868D02-D8F5-470C-891B-B0D8A8F52B42", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2833.1186440677975, - "start" : 922.3050847457627, - "track" : "v0" - }, - { - "duration" : 1.9322033898305335, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "43CC1239-2AD4-4790-8627-80441483FA43", - "kind" : "audio", - "linkId" : "6C868D02-D8F5-470C-891B-B0D8A8F52B42", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2833.0347725264564, - "start" : 922.3050847457627, - "track" : "v1" - }, - { - "duration" : 1.9322033898305335, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3E10D3AF-E56C-4B77-B15A-7A6A433E78B5", - "kind" : "audio", - "linkId" : "6C868D02-D8F5-470C-891B-B0D8A8F52B42", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2833.0814488296874, - "start" : 922.3050847457627, - "track" : "v2" - }, - { - "duration" : 1.9322033898305335, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0AE9423C-B5CA-40D7-A25F-1D5950077D5C", - "kind" : "video", - "linkId" : "6C868D02-D8F5-470C-891B-B0D8A8F52B42", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2832.9448416934592, - "start" : 922.3050847457627, - "track" : "v3" - }, - { - "duration" : 1.9322033898305335, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FA6C223A-7F5A-4150-A5F4-D2B4B445D9C0", - "kind" : "video", - "linkId" : "6C868D02-D8F5-470C-891B-B0D8A8F52B42", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2832.9448371094604, - "start" : 922.3050847457627, - "track" : "v4" - }, - { - "duration" : 7.186440677966175, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "934F79A8-575E-43F0-AD6C-F24F5199B7CB", - "kind" : "video", - "linkId" : "851CBD5D-70B1-429E-9236-7ECFEDAAA252", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2838.7118644067805, - "start" : 924.2372881355932, - "track" : "v0" - }, - { - "duration" : 7.186440677966175, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A6FAE2FB-6564-42B5-ACC5-56029B3331BE", - "kind" : "audio", - "linkId" : "851CBD5D-70B1-429E-9236-7ECFEDAAA252", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2838.6279928654394, - "start" : 924.2372881355932, - "track" : "v1" - }, - { - "duration" : 7.186440677966175, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EE147F25-D8F4-43EB-B6A4-DE05CB21BB12", - "kind" : "audio", - "linkId" : "851CBD5D-70B1-429E-9236-7ECFEDAAA252", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2838.6746691686703, - "start" : 924.2372881355932, - "track" : "v2" - }, - { - "duration" : 7.186440677966175, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4890D002-161D-4D3E-A662-D441F2232FFF", - "kind" : "video", - "linkId" : "851CBD5D-70B1-429E-9236-7ECFEDAAA252", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2838.538062032442, - "start" : 924.2372881355932, - "track" : "v3" - }, - { - "duration" : 7.186440677966175, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "9BAE233F-FFA4-4044-A029-3B4EC16018C3", - "kind" : "video", - "linkId" : "851CBD5D-70B1-429E-9236-7ECFEDAAA252", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2838.5380574484434, - "start" : 924.2372881355932, - "track" : "v4" - }, - { - "duration" : 3.830508474576277, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FB84EAA1-5B89-4489-89B4-D080098A1CD8", - "kind" : "video", - "linkId" : "6206B630-2FC1-4084-9937-5E08F99F118D", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2890.5084745762715, - "start" : 931.4237288135594, - "track" : "v0" - }, - { - "duration" : 3.830508474576277, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "EE6692DD-2585-4607-A46A-C6A25FB86B72", - "kind" : "audio", - "linkId" : "6206B630-2FC1-4084-9937-5E08F99F118D", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2890.4246030349304, - "start" : 931.4237288135594, - "track" : "v1" - }, - { - "duration" : 3.830508474576277, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "78A13F6F-DCA5-491A-8B52-0FC2409760AB", - "kind" : "audio", - "linkId" : "6206B630-2FC1-4084-9937-5E08F99F118D", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2890.4712793381614, - "start" : 931.4237288135594, - "track" : "v2" - }, - { - "duration" : 3.830508474576277, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "629569A0-6C4A-466A-AFA1-918C22E2E018", - "kind" : "video", - "linkId" : "6206B630-2FC1-4084-9937-5E08F99F118D", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2890.3346722019332, - "start" : 931.4237288135594, - "track" : "v3" - }, - { - "duration" : 3.830508474576277, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "55E9F328-E350-4B59-ACEA-6C4EF5E5A345", - "kind" : "video", - "linkId" : "6206B630-2FC1-4084-9937-5E08F99F118D", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2890.3346676179344, - "start" : 931.4237288135594, - "track" : "v4" - }, - { - "duration" : 3.016949152542338, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "61D97AD3-FA77-4D5E-85F2-0D8E088424D8", - "kind" : "video", - "linkId" : "03409808-1CF1-4E22-8CFC-D7F337CA20C0", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2897.593220338983, - "start" : 935.2542372881356, - "track" : "v0" - }, - { - "duration" : 3.016949152542338, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A4B63EE7-142A-4568-B3A3-9BD1DFE06CE5", - "kind" : "audio", - "linkId" : "03409808-1CF1-4E22-8CFC-D7F337CA20C0", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2897.509348797642, - "start" : 935.2542372881356, - "track" : "v1" - }, - { - "duration" : 3.016949152542338, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2195C3FF-C478-48A3-B480-A8D661ECF7EF", - "kind" : "audio", - "linkId" : "03409808-1CF1-4E22-8CFC-D7F337CA20C0", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2897.556025100873, - "start" : 935.2542372881356, - "track" : "v2" - }, - { - "duration" : 3.016949152542338, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "12215B20-BCE9-4ED2-BEC6-1EF6791F42B1", - "kind" : "video", - "linkId" : "03409808-1CF1-4E22-8CFC-D7F337CA20C0", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2897.4194179646447, - "start" : 935.2542372881356, - "track" : "v3" - }, - { - "duration" : 3.016949152542338, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "416FCED0-3BEE-4052-95C4-0D4395DF944A", - "kind" : "video", - "linkId" : "03409808-1CF1-4E22-8CFC-D7F337CA20C0", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2897.419413380646, - "start" : 935.2542372881356, - "track" : "v4" - }, - { - "duration" : 0.8135593220338251, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3D695665-26AC-4ECE-B055-5ED89B2AEF2E", - "kind" : "video", - "linkId" : "D7000904-0663-4407-824B-32447EFF33F6", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2904.5423728813553, - "start" : 938.271186440678, - "track" : "v0" - }, - { - "duration" : 0.8135593220338251, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1895810B-430E-4DE6-8E5B-653139F5FBEE", - "kind" : "audio", - "linkId" : "D7000904-0663-4407-824B-32447EFF33F6", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2904.458501340014, - "start" : 938.271186440678, - "track" : "v1" - }, - { - "duration" : 0.8135593220338251, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0E102FE5-7926-4A40-9B94-CF405C37EB75", - "kind" : "audio", - "linkId" : "D7000904-0663-4407-824B-32447EFF33F6", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2904.505177643245, - "start" : 938.271186440678, - "track" : "v2" - }, - { - "duration" : 0.8135593220338251, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "222B3055-014A-4379-98CD-69EC43A35470", - "kind" : "video", - "linkId" : "D7000904-0663-4407-824B-32447EFF33F6", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2904.368570507017, - "start" : 938.271186440678, - "track" : "v3" - }, - { - "duration" : 0.8135593220338251, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "28CA102D-CDC8-46C5-988E-86F0EEF5EEDC", - "kind" : "video", - "linkId" : "D7000904-0663-4407-824B-32447EFF33F6", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2904.368565923018, - "start" : 938.271186440678, - "track" : "v4" - }, - { - "duration" : 16.169491525423723, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1BBD2846-8898-43ED-B9EF-51ABB977D172", - "kind" : "video", - "linkId" : "441E27DB-C392-40BA-85B8-B3F421EF14E8", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2905.3559322033893, - "start" : 939.0847457627119, - "track" : "v0" - }, - { - "duration" : 16.169491525423723, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0238A063-2A08-4FDB-920C-7372B455BCB8", - "kind" : "audio", - "linkId" : "441E27DB-C392-40BA-85B8-B3F421EF14E8", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2905.2720606620483, - "start" : 939.0847457627119, - "track" : "v1" - }, - { - "duration" : 16.169491525423723, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "CAF3113A-611A-4A6B-9A03-3539D6A0E4B1", - "kind" : "audio", - "linkId" : "441E27DB-C392-40BA-85B8-B3F421EF14E8", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2905.318736965279, - "start" : 939.0847457627119, - "track" : "v2" - }, - { - "duration" : 16.169491525423723, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "19424805-E973-46BC-BD0D-3547754230B4", - "kind" : "video", - "linkId" : "441E27DB-C392-40BA-85B8-B3F421EF14E8", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2905.182129829051, - "start" : 939.0847457627119, - "track" : "v3" - }, - { - "duration" : 16.169491525423723, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D38B18BD-6C89-4507-A1EF-9B817B8E10CA", - "kind" : "video", - "linkId" : "441E27DB-C392-40BA-85B8-B3F421EF14E8", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2905.1821252450522, - "start" : 939.0847457627119, - "track" : "v4" - }, - { - "duration" : 32.4406779661017, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "92828BED-54D1-43DA-ADCC-A2784A7E60A8", - "kind" : "video", - "linkId" : "C90940D8-882F-4CBD-839A-3F097CE89ED2", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2950.3389830508468, - "start" : 955.2542372881356, - "track" : "v0" - }, - { - "duration" : 32.4406779661017, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "64CCF21E-AA71-4C5F-9CB3-EAE93F031C97", - "kind" : "audio", - "linkId" : "C90940D8-882F-4CBD-839A-3F097CE89ED2", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2950.2551115095057, - "start" : 955.2542372881356, - "track" : "v1" - }, - { - "duration" : 32.4406779661017, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0EF2F1D8-B27D-425B-A77E-024D9E8ED834", - "kind" : "audio", - "linkId" : "C90940D8-882F-4CBD-839A-3F097CE89ED2", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2950.3017878127366, - "start" : 955.2542372881356, - "track" : "v2" - }, - { - "duration" : 32.4406779661017, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7F966B3B-7499-46E3-A145-F4F6CB9899E5", - "kind" : "video", - "linkId" : "C90940D8-882F-4CBD-839A-3F097CE89ED2", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2950.1651806765085, - "start" : 955.2542372881356, - "track" : "v3" - }, - { - "duration" : 32.4406779661017, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "869C0AAC-694F-4A83-ACBD-BA02B2F1EBCA", - "kind" : "video", - "linkId" : "C90940D8-882F-4CBD-839A-3F097CE89ED2", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2950.1651760925097, - "start" : 955.2542372881356, - "track" : "v4" - }, - { - "duration" : 2.5762711864406356, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "25EDDE61-86BB-4F70-A496-23BF74AE9006", - "kind" : "video", - "linkId" : "9D7E5089-781D-46B7-83D2-8B95043855FA", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2983.661016949152, - "start" : 987.6949152542373, - "track" : "v0" - }, - { - "duration" : 2.5762711864406356, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "AAAAE9EC-5D0B-4491-B3AE-40D47947A105", - "kind" : "audio", - "linkId" : "9D7E5089-781D-46B7-83D2-8B95043855FA", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2983.577145407811, - "start" : 987.6949152542373, - "track" : "v1" - }, - { - "duration" : 2.5762711864406356, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A75BBCB8-738D-4750-B235-F5EC59113946", - "kind" : "audio", - "linkId" : "9D7E5089-781D-46B7-83D2-8B95043855FA", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2983.6238217110417, - "start" : 987.6949152542373, - "track" : "v2" - }, - { - "duration" : 2.5762711864406356, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "71F23B1C-6343-4637-9DFC-DF10EEE27AD4", - "kind" : "video", - "linkId" : "9D7E5089-781D-46B7-83D2-8B95043855FA", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2983.4872145748136, - "start" : 987.6949152542373, - "track" : "v3" - }, - { - "duration" : 2.5762711864406356, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BFF6C236-F0FD-45E2-B714-265E9B8F2102", - "kind" : "video", - "linkId" : "9D7E5089-781D-46B7-83D2-8B95043855FA", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2983.487209990815, - "start" : 987.6949152542373, - "track" : "v4" - }, - { - "duration" : 14.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2B6D6836-A007-4343-9775-70DD28CB6EB9", - "kind" : "video", - "linkId" : "EFD99B70-8810-42EC-8F2B-5AA76217327E", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2986.542372881355, - "start" : 990.271186440678, - "track" : "v0" - }, - { - "duration" : 14.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "7E48882C-AD53-4651-90D8-9CBDFEF95F0C", - "kind" : "audio", - "linkId" : "EFD99B70-8810-42EC-8F2B-5AA76217327E", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 2986.4585013400138, - "start" : 990.271186440678, - "track" : "v1" - }, - { - "duration" : 14.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "BA449FE2-AFB9-4E77-8799-8FE2429251A7", - "kind" : "audio", - "linkId" : "EFD99B70-8810-42EC-8F2B-5AA76217327E", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2986.5051776432447, - "start" : 990.271186440678, - "track" : "v2" - }, - { - "duration" : 14.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A3C307CE-4B58-4DB3-B763-3452763F1277", - "kind" : "video", - "linkId" : "EFD99B70-8810-42EC-8F2B-5AA76217327E", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2986.3685705070166, - "start" : 990.271186440678, - "track" : "v3" - }, - { - "duration" : 14.101694915254257, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "322BFB74-8CE6-4C2F-950C-898A113AB445", - "kind" : "video", - "linkId" : "EFD99B70-8810-42EC-8F2B-5AA76217327E", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 2986.3685659230177, - "start" : 990.271186440678, - "track" : "v4" - }, - { - "duration" : 41.18644067796606, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "1C0BB1B6-1862-4A8E-B78B-26DBECD3F37B", - "kind" : "video", - "linkId" : "9F7D047B-0DE6-4655-9D6E-5B8ADDD0267B", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3016.4745762711855, - "start" : 1004.3728813559322, - "track" : "v0" - }, - { - "duration" : 41.18644067796606, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3DF07444-8948-4073-A7B7-DCD022163D06", - "kind" : "audio", - "linkId" : "9F7D047B-0DE6-4655-9D6E-5B8ADDD0267B", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 3016.3907047298444, - "start" : 1004.3728813559322, - "track" : "v1" - }, - { - "duration" : 41.18644067796606, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "17518456-794B-46AE-8B57-11535384C6DD", - "kind" : "audio", - "linkId" : "9F7D047B-0DE6-4655-9D6E-5B8ADDD0267B", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3016.4373810330753, - "start" : 1004.3728813559322, - "track" : "v2" - }, - { - "duration" : 41.18644067796606, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D83C6190-0569-4756-B034-B56F5EA95C6A", - "kind" : "video", - "linkId" : "9F7D047B-0DE6-4655-9D6E-5B8ADDD0267B", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3016.300773896847, - "start" : 1004.3728813559322, - "track" : "v3" - }, - { - "duration" : 41.18644067796606, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "04909166-A121-49EA-B441-2498FB853E8E", - "kind" : "video", - "linkId" : "9F7D047B-0DE6-4655-9D6E-5B8ADDD0267B", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3016.3007693128484, - "start" : 1004.3728813559322, - "track" : "v4" - }, - { - "duration" : 1.661016949152554, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "AB7E6E67-21DC-473D-A4F5-32C751A46EE1", - "kind" : "video", - "linkId" : "0B407464-C16F-4EE0-8709-4530856B5481", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3065.593220338982, - "start" : 1045.5593220338983, - "track" : "v0" - }, - { - "duration" : 1.661016949152554, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3945B5C6-C770-47E5-BF52-BC27D9ABFAE0", - "kind" : "audio", - "linkId" : "0B407464-C16F-4EE0-8709-4530856B5481", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 3065.509348797641, - "start" : 1045.5593220338983, - "track" : "v1" - }, - { - "duration" : 1.661016949152554, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "D21DCFAE-885B-46AB-BBCD-46F5F68A4841", - "kind" : "audio", - "linkId" : "0B407464-C16F-4EE0-8709-4530856B5481", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3065.556025100872, - "start" : 1045.5593220338983, - "track" : "v2" - }, - { - "duration" : 1.661016949152554, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "09CE071F-2BC1-44E9-80B4-A3B494754324", - "kind" : "video", - "linkId" : "0B407464-C16F-4EE0-8709-4530856B5481", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3065.419417964644, - "start" : 1045.5593220338983, - "track" : "v3" - }, - { - "duration" : 1.661016949152554, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E59CB212-BAFB-4792-9E87-27750E214AFF", - "kind" : "video", - "linkId" : "0B407464-C16F-4EE0-8709-4530856B5481", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3065.419413380645, - "start" : 1045.5593220338983, - "track" : "v4" - }, - { - "duration" : 4.745762711864472, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "E92C9D2B-F806-42DF-BBBD-6444CED9CFA0", - "kind" : "video", - "linkId" : "BBA33081-9695-4391-B23B-D376880D264E", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3070.203389830507, - "start" : 1047.2203389830509, - "track" : "v0" - }, - { - "duration" : 4.745762711864472, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "C13910BD-C1D5-42AA-B25A-F4AFA7E0807E", - "kind" : "audio", - "linkId" : "BBA33081-9695-4391-B23B-D376880D264E", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 3070.119518289166, - "start" : 1047.2203389830509, - "track" : "v1" - }, - { - "duration" : 4.745762711864472, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6A6D3754-91FF-45E9-9A9C-EC342D95E136", - "kind" : "audio", - "linkId" : "BBA33081-9695-4391-B23B-D376880D264E", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3070.166194592397, - "start" : 1047.2203389830509, - "track" : "v2" - }, - { - "duration" : 4.745762711864472, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "122D9228-98AA-4877-A0C4-2DFD8CAE845B", - "kind" : "video", - "linkId" : "BBA33081-9695-4391-B23B-D376880D264E", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3070.029587456169, - "start" : 1047.2203389830509, - "track" : "v3" - }, - { - "duration" : 4.745762711864472, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "9124C8F4-A9B8-40C9-A87D-03794D9BFD5B", - "kind" : "video", - "linkId" : "BBA33081-9695-4391-B23B-D376880D264E", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3070.02958287217, - "start" : 1047.2203389830509, - "track" : "v4" - }, - { - "duration" : 11.93220338983042, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "28D0F5F8-073A-4EB4-ABCD-C489AF0CA087", - "kind" : "video", - "linkId" : "318B4FD4-E1D7-41FD-B760-CA9E5DAB9B1F", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3082.271186440677, - "start" : 1051.9661016949153, - "track" : "v0" - }, - { - "duration" : 11.93220338983042, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "2B967FFC-3967-43AC-B348-992E7603AE2C", - "kind" : "audio", - "linkId" : "318B4FD4-E1D7-41FD-B760-CA9E5DAB9B1F", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 3082.187314899336, - "start" : 1051.9661016949153, - "track" : "v1" - }, - { - "duration" : 11.93220338983042, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5DCE6786-7E43-4D1A-9415-C2E364A3C1E5", - "kind" : "audio", - "linkId" : "318B4FD4-E1D7-41FD-B760-CA9E5DAB9B1F", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3082.233991202567, - "start" : 1051.9661016949153, - "track" : "v2" - }, - { - "duration" : 11.93220338983042, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "CCB5A926-E4B6-4AA9-9EF3-2E8A78E7B2F2", - "kind" : "video", - "linkId" : "318B4FD4-E1D7-41FD-B760-CA9E5DAB9B1F", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3082.0973840663387, - "start" : 1051.9661016949153, - "track" : "v3" - }, - { - "duration" : 11.93220338983042, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "296DECA8-17FA-4732-85F9-E7E62AB4390E", - "kind" : "video", - "linkId" : "318B4FD4-E1D7-41FD-B760-CA9E5DAB9B1F", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3082.09737948234, - "start" : 1051.9661016949153, - "track" : "v4" - }, - { - "duration" : 2.7118644067795685, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "F118809D-1E6F-4E06-9DD8-59627D7FD31E", - "kind" : "video", - "linkId" : "D27F02ED-F905-4600-B733-AF4B73B5174F", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3097.559322033897, - "start" : 1063.8983050847457, - "track" : "v0" - }, - { - "duration" : 2.7118644067795685, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "4A2040D0-20BE-429F-AD73-5B3ED24C0A6D", - "kind" : "audio", - "linkId" : "D27F02ED-F905-4600-B733-AF4B73B5174F", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 3097.475450492556, - "start" : 1063.8983050847457, - "track" : "v1" - }, - { - "duration" : 2.7118644067795685, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "3D418272-0D62-4126-A9C7-71E7D7FC255D", - "kind" : "audio", - "linkId" : "D27F02ED-F905-4600-B733-AF4B73B5174F", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3097.522126795787, - "start" : 1063.8983050847457, - "track" : "v2" - }, - { - "duration" : 2.7118644067795685, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "B50F2156-B9AC-4F52-B362-40B0B0D732A4", - "kind" : "video", - "linkId" : "D27F02ED-F905-4600-B733-AF4B73B5174F", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3097.3855196595587, - "start" : 1063.8983050847457, - "track" : "v3" - }, - { - "duration" : 2.7118644067795685, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8C35ED3F-0857-4157-9DDF-8937A65375E5", - "kind" : "video", - "linkId" : "D27F02ED-F905-4600-B733-AF4B73B5174F", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3097.38551507556, - "start" : 1063.8983050847457, - "track" : "v4" - }, - { - "duration" : 29.864406779661067, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "5F3F6F18-5791-47A8-BCA4-A9790558A314", - "kind" : "video", - "linkId" : "0F08CC53-E7F9-4A91-9D1E-F73D91D3ECB9", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3101.3559322033884, - "start" : 1066.6101694915253, - "track" : "v0" - }, - { - "duration" : 29.864406779661067, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "CF1D8F7D-26D5-4CD2-8D99-2370B29915C0", - "kind" : "audio", - "linkId" : "0F08CC53-E7F9-4A91-9D1E-F73D91D3ECB9", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 3101.2720606620474, - "start" : 1066.6101694915253, - "track" : "v1" - }, - { - "duration" : 29.864406779661067, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A1160BCB-0740-4FC5-BEA2-A90D6030BFA6", - "kind" : "audio", - "linkId" : "0F08CC53-E7F9-4A91-9D1E-F73D91D3ECB9", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3101.3187369652783, - "start" : 1066.6101694915253, - "track" : "v2" - }, - { - "duration" : 29.864406779661067, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "8E323D18-4C84-4AFA-B90D-264EE10DBF94", - "kind" : "video", - "linkId" : "0F08CC53-E7F9-4A91-9D1E-F73D91D3ECB9", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3101.18212982905, - "start" : 1066.6101694915253, - "track" : "v3" - }, - { - "duration" : 29.864406779661067, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "A1E18A2B-C2ED-4F4F-B090-2F04AB1EAB86", - "kind" : "video", - "linkId" : "0F08CC53-E7F9-4A91-9D1E-F73D91D3ECB9", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3101.1821252450513, - "start" : 1066.6101694915253, - "track" : "v4" - }, - { - "duration" : 3046.474576440677, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "77231674-DFD4-469D-8BD3-B0384766ED44", - "kind" : "video", - "linkId" : "06145E01-F314-4D25-9F5A-C1D23472B857", - "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3175.7288135593208, - "start" : 1096.4745762711864, - "track" : "v0" - }, - { - "duration" : 3046.477724982019, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "46D41482-2761-4F49-8714-E482340928D2", - "kind" : "audio", - "linkId" : "06145E01-F314-4D25-9F5A-C1D23472B857", - "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "muted" : true, - "newShot" : false, - "speed" : 1, - "srcIn" : 3175.6449420179797, - "start" : 1096.4745762711864, - "track" : "v1" - }, - { - "duration" : 3046.516381678787, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "6655D05A-2F89-4E98-A2C0-475D1908D6DB", - "kind" : "audio", - "linkId" : "06145E01-F314-4D25-9F5A-C1D23472B857", - "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3175.6916183212106, - "start" : 1096.4745762711864, - "track" : "v2" - }, - { - "duration" : 3046.5116558150157, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "0BA9AB3D-B156-466B-A477-DD6CBCC6D5D0", - "kind" : "video", - "linkId" : "06145E01-F314-4D25-9F5A-C1D23472B857", - "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3175.5550111849825, - "start" : 1096.4745762711864, - "track" : "v3" - }, - { - "duration" : 3046.5116603990145, - "fadeIn" : 0, - "fadeOut" : 0, - "id" : "FA5C83A2-F1FC-4E94-B0A3-D12DF73B9D65", - "kind" : "video", - "linkId" : "06145E01-F314-4D25-9F5A-C1D23472B857", - "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "muted" : false, - "newShot" : false, - "speed" : 1, - "srcIn" : 3175.5550066009837, - "start" : 1096.4745762711864, - "track" : "v4" - } - ], - "fps" : 29.5, - "markers" : [ - - ], - "media" : [ - { - "cacheKey" : "cb2b0cb1418eb04f", - "duration" : 812.745763, - "fps" : 29.5, - "hasAudio" : false, - "height" : 720, - "id" : "26CBEF77-6850-4D82-A5BD-A6A37587C040", - "isAudio" : false, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_13.49\/cam.mov", - "width" : 1280 - }, - { - "cacheKey" : "49108ecb3168df5a", - "duration" : 812.885333, - "fps" : 30, - "hasAudio" : true, - "height" : 0, - "id" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB", - "isAudio" : true, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_13.49\/desktop.m4a", - "width" : 0 - }, - { - "cacheKey" : "9f6621781616504d", - "duration" : 813.269333, - "fps" : 30, - "hasAudio" : true, - "height" : 0, - "id" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD", - "isAudio" : true, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_13.49\/mic.m4a", - "width" : 0 - }, - { - "cacheKey" : "fbe623fb3a0f34ba", - "duration" : 812.833333, - "fps" : 30, - "hasAudio" : false, - "height" : 2160, - "id" : "609E978E-5994-4757-A6C7-EAAE72818A49", - "isAudio" : false, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_13.49\/screen-1.mov", - "width" : 3840 - }, - { - "cacheKey" : "1be7aaff98d7b0ff", - "duration" : 812.833333, - "fps" : 30, - "hasAudio" : false, - "height" : 2160, - "id" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1", - "isAudio" : false, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_13.49\/screen-2.mov", - "width" : 3840 - }, - { - "cacheKey" : "bc4651748a6b1673", - "duration" : 6222.20339, - "fps" : 29.5, - "hasAudio" : false, - "height" : 720, - "id" : "40BBB768-793D-44DA-BBC9-B22F6591159D", - "isAudio" : false, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_14.03\/cam.mov", - "width" : 1280 - }, - { - "cacheKey" : "27d4dedeccdacecc", - "duration" : 6222.122667, - "fps" : 30, - "hasAudio" : true, - "height" : 0, - "id" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6", - "isAudio" : true, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_14.03\/desktop.m4a", - "width" : 0 - }, - { - "cacheKey" : "103551fc583e9882", - "duration" : 6222.208, - "fps" : 30, - "hasAudio" : true, - "height" : 0, - "id" : "501034F8-09AC-44B1-8826-FCE9B36C68F9", - "isAudio" : true, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_14.03\/mic.m4a", - "width" : 0 - }, - { - "cacheKey" : "5bfa7a15f159568d", - "duration" : 6222.066667, - "fps" : 30, - "hasAudio" : false, - "height" : 2160, - "id" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE", - "isAudio" : false, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_14.03\/screen-1.mov", - "width" : 3840 - }, - { - "cacheKey" : "8cea14167c6e9778", - "duration" : 6222.066667, - "fps" : 30, - "hasAudio" : false, - "height" : 2160, - "id" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1", - "isAudio" : false, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_14.03\/screen-2.mov", - "width" : 3840 - }, - { - "cacheKey" : "b966487289885ec1", - "duration" : 15112.237288, - "fps" : 29.5, - "hasAudio" : false, - "height" : 720, - "id" : "CC300C55-7090-4A6E-81FA-55CB6644FAC9", - "isAudio" : false, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_16.36\/cam.mov", - "width" : 1280 - }, - { - "cacheKey" : "9044d1d53bcf480f", - "duration" : 15112.106667, - "fps" : 30, - "hasAudio" : true, - "height" : 0, - "id" : "2613683E-12E6-402E-BF4D-6D9ACD7C1491", - "isAudio" : true, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_16.36\/desktop.m4a", - "width" : 0 - }, - { - "cacheKey" : "36fc42c715b64632", - "duration" : 15112.213333, - "fps" : 30, - "hasAudio" : true, - "height" : 0, - "id" : "7889E9EC-F67A-491B-81C0-0CF8338965A3", - "isAudio" : true, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_16.36\/mic.m4a", - "width" : 0 - }, - { - "cacheKey" : "0308132cb01a092a", - "duration" : 15112.066667, - "fps" : 30, - "hasAudio" : false, - "height" : 2160, - "id" : "BC4E74C5-4C45-4633-B31A-3CAA58D4B2BF", - "isAudio" : false, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_16.36\/screen-1.mov", - "width" : 3840 - }, - { - "cacheKey" : "270afa9f16a6fb41", - "duration" : 15111.366667, - "fps" : 30, - "hasAudio" : false, - "height" : 2160, - "id" : "98882264-A2ED-477B-A7B8-B63E54EDEACE", - "isAudio" : false, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_16.36\/screen-2.mov", - "width" : 3840 - }, - { - "cacheKey" : "02a6eb97d64d4630", - "duration" : 10045.254237, - "fps" : 29.5, - "hasAudio" : false, - "height" : 720, - "id" : "2D5EED57-87EB-44B4-9623-2CF48314C5F9", - "isAudio" : false, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_22.54\/cam.mov", - "width" : 1280 - }, - { - "cacheKey" : "cc655edd968d5060", - "duration" : 10045.162667, - "fps" : 30, - "hasAudio" : true, - "height" : 0, - "id" : "0ECB1F4F-9065-432F-B0B1-7F47BD31FDCC", - "isAudio" : true, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_22.54\/desktop.m4a", - "width" : 0 - }, - { - "cacheKey" : "4a0d44fc07d2db63", - "duration" : 10045.248, - "fps" : 30, - "hasAudio" : true, - "height" : 0, - "id" : "02A5DFB3-9348-4102-BD13-F1856B431A70", - "isAudio" : true, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_22.54\/mic.m4a", - "width" : 0 - }, - { - "cacheKey" : "edc26bd451155492", - "duration" : 10045.133333, - "fps" : 30, - "hasAudio" : false, - "height" : 2160, - "id" : "9CA51332-5865-4C3D-9972-E781BDE75ECD", - "isAudio" : false, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_22.54\/screen-1.mov", - "width" : 3840 - }, - { - "cacheKey" : "3c45ae09f47beeae", - "duration" : 10045.166667, - "fps" : 30, - "hasAudio" : false, - "height" : 2160, - "id" : "0DEB0D7E-0C1E-49DA-9911-7773FB2158DE", - "isAudio" : false, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_22.54\/screen-2.mov", - "width" : 3840 - }, - { - "cacheKey" : "81f385d21caafc19", - "duration" : 11597.966102, - "fps" : 29.5, - "hasAudio" : false, - "height" : 720, - "id" : "F06DF483-F96F-4DD1-8602-53609FA888F1", - "isAudio" : false, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-04_17.30\/cam.mov", - "width" : 1280 - }, - { - "cacheKey" : "88621a686d385fe8", - "duration" : 11598.101333, - "fps" : 30, - "hasAudio" : true, - "height" : 0, - "id" : "59213540-870A-4795-BD44-B3AFBC9F14E3", - "isAudio" : true, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-04_17.30\/desktop.m4a", - "width" : 0 - }, - { - "cacheKey" : "3ee60110dd18110c", - "duration" : 11598.464, - "fps" : 30, - "hasAudio" : true, - "height" : 0, - "id" : "F4DB0AF3-2C74-4501-A58A-A519C046E3D6", - "isAudio" : true, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-04_17.30\/mic.m4a", - "width" : 0 - }, - { - "cacheKey" : "290f82433dd61a4c", - "duration" : 11598.033333, - "fps" : 30, - "hasAudio" : false, - "height" : 2160, - "id" : "9F95D76B-EAB2-49AA-897A-C3C9A265E0CC", - "isAudio" : false, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-04_17.30\/screen-1.mov", - "width" : 3840 - }, - { - "cacheKey" : "12165002971af6ff", - "duration" : 11598, - "fps" : 30, - "hasAudio" : false, - "height" : 2160, - "id" : "CC497E03-E3E7-4283-A96C-F076D9F154A1", - "isAudio" : false, - "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-04_17.30\/screen-2.mov", - "width" : 3840 - } - ], - "preferredTakes" : [ - - ], - "tracks" : [ - { - "hue" : 0 - }, - { - "hue" : 0.6180339887498949 - }, - { - "hue" : 0.2360679774997898 - }, - { - "hue" : 0.8541019662496847 - }, - { - "hue" : 0.4721359549995796 - } - ] - }, - "view" : { - "focusedTracks" : [ - - ], - "fusionFocus" : false, - "fusionHidden" : false, - "hiddenTracks" : [ - - ], - "laneScale" : 1.5562744140625, - "previewsOnLeft" : false, - "priorityPane" : "v4", - "showFilmstrips" : true, - "snapping" : true, - "trackHeights" : [ - { - "factor" : 0.99639892578125, - "track" : "v0" - }, - { - "factor" : 1, - "track" : "v4" - } - ] - } -} \ No newline at end of file diff --git a/sequencer/Sources/Sequencer/AppDelegate.swift b/sequencer/Sources/Sequencer/AppDelegate.swift index f938f54fc324eda0e216ebb48200924267f5100a..a3c73704dd827f88ef9c1c1c1de5b9a1ec9061cf 100644 --- a/sequencer/Sources/Sequencer/AppDelegate.swift +++ b/sequencer/Sources/Sequencer/AppDelegate.swift @@ -52,6 +52,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate { /// (Fusion-feeding) workflow. func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true } + /// In-flight ffmpeg encodes must die with the app: orphans hold shared + /// VideoToolbox decode sessions, and a few accumulated ones make every + /// AVPlayer in the NEXT instance render black (see MediaPipeline). + func applicationWillTerminate(_ notification: Notification) { + MediaPipeline.terminateChildren() + } + /// On launch, reopen the most recent project instead of a blank untitled /// one; fall back to a fresh untitled document when there's no history. func applicationOpenUntitledFile(_ sender: NSApplication) -> Bool { diff --git a/sequencer/Sources/Sequencer/Cachetest.swift b/sequencer/Sources/Sequencer/Cachetest.swift new file mode 100644 index 0000000000000000000000000000000000000000..bf471b97a363e75bbc14e5d7514615e5ec7283db --- /dev/null +++ b/sequencer/Sources/Sequencer/Cachetest.swift @@ -0,0 +1,132 @@ +import Foundation + +/// Headless cache-budget check: `sequencer --cachetest [capGB]`. +/// +/// Loads the real project into the headless context, jumps the playhead to +/// several spots spread across the timeline, and lets demand builds + +/// evictions run at each stop while continuously asserting the invariant +/// that makes the cap a hard ceiling: +/// +/// ledger + reserved ≤ cap (checked every second) +/// bytes on disk ≤ cap + slack (checked every few seconds) +/// +/// At each stop it also waits for the chunk under the playhead to become +/// covered — proving the system optimizes around where the user is even when +/// the whole project can't fit (the "Deltarune problem"). +func runCacheTest(path: String, capGB: Int) { + func spin(_ seconds: Double) { + RunLoop.main.run(until: Date().addingTimeInterval(seconds)) + } + + print("== Sequencer cachetest ==") + if capGB > 0 { UserDefaults.standard.set(capGB, forKey: "maxCacheGB") } + let pipeline = MediaPipeline.shared + let cap = pipeline.maxCacheBytes + print("cache: \(pipeline.cacheRoot.path)") + print(String(format: "cap: %.1f GB", Double(cap) / 1e9)) + + // Load the project (package or legacy flat file) into the headless context. + let url = URL(fileURLWithPath: path) + var jsonURL = url + var isDir: ObjCBool = false + if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir), isDir.boolValue { + jsonURL = url.appendingPathComponent("project.json") + } + guard let data = try? Data(contentsOf: jsonURL), + let doc = try? JSONDecoder().decode(SequencerDocument.self, from: data) else { + print("FAIL: cannot read project at \(path)") + return + } + let ctx = DocumentContext.headless + ctx.store.adopt(doc.project) + let project = ctx.store.project + let span = project.clips.map(\.end).max() ?? 0 + print(String(format: "project: %d clips, %d media, %.0f s timeline", + project.clips.count, project.media.count, span)) + + // Wait for the launch reconcile so admission is open. + let bootDeadline = Date().addingTimeInterval(60) + while !pipeline.budgetReady && Date() < bootDeadline { spin(0.1) } + guard pipeline.budgetReady else { print("FAIL: ledger reconcile timed out"); return } + + // Playhead stops: spread across the timeline, snapped to the midpoint of a + // real video clip so there is always something to cover. + var stops: [Double] = [] + for frac in [0.05, 0.35, 0.65, 0.9] { + let t = span * frac + if let clip = project.clips + .filter({ $0.kind == .video }) + .min(by: { abs(($0.start + $0.end) / 2 - t) < abs(($1.start + $1.end) / 2 - t) }) { + let mid = (clip.start + clip.end) / 2 + if !stops.contains(where: { abs($0 - mid) < 60 }) { stops.append(mid) } + } + } + + var failures = 0 + var walkTick = 0 + func checkInvariant(_ label: String) { + let held = pipeline.ledgerBytes <= cap + if !held { + failures += 1 + print(String(format: "FAIL: ledger %.2f GB over cap (%@)", + Double(pipeline.ledgerBytes) / 1e9, label)) + } + // The disk walk is the ground truth the ledger approximates. Slack + // covers unbudgeted small writes (filmstrips) between reconciles. + walkTick += 1 + if walkTick % 5 == 0 { + let disk = MediaPipeline.directorySize(pipeline.cacheRoot) + if disk > cap + 500_000_000 { + failures += 1 + print(String(format: "FAIL: %.2f GB on disk exceeds cap (%@)", + Double(disk) / 1e9, label)) + } + } + } + + for (i, t) in stops.enumerated() { + print(String(format: "\n-- stop %d: playhead %.0f s", i + 1, t)) + ctx.playback.seek(to: t) + ctx.chunks.ensure(for: project) + ctx.chunks.updateDemand(force: true) + let clipsHere = project.clips.filter { + $0.kind == .video && $0.start <= t && t < $0.end + } + let deadline = Date().addingTimeInterval(150) + var covered = false + var announced = false + while Date() < deadline { + spin(1.0) + checkInvariant("stop \(i + 1)") + var missing: [String] = [] + for clip in clipsHere { + guard let media = project.media(clip.mediaId) else { continue } + let src = clip.srcIn + (t - clip.start) * clip.speed + if !ctx.chunks.isCovered(media: media, sourceTime: src) + && !ctx.chunks.buildFailed(media: media, sourceTime: src) { + missing.append("\(media.cacheKey.prefix(8))#\(ChunkManager.chunkIndex(forSource: src))") + } + } + covered = missing.isEmpty + if covered { break } + if !announced { + announced = true + NSLog("[cachetest] stop %d waiting on: %@", i + 1, + missing.joined(separator: " ")) + } + } + print(String(format: "coverage at playhead: %@ (ledger %.2f GB)", + covered ? "OK" : "FAIL (not covered in 150s)", + Double(pipeline.ledgerBytes) / 1e9)) + if !covered { failures += 1 } + } + + // Let any in-flight builds settle, then final ground-truth comparison. + spin(5) + let disk = MediaPipeline.directorySize(pipeline.cacheRoot) + print(String(format: "\nfinal: ledger %.2f GB, disk %.2f GB, cap %.1f GB", + Double(pipeline.ledgerBytes) / 1e9, Double(disk) / 1e9, + Double(cap) / 1e9)) + if disk > cap + 500_000_000 { failures += 1; print("FAIL: final disk size over cap") } + print(failures == 0 ? "\n== cachetest PASS ==" : "\n== cachetest FAIL (\(failures)) ==") +} diff --git a/sequencer/Sources/Sequencer/ChunkedProxy.swift b/sequencer/Sources/Sequencer/ChunkedProxy.swift index a76d0afaf1c109fd1ecde975b4bafaf82e019138..b43b429fdcfcadc2f025700f0bccde3e9a3d54a1 100644 --- a/sequencer/Sources/Sequencer/ChunkedProxy.swift +++ b/sequencer/Sources/Sequencer/ChunkedProxy.swift @@ -36,11 +36,18 @@ final class ChunkManager { /// fixed 960 until the viewer measures itself. private(set) var previewTargetWidth = 960 + /// Hard upper bound on proxy resolution: 1080p-wide. A proxy only has to be + /// sharp enough to edit against, not master from — and a 4K source is ~8× + /// the pixels (and cache bytes) of 1080p for detail an editing preview can't + /// use. (It also caps content that was cheaply up-scaled to 4K — e.g. 800×600 + /// gameplay blown up to 4K — back to a size that reflects its real detail.) + static let maxProxyWidth = 1920 + /// The width a fresh proxy for this media should reach — the preview target, - /// never upscaled past the source. + /// never upscaled past the source, and never above the 1080p cap. private func targetWidth(for media: MediaItem) -> Int { let native = media.width > 0 ? media.width : previewTargetWidth - return min(native, previewTargetWidth) + return min(native, previewTargetWidth, Self.maxProxyWidth) } /// Effective encode width at `level` for this media (even, ffmpeg-friendly). @@ -50,9 +57,28 @@ final class ChunkManager { return max(160, w - (w % 2)) } + /// Whether a chunk already on disk at width `built` should be re-encoded: + /// either it's below the current sharpness target (sharpen up toward it), or + /// it's above the hard 1080p cap (a legacy 4K proxy to shrink back down — the + /// cap is constant, so this converges and never churns on window resize). The + /// `attempted` guard stops a failed re-encode from looping. + private func needsReencode(built: Int, attempted: Int?, target: Int) -> Bool { + if built < target { return (attempted ?? 0) < target } + if built > Self.maxProxyWidth { return (attempted ?? built) > Self.maxProxyWidth } + return false + } + + /// A short "rescue" slice standing in for a chunk the playhead landed on + /// cold: only `dur` seconds starting `offset` into the chunk's grid slot + /// exist on disk (as `rNNNNNN.mov`). Session-only — never persisted; stale + /// slice files from a crash are purged by the launch reconcile walk. + struct Rescue { let offset: Double; let dur: Double; let width: Int } + private struct MediaState { var built: [Int: Int] = [:] // chunk index → effective width on disk var attempted: [Int: Int] = [:] // chunk index → width of the last attempt + var partial: [Int: Rescue] = [:] // chunk index → rescue slice on disk + var rescueAttempted: Set = [] var inFlight: Set = [] var failed: Set = [] var urgent: [Int] = [] @@ -79,6 +105,18 @@ final class ChunkManager { /// queue is retained, resuming where it left off. Main-thread only. private(set) var isPaused = false + /// Set when the owning document closes. In-flight builds run off-main and + /// their completions land back on main; a completion (via `pump`) reaches + /// `ctx`, which is `unowned` and may already be gone once the document is + /// torn down. `stopped` makes every ctx-touching entry point a no-op, so a + /// build finishing after close can't trap on a dangling context. + private var stopped = false + func stop() { + stopped = true + geometryPending?.cancel() + geometryPending = nil + } + /// Toggle proxy optimization on/off (driven by the status-bar readout). func setPaused(_ paused: Bool) { guard paused != isPaused else { return } @@ -110,9 +148,7 @@ final class ChunkManager { init() { NotificationCenter.default.addObserver( forName: .projectChanged, object: nil, queue: .main) { [weak self] _ in - guard let self else { return } - self.ensure(for: self.ctx.store.project) - self.updateDemand(force: true) + self?.scheduleRebuild() } // Hiding/focusing a track changes what to optimize first. NotificationCenter.default.addObserver( @@ -121,6 +157,26 @@ final class ChunkManager { } } + private var geometryPending: DispatchWorkItem? + /// Coalesce project-geometry rebuilds. During a continuous drag + /// `.projectChanged` fires ~60×/s; rebuilding the whole background queue each + /// time is pure waste — moving a clip in TIME doesn't change which source + /// chunks it needs. Debounce so the queue rebuilds once the edit settles. + /// (Initial load calls `ensure` directly via `startServices`, and playback + /// refreshes demand every tick, so nothing waits on this.) + private func scheduleRebuild() { + guard !stopped else { return } + geometryPending?.cancel() + let w = DispatchWorkItem { [weak self] in + guard let self, !self.stopped else { return } + self.geometryPending = nil + self.ensure(for: self.ctx.store.project) + self.updateDemand(force: true) + } + geometryPending = w + DispatchQueue.main.asyncAfter(deadline: .now() + 0.12, execute: w) + } + // MARK: - Paths private func chunksDir(_ key: String) -> URL { @@ -135,6 +191,9 @@ final class ChunkManager { private func chunkURL(key: String, index: Int) -> URL { chunksDir(key).appendingPathComponent(String(format: "c%06d.mov", index)) } + private func rescueURL(key: String, index: Int) -> URL { + chunksDir(key).appendingPathComponent(String(format: "r%06d.mov", index)) + } private func manifestURL(_ key: String) -> URL { chunksDir(key).appendingPathComponent("widths.json") } @@ -172,6 +231,15 @@ final class ChunkManager { var s = states[media.cacheKey] ?? MediaState() if !s.scanned { s.scanned = true + // Cold big network media starts the realtime ladder two rungs + // down: the first urgent (playhead) build must land in seconds, + // and a full-target encode of 4K source rarely does — the + // adaptive controller would only learn that AFTER the user + // stared at a placeholder. It climbs back once builds measure + // comfortably fast; background upgrades restore full quality. + if media.width >= 2560, Self.isNetworkPath(media.path) { + s.qualityIndex = 2 + } let widths = loadWidths(media.cacheKey) if let names = try? FileManager.default .contentsOfDirectory(atPath: chunksDir(media.cacheKey).path) { @@ -205,28 +273,113 @@ final class ChunkManager { guard s.urgent != wanted else { return } s.urgent = wanted states[media.cacheKey] = s + windowStarved = false // fresh urgency → re-attempt admission + pump() + } + + /// A live trim/slip drag is exposing this source moment at a clip edge — + /// start building its chunk (and the neighbor in the drag direction) + /// BEFORE mouse-up, so a newly extended range is covered by the time the + /// user plays across it. Cheap and self-deduping; safe to call per drag + /// tick. + func noteGestureExposure(media: MediaItem, sourceTime: Double, direction: Int) { + guard !stopped, media.duration > 0, !media.isAudio, !hasFullProxy(media) else { return } + var s = state(for: media) + let n = Self.chunkCount(duration: media.duration) + let i = min(n - 1, max(0, Self.chunkIndex(forSource: sourceTime))) + let wanted = [i, i + (direction < 0 ? -1 : 1)].filter { + $0 >= 0 && $0 < n && s.built[$0] == nil + && !s.inFlight.contains($0) && !s.failed.contains($0) + } + guard s.urgent != wanted else { return } + s.urgent = wanted + states[media.cacheKey] = s + windowStarved = false // fresh urgency → re-attempt admission pump() } /// Rebuild the background fill queue from the project: every chunk in - /// every clip's used source range, in order. + /// every clip's used source range. Cut heads first (the first chunk of + /// every clip is the landing pad for clip-to-clip navigation — a sliver + /// of the bytes for most of the "timeline feels instant" effect), then + /// everything else; both passes nearest-the-playhead first, so the fill + /// grows the working set outward instead of marching from t=0. func ensure(for project: ProjectModel) { + guard !stopped else { return } + let ph = ctx.playback.playhead for media in project.media { guard media.duration > 0, !media.isAudio, !hasFullProxy(media) else { continue } var s = state(for: media) let n = Self.chunkCount(duration: media.duration) - var order: [Int] = [] + var heads: [(i: Int, d: Double)] = [] + var rest: [(i: Int, d: Double)] = [] + var seen = Set() for clip in project.clips where clip.mediaId == media.id { let a = min(n - 1, Self.chunkIndex(forSource: clip.srcIn)) let b = min(n - 1, Self.chunkIndex(forSource: clip.srcIn + clip.duration - 0.001)) - for i in a...max(a, b) where !order.contains(i) { order.append(i) } + let d = abs(clip.start - ph) + for i in a...max(a, b) where seen.insert(i).inserted { + if i == a { heads.append((i, d)) } else { rest.append((i, d)) } + } } - s.background = order + heads.sort { $0.d < $1.d } + rest.sort { $0.d < $1.d } + s.background = heads.map(\.i) + rest.map(\.i) states[media.cacheKey] = s } + // Geometry changed: clips removed from the timeline may have freed + // evictable chunks, so a starved fill is worth one more attempt. + fillStarved = false + // The scan above is also what makes this document's chunks visible as + // eviction candidates. If the cache is over cap (launch reconcile ran + // before any document had scanned — nothing was evictable then), this + // is the moment eviction can actually see the cold chunks: re-check. + MediaPipeline.shared.evictIfNeeded() pump() } + // MARK: - Edit locus + + /// Timeline positions of recent edits, oldest first. Editors scrub and + /// re-play around where they're cutting, so chunks near these positions + /// build early and evict late. Fed by `Store` after each committed edit; + /// entries expire after ~15 minutes. + private var editLoci: [(time: Double, at: Date)] = [] + + func noteEdits(times: [Double]) { + guard !stopped, !times.isEmpty else { return } + let now = Date() + for t in times { + if let i = editLoci.firstIndex(where: { abs($0.time - t) < 30 }) { + editLoci[i] = (t, now) + } else { + editLoci.append((t, now)) + } + } + if editLoci.count > 8 { editLoci.removeFirst(editLoci.count - 8) } + updateDemand(force: true) + } + + /// Loci still fresh enough to matter. + private func activeEditLoci() -> [Double] { + let cutoff = Date().addingTimeInterval(-15 * 60) + editLoci.removeAll { $0.at < cutoff } + return editLoci.map(\.time) + } + + /// The timeline window the user can currently SEE (when zoomed in enough + /// to be meaningful) — scrubbing happens inside it. Set by TimelineView. + private func visibleWindow() -> ClosedRange? { + guard let r = ctx.session.visibleTimeRange, + r.upperBound - r.lowerBound <= 600 else { return nil } + return r + } + + /// Distance from a timeline interval to a point (0 when inside). + private static func dist(_ t: Double, _ lo: Double, _ hi: Double) -> Double { + t < lo ? lo - t : (t > hi ? t - hi : 0) + } + // MARK: - Prefetch demand (what to optimize first) private struct DemandKey: Hashable { let key: String; let index: Int } @@ -237,6 +390,9 @@ final class ChunkManager { /// build at the adaptive realtime quality so they land in time; everything /// else builds at the full preview-quality target. private var demandImminent: Set = [] + /// Uncovered chunks the playhead is sitting INSIDE right now, mapped to the + /// source time being shown — the trigger (and anchor) for rescue slices. + private var demandRescue: [DemandKey: Double] = [:] private var lastDemandPlayhead = -1e9 private var lastDemandSign = 0.0 @@ -249,14 +405,16 @@ final class ChunkManager { private static let imminentAhead = 45.0 /// Recompute the build order — the heart of "optimize the right thing first." - /// For every video clip near the playhead we score the proxy chunks its + /// For every video clip near an ANCHOR we score the proxy chunks its /// source range needs and sort them: coverage before sharpening, visible - /// (and focused) tracks before hidden, and nearer the playhead in the - /// playback direction before farther. Chunks outside the window fall through - /// to the whole-project background queue (`ensure`). Cheap; safe to call as - /// the playhead moves (self-throttled). + /// (and focused) tracks before hidden, and nearer the anchor before + /// farther. Anchors, hottest first: the playhead (direction-weighted), + /// recent edit sites, and the visible timeline window — the places the + /// user is most likely to play next. Chunks outside every window fall + /// through to the whole-project background queue (`ensure`). Cheap; safe + /// to call as the playhead moves (self-throttled). func updateDemand(force: Bool = false) { - guard ctx != nil else { return } + guard !stopped else { return } let ph = ctx.playback.playhead let sign = ctx.playback.rate < 0 ? -1.0 : 1.0 guard force || abs(ph - lastDemandPlayhead) > 1.5 || sign != lastDemandSign else { return } @@ -267,48 +425,97 @@ final class ChunkManager { let dir = sign let anyFocused = !ctx.session.focusedTracks.isEmpty + // Secondary anchors: recent edit sites, then the visible window. + // Their bias keeps them strictly behind playhead-window work but far + // ahead of the whole-project background fill. + struct Window { let lo: Double; let hi: Double; let anchor: Double; let bias: Double } + var windows = [Window(lo: ph - (dir > 0 ? Self.prefetchBehind : Self.prefetchAhead), + hi: ph + (dir > 0 ? Self.prefetchAhead : Self.prefetchBehind), + anchor: ph, bias: 0)] + for l in activeEditLoci() { + windows.append(Window(lo: l - 45, hi: l + 45, anchor: l, bias: 2_000)) + } + if let vis = visibleWindow() { + windows.append(Window(lo: vis.lowerBound, hi: vis.upperBound, + anchor: (vis.lowerBound + vis.upperBound) / 2, bias: 6_000)) + } + struct Cand { let key: DemandKey; let score: Double; let imminent: Bool } var cands: [Cand] = [] + var rescue: [DemandKey: Double] = [:] for clip in project.clips where clip.kind == .video { guard let media = project.media(clip.mediaId), media.duration > 0, !media.isAudio, !hasFullProxy(media) else { continue } let visible = !ctx.session.hiddenTracks.contains(clip.track) let focused = ctx.session.focusedTracks.contains(clip.track) - let lo = ph - (dir > 0 ? Self.prefetchBehind : Self.prefetchAhead) - let hi = ph + (dir > 0 ? Self.prefetchAhead : Self.prefetchBehind) - let a = max(clip.start, lo), b = min(clip.end, hi) - guard b > a else { continue } - let n = Self.chunkCount(duration: media.duration) - let s = state(for: media) - let target = targetWidth(for: media) - let srcA = clip.srcIn + (a - clip.start) * clip.speed - let srcB = clip.srcIn + (b - clip.start) * clip.speed - let ci = min(n - 1, Self.chunkIndex(forSource: min(srcA, srcB))) - let cj = min(n - 1, Self.chunkIndex(forSource: max(srcA, srcB) - 1e-6)) - for idx in ci...max(ci, cj) { - if (s.built[idx] ?? 0) >= target { continue } // already good enough - let covered = s.built[idx] != nil - // Timeline moment this chunk's content plays inside this clip. - let tl = clip.start - + (Double(idx) * Self.chunkSeconds - clip.srcIn) / max(1e-4, clip.speed) - let signed = (min(max(tl, a), b) - ph) * dir // >0 = ahead - var score = signed >= 0 ? signed : -signed * 4 // behind costs 4× - if covered { score += 10_000 } // coverage beats sharpening - if !visible { score += 100_000 } // hidden tracks last - else if anyFocused && !focused { score += 1_000 } // the enlarged pane first - let imminent = visible && !covered && signed >= 0 && signed < Self.imminentAhead - cands.append(Cand(key: DemandKey(key: media.cacheKey, index: idx), - score: score, imminent: imminent)) + var n = 0, target = 0 + var s: MediaState? + for w in windows { + let a = max(clip.start, w.lo), b = min(clip.end, w.hi) + guard b > a else { continue } + if s == nil { // lazy: only scan media that some window needs + s = state(for: media) + n = Self.chunkCount(duration: media.duration) + target = targetWidth(for: media) + } + guard let st = s else { continue } + let srcA = clip.srcIn + (a - clip.start) * clip.speed + let srcB = clip.srcIn + (b - clip.start) * clip.speed + let ci = min(n - 1, Self.chunkIndex(forSource: min(srcA, srcB))) + let cj = min(n - 1, Self.chunkIndex(forSource: max(srcA, srcB) - 1e-6)) + for idx in ci...max(ci, cj) { + if (st.built[idx] ?? 0) >= target { continue } // already good enough + let covered = st.built[idx] != nil + // Timeline interval this chunk's content plays inside this + // clip. Scoring by the INTERVAL (not the chunk's start + // moment) is what puts the chunk UNDER the playhead at + // score 0 — its start is always "behind", and treating it + // that way made the builder prefetch a dozen ahead-chunks + // while the user stared at "Loading Media…" on the frame + // they'd actually landed on. + let t0 = clip.start + + (Double(idx) * Self.chunkSeconds - clip.srcIn) / max(1e-4, clip.speed) + let t1 = clip.start + + (Double(idx + 1) * Self.chunkSeconds - clip.srcIn) / max(1e-4, clip.speed) + let cLo = max(min(t0, t1), a), cHi = min(max(t0, t1), b) + let isPlayhead = w.bias == 0 + var score: Double + var under = false + if isPlayhead { + let ahead = dir > 0 ? cLo - ph : ph - cHi // >0 = strictly ahead + let behind = dir > 0 ? ph - cHi : cLo - ph // >0 = strictly behind + under = ahead <= 0 && behind <= 0 // playing right now + score = ahead > 0 ? ahead : (behind > 0 ? behind * 4 : 0) + } else { + score = w.bias + max(0, max(cLo - w.anchor, w.anchor - cHi)) + } + if covered { score += 10_000 } // coverage beats sharpening + if !visible { score += 100_000 } // hidden tracks last + else if anyFocused && !focused { score += 1_000 } // the enlarged pane first + let ahead = dir > 0 ? cLo - ph : ph - cHi + let imminent = isPlayhead && visible && !covered + && (under || (ahead >= 0 && ahead < Self.imminentAhead)) + let dk = DemandKey(key: media.cacheKey, index: idx) + if isPlayhead, visible, under, !covered { + rescue[dk] = clip.srcIn + (ph - clip.start) * clip.speed + } + cands.append(Cand(key: dk, score: score, imminent: imminent)) + } } } cands.sort { $0.score < $1.score } + let oldDemand = demand demand.removeAll(keepingCapacity: true) demandImminent.removeAll(keepingCapacity: true) + demandRescue = rescue var seen = Set() for c in cands where seen.insert(c.key).inserted { demand.append(c.key) if c.imminent { demandImminent.insert(c.key) } } + // The window moved: what starved before may fit now (different chunks, + // and colder ones may have fallen out of the protected radius). + if demand != oldDemand { windowStarved = false } pump() } @@ -318,7 +525,13 @@ final class ChunkManager { if media.isAudio { return true } // audio plays the original directly if hasFullProxy(media) { return true } let s = state(for: media) - return s.built[Self.chunkIndex(forSource: sourceTime)] != nil + let i = Self.chunkIndex(forSource: sourceTime) + if s.built[i] != nil { return true } + if let p = s.partial[i] { // a rescue slice covers only part of the slot + let t = sourceTime - Double(i) * Self.chunkSeconds + return t >= p.offset - 0.05 && t <= p.offset + p.dur - 0.05 + } + return false } /// Last known answer; unknown kicks the async composition build (which @@ -330,6 +543,196 @@ final class ChunkManager { return false } + // MARK: - Cache budget (admission + eviction support) + + /// Set when a build was skipped because the cache is at its cap and + /// nothing colder could be evicted to make room. `window` covers the + /// playhead prefetch; `fill` the whole-project background queue. Cleared + /// whenever the budget or the demand changes. + private var windowStarved = false + private var fillStarved = false + /// The cache is full and optimization is deliberately not building + /// everything — drives the status-bar messaging. + var budgetStarved: Bool { windowStarved || fillStarved } + + /// The global cache budget moved (reconcile finished, eviction freed + /// space, cap changed): try again from a clean slate. + func budgetChanged() { + guard !stopped else { return } + windowStarved = false + fillStarved = false + pump() + } + + /// `SEQ_BUILDLOG=1`: log every build START and admission failure — the + /// queue's decisions, not just its results. For chasing "why isn't chunk + /// X building" (success completions are otherwise silent). + static let buildLog = ProcessInfo.processInfo.environment["SEQ_BUILDLOG"] != nil + + /// Global correction factor: measured chunk bytes ÷ raw estimate, EMA'd. + /// Starts at 1 (the raw model is a ProRes-proxy ballpark) and converges on + /// the actual footage within a few chunks. + private static var chunkRateEMA = 1.0 + + /// Ballpark bytes for a chunk at `width` before correction: ProRes proxy + /// ≈ 0.09 bytes per pixel per frame, plus PCM audio when present. + /// `seconds` overrides the encoded duration (rescue slices). + private func rawChunkEstimate(media: MediaItem, width: Int, index: Int, + seconds: Double? = nil) -> Double { + let dur = seconds ?? min(Self.chunkSeconds, + max(1, media.duration - Double(index) * Self.chunkSeconds)) + let aspect = (media.width > 0 && media.height > 0) + ? Double(media.height) / Double(media.width) : 9.0 / 16 + let fps = min(60.0, max(10.0, media.fps)) + var b = 0.09 * Double(width) * (Double(width) * aspect) * fps * dur + if media.hasAudio { b += 200_000 * dur } + return b + } + + /// Corrected + safety-margined estimate the admission gate reserves. + private func estimateChunkBytes(media: MediaItem, width: Int, index: Int, + seconds: Double? = nil) -> Int64 { + Int64(rawChunkEstimate(media: media, width: width, index: index, seconds: seconds) + * Self.chunkRateEMA * 1.3) + } + + /// A cold, already-built chunk the global cache may delete to make room. + struct EvictionCandidate { + let key: String + let index: Int + let url: URL + /// Timeline seconds from this document's playhead to the nearest use + /// of the chunk; 1e12 when no clip uses it at all (media edited off + /// the timeline — the coldest bytes there are). + let coldness: Double + } + + /// Timeline distance within which built chunks are HARD-protected: the + /// frames playback will hit imminently. Everything beyond is merely + /// ranked by distance — evictable coldest-first — so when the working set + /// alone exceeds the cap, it shrinks to fit instead of wedging eviction. + private static let hardProtectRadius = 60.0 + + /// Every built chunk of this document that eviction MAY delete, scored by + /// coldness (timeline distance from the playhead; off-timeline chunks are + /// coldest). Hard-excluded: the demand window, anything mid-build or + /// urgent, and chunks within `hardProtectRadius` of the playhead — which + /// for a background window is exactly its resume neighborhood. + func evictionCandidates() -> [EvictionCandidate] { + guard !stopped else { return [] } + let project = ctx.store.project + let ph = ctx.playback.playhead + var protected: Set = Set(demand) + for (key, s) in states { + for i in s.inFlight { protected.insert(DemandKey(key: key, index: i)) } + for i in s.urgent { protected.insert(DemandKey(key: key, index: i)) } + } + // Score every chunk any clip uses by its distance from the nearest + // heat anchor (playhead, then recent edit sites and the visible + // window at a penalty so the playhead wins ties); hard-protect only + // the playhead-imminent ones. Cut heads read as 4× closer than they + // are, so the "instant timeline" landing pads die last. + let loci = activeEditLoci() + let vis = visibleWindow() + var distance: [DemandKey: Double] = [:] + var heads: Set = [] + for clip in project.clips where clip.kind == .video { + guard let media = project.media(clip.mediaId), media.duration > 0, + !media.isAudio else { continue } + let n = Self.chunkCount(duration: media.duration) + let a = min(n - 1, Self.chunkIndex(forSource: clip.srcIn)) + let b = min(n - 1, Self.chunkIndex(forSource: clip.srcIn + clip.duration - 0.001)) + heads.insert(DemandKey(key: media.cacheKey, index: a)) + for i in a...max(a, b) { + let t0 = clip.start + (Double(i) * Self.chunkSeconds - clip.srcIn) / max(1e-4, clip.speed) + let t1 = clip.start + (Double(i + 1) * Self.chunkSeconds - clip.srcIn) / max(1e-4, clip.speed) + let lo = max(clip.start, min(t0, t1)), hi = min(clip.end, max(t0, t1)) + var d = Self.dist(ph, lo, hi) + for l in loci { d = min(d, Self.dist(l, lo, hi) + 60) } + if let vis { // gap between the chunk's interval and the visible range + let gap = max(0, max(lo - vis.upperBound, vis.lowerBound - hi)) + d = min(d, gap + 120) + } + let dk = DemandKey(key: media.cacheKey, index: i) + distance[dk] = min(distance[dk] ?? .infinity, d) + if Self.dist(ph, lo, hi) < Self.hardProtectRadius { protected.insert(dk) } + } + } + var out: [EvictionCandidate] = [] + var builtTotal = 0 + for (key, s) in states { + builtTotal += s.built.count + for i in s.built.keys { + let dk = DemandKey(key: key, index: i) + guard !protected.contains(dk), !s.inFlight.contains(i) else { continue } + var coldness = distance[dk] ?? 1e12 + if heads.contains(dk) { coldness *= 0.25 } // landing pads die last + out.append(EvictionCandidate(key: key, index: i, + url: chunkURL(key: key, index: i), + coldness: coldness)) + } + // Rescue slices are evictable like any chunk (the playhead radius + // protects the live one); they just live in an rNNNNNN.mov file. + for i in s.partial.keys where s.built[i] == nil { + let dk = DemandKey(key: key, index: i) + guard !protected.contains(dk), !s.inFlight.contains(i) else { continue } + out.append(EvictionCandidate(key: key, index: i, + url: rescueURL(key: key, index: i), + coldness: distance[dk] ?? 1e12)) + } + } + NSLog("[cache] candidates: %d of %d built chunks (%d protected, playhead %.0fs)", + out.count, builtTotal, protected.count, ph) + return out + } + + /// The global cache deleted these chunk files: drop them from state and + /// bump the version so compositions rebuild onto the original-file + /// fallback instead of pointing at deleted movs. + func noteEvicted(key: String, indices: [Int]) { + guard var s = states[key] else { return } + var changed = false + for i in indices { + if s.built.removeValue(forKey: i) != nil { + s.attempted.removeValue(forKey: i) + changed = true + } + if s.partial.removeValue(forKey: i) != nil { changed = true } + } + guard changed else { return } + s.version += 1 + states[key] = s + persistWidths(key: key) + } + + /// Chunk states for the timeline's optimization strip — one cheap value + /// snapshot per media per strip rebuild. nil when the media hasn't been + /// scanned yet (unknown; the strip paints it as unoptimized until the + /// initial `ensure` pass scans it). + struct StripSnapshot { + let n: Int + let target: Int + let built: [Int: Int] + let inFlight: Set + let partial: Set + let failed: Set + let fullProxy: Bool + } + + func stripSnapshot(media: MediaItem) -> StripSnapshot? { + guard media.duration > 0, !media.isAudio else { return nil } + if hasFullProxy(media) { + return StripSnapshot(n: 1, target: 0, built: [:], inFlight: [], + partial: [], failed: [], fullProxy: true) + } + guard let s = states[media.cacheKey], s.scanned else { return nil } + return StripSnapshot(n: Self.chunkCount(duration: media.duration), + target: targetWidth(for: media), + built: s.built, inFlight: s.inFlight, + partial: Set(s.partial.keys), failed: s.failed, + fullProxy: false) + } + /// (building now, waiting in queue) across all media — for the status bar. /// A chunk counts as queued while it's either missing OR still below the /// preview-quality target (i.e. an upgrade is pending). @@ -348,8 +751,14 @@ final class ChunkManager { /// Per-chunk proxy width right now (players record this at item-swap time /// to judge whether a later swap upgrades the frame under the playhead). + /// Rescue slices report as width 1: "something is there", and any full + /// build over them reads as a strict upgrade so the player adopts it. func builtWidths(media: MediaItem) -> [Int: Int] { - state(for: media).built + let s = state(for: media) + guard !s.partial.isEmpty else { return s.built } + var w = s.built + for i in s.partial.keys where w[i] == nil { w[i] = 1 } + return w } func builtChunkURL(media: MediaItem, index: Int) -> URL? { @@ -357,53 +766,181 @@ final class ChunkManager { ? chunkURL(key: media.cacheKey, index: index) : nil } + /// A playable (file, time-in-file) pair that shows `sourceTime` — the + /// sharpest thing on disk right now: legacy full proxy, built chunk, + /// covering rescue slice, or the original when it's known playable. nil + /// when this frame genuinely can't be decoded yet. Feeds the RAM frame + /// cache's stand-in decodes; main-thread. + func frameSource(media: MediaItem, sourceTime: Double) -> (url: URL, time: Double)? { + guard !media.isAudio else { return nil } + if let proxy = MediaPipeline.shared.proxyURL(for: media) { + return (proxy, sourceTime) + } + let s = state(for: media) + let i = Self.chunkIndex(forSource: sourceTime) + let local = sourceTime - Double(i) * Self.chunkSeconds + if s.built[i] != nil { + return (chunkURL(key: media.cacheKey, index: i), local) + } + if let p = s.partial[i], local >= p.offset, local <= p.offset + p.dur - 0.05 { + return (rescueURL(key: media.cacheKey, index: i), local - p.offset) + } + if s.originalPlayable == true { return (media.url, sourceTime) } + return nil + } + + /// Rough bytes this project needs to be FULLY optimized — every chunk any + /// clip uses, at the current preview target width — and how many bytes its + /// media already have on disk. Estimates use the same corrected model as + /// the admission gate (sans safety margin); `built` is a real disk walk of + /// each media's chunk dir, so call this on demand (Settings), not per frame. + func optimizeEstimate(for project: ProjectModel) -> (total: Int64, built: Int64) { + var total: Int64 = 0, built: Int64 = 0 + for media in project.media where !media.isAudio && media.duration > 0 { + if hasFullProxy(media), let proxy = MediaPipeline.shared.proxyURL(for: media) { + let sz = (try? FileManager.default.attributesOfItem(atPath: proxy.path)[.size] + as? NSNumber)?.int64Value ?? 0 + total += sz + built += sz + continue + } + let n = Self.chunkCount(duration: media.duration) + var used = Set() + for clip in project.clips where clip.mediaId == media.id && clip.kind == .video { + let a = min(n - 1, Self.chunkIndex(forSource: clip.srcIn)) + let b = min(n - 1, Self.chunkIndex(forSource: clip.srcIn + clip.duration - 0.001)) + for i in a...max(a, b) { used.insert(i) } + } + guard !used.isEmpty else { continue } + let target = targetWidth(for: media) + for i in used { + total += Int64(rawChunkEstimate(media: media, width: target, index: i) + * Self.chunkRateEMA) + } + built += MediaPipeline.directorySize(chunksDir(media.cacheKey)) + } + return (total, built) + } + + /// One-line explanation of why the frame at `sourceTime` might not be + /// showing — the [miss] log's payload. Every "Loading Media…" spinner the + /// viewer escalates to gets one of these, so a spinner sighting is + /// diagnosable from the log instead of a shrug: was the chunk missing + /// entirely (and did the demand scorer even know about it?), mid-build, + /// built-but-not-yet-stitched, or built with the player just late? + func missDiagnosis(media: MediaItem, sourceTime: Double) -> String { + guard !media.isAudio else { return "audio" } + guard !hasFullProxy(media) else { return "full-proxy player-late" } + let s = state(for: media) + let i = Self.chunkIndex(forSource: sourceTime) + var bits = ["\(media.cacheKey.prefix(8))#\(i)"] + if let w = s.built[i] { + bits.append("built(w=\(w))") + bits.append(s.compositionVersion != s.version ? "composition-lag" + : "player-late") + } else if let p = s.partial[i] { + let t = sourceTime - Double(i) * Self.chunkSeconds + bits.append(String(format: "rescue(%.0fs@%.0fs t=%.1f)", p.dur, p.offset, t)) + bits.append(s.compositionVersion != s.version ? "composition-lag" + : "player-late") + } else if s.inFlight.contains(i) { + bits.append("building") + } else if s.failed.contains(i) { + bits.append("build-failed") + } else { + let dk = DemandKey(key: media.cacheKey, index: i) + if let rank = demand.firstIndex(of: dk) { + bits.append("queued(demand#\(rank)\(demandImminent.contains(dk) ? " imminent" : ""))") + } else if s.background.contains(i) { + bits.append("queued(background)") // demand window missed it + } else { + bits.append("NOT-QUEUED") // heuristic gap — the bad one + } + } + if budgetStarved { bits.append("budget-starved") } + if isPaused { bits.append("opt-paused") } + if s.originalPlayable == nil { bits.append("orig-unknown") } + else if s.originalPlayable == false { bits.append("orig-unplayable") } + return bits.joined(separator: " ") + } + + /// The proxy chunk covering `sourceTime` tried to build and hard-failed + /// (both encoders) with nothing usable — the viewer surfaces this instead of + /// spinning "processing…" forever. + func buildFailed(media: MediaItem, sourceTime: Double) -> Bool { + if media.isAudio || hasFullProxy(media) { return false } + return state(for: media).failed.contains(Self.chunkIndex(forSource: sourceTime)) + } + // MARK: - Build queue + /// Which budget tier a job builds for. `window` jobs (playhead prefetch, + /// urgent coverage) may evict cold chunks of open documents to make room; + /// `fill` jobs (whole-project background) may only consume free budget or + /// space freed from closed projects — never evict another chunk. That + /// asymmetry is what makes the cache converge instead of thrash: a build + /// can only displace bytes strictly colder than itself. + private enum JobTier { case window, fill } + /// Next chunk to build, in priority order: /// 0. urgent — coverage the playhead needs NOW, at any quality; /// 1. missing — background chunks not yet built at all (coverage first); /// 2. upgrade — background chunks built below the preview-quality target. /// Coverage always beats sharpening, so playback never stalls waiting on a /// quality upgrade of a frame that's already visible. - private func nextJob() -> (media: MediaItem, index: Int, urgent: Bool)? { + private func nextJob(frontDoc: Bool) -> (media: MediaItem, index: Int, urgent: Bool, tier: JobTier)? { + // Only the frontmost document builds proxies. macOS state restoration + // reopens every previously-open project on launch; if each one built its + // proxies, they'd transcode their full ProRes sets in parallel. Every open + // project's media counts as "in use", so eviction can't reclaim any of it — + // the shared cache blows past its cap and fills the disk (the reported bug). + // A background project builds nothing until you switch to it (its window + // becoming main re-pumps it — see windowDidBecomeMain); the document under + // the playhead is always the front one, so playback is unaffected. + guard frontDoc else { return nil } // 1. Prefetch demand — already ordered best-first (visible/focused, near, // coverage before sharpening). Coverage rides the adaptive realtime // quality when imminent; an upgrade goes for the full target. - for dk in demand { - guard let media = mediaByKey[dk.key] else { continue } - let s = states[dk.key] ?? state(for: media) - guard !s.inFlight.contains(dk.index), !s.failed.contains(dk.index) else { continue } - let target = targetWidth(for: media) - if let have = s.built[dk.index] { - if have < target, (s.attempted[dk.index] ?? 0) < target { - return (media, dk.index, false) // upgrade → target + if !windowStarved { + for dk in demand { + guard let media = mediaByKey[dk.key] else { continue } + let s = states[dk.key] ?? state(for: media) + guard !s.inFlight.contains(dk.index), !s.failed.contains(dk.index) else { continue } + let target = targetWidth(for: media) + if let have = s.built[dk.index] { + if needsReencode(built: have, attempted: s.attempted[dk.index], target: target) { + return (media, dk.index, false, .window) // sharpen or shrink → target + } + } else { + return (media, dk.index, demandImminent.contains(dk), .window) // coverage } - } else { - return (media, dk.index, demandImminent.contains(dk)) // coverage } - } - // 2. Legacy urgent (headless `want`) then the whole-project background - // fill for chunks off-screen of the prefetch window. - for (key, s) in states { - guard let media = mediaByKey[key] else { continue } - for i in s.urgent where s.built[i] == nil - && !s.inFlight.contains(i) && !s.failed.contains(i) { - return (media, i, true) + // 2. Legacy urgent (headless `want`). + for (key, s) in states { + guard let media = mediaByKey[key] else { continue } + for i in s.urgent where s.built[i] == nil + && !s.inFlight.contains(i) && !s.failed.contains(i) { + return (media, i, true, .window) + } } } + // 3. Whole-project background fill for chunks off-screen of the + // prefetch window — the tier that stops when the cache is full. + guard !fillStarved else { return nil } for (key, s) in states { guard let media = mediaByKey[key] else { continue } for i in s.background where s.built[i] == nil && !s.inFlight.contains(i) && !s.failed.contains(i) { - return (media, i, false) + return (media, i, false, .fill) } } for (key, s) in states { guard let media = mediaByKey[key] else { continue } let target = targetWidth(for: media) for i in s.background where !s.inFlight.contains(i) { - if let have = s.built[i], have < target, (s.attempted[i] ?? 0) < target { - return (media, i, false) + if let have = s.built[i], + needsReencode(built: have, attempted: s.attempted[i], target: target) { + return (media, i, false, .fill) } } } @@ -411,40 +948,161 @@ final class ChunkManager { } private func pump() { + guard !stopped else { return } // document closing — don't touch ctx // Paused stops idle background fill, but playback still optimizes the // chunks it's about to need. + // Only the front document runs its whole-project background fill (see + // nextJob) — this is the guard that stops N restored projects transcoding + // their full proxy sets in parallel and overflowing the cache. + let frontDoc = DocumentContext.current === ctx while (!isPaused || ctx.playback.isPlaying), - activeBuilds < maxBuilds, let job = nextJob() { - // During playback keep one slot free for urgent coverage so a slow - // background quality-upgrade can't stall the frames being played. - if !job.urgent, ctx.playback.isPlaying, activeBuilds >= maxBuilds - 1 { break } - let (media, index, urgent) = job + activeBuilds < maxBuilds + 1, let job = nextJob(frontDoc: frontDoc) { + // URGENT coverage (the playhead just landed on/near this chunk) may + // take one OVERFLOW slot: a couple of minutes-long 4K background + // encodes must never wall off the frame the user is looking at — + // that wait was the last reproducible "Loading Media…" spinner. + // Everything else respects maxBuilds, and during playback keeps one + // slot free for urgent coverage on top. + if !job.urgent { + if activeBuilds >= maxBuilds { break } + if ctx.playback.isPlaying, activeBuilds >= maxBuilds - 1 { break } + } + let (media, index, urgent, tier) = job + // RESCUE: the playhead is sitting on this uncovered chunk right + // now. A full 30-second encode makes the user wait for content + // they're already staring at — so first land a short slice + // starting AT the playhead (read-bound NAS sources scale with + // encoded seconds, so this is fast even when quality drops + // aren't), then immediately re-queue the full chunk behind it. + var slice: (offset: Double, dur: Double)? = nil + let dk = DemandKey(key: media.cacheKey, index: index) + if urgent, let st = states[media.cacheKey], st.built[index] == nil, + st.partial[index] == nil, !st.rescueAttempted.contains(index), + let srcT = demandRescue[dk] { + let chunkStart = Double(index) * Self.chunkSeconds + let content = min(Self.chunkSeconds, media.duration - chunkStart) + let offset = min(max(0, (srcT - chunkStart - 1).rounded(.down)), + max(0, content - 2)) + let dur = min(Self.rescueSeconds, content - offset) + // Only worth two encodes when the slice is a real shortcut. + if dur >= 4, dur <= content - offset, dur < content * 0.7 { + slice = (offset, dur) + } + } // Urgent builds ride the adaptive realtime level; background builds - // go for the full preview-quality target. - let level = urgent ? (states[media.cacheKey]?.qualityIndex ?? 0) : 0 + // go for the full preview-quality target. A rescue slice drops one + // more rung — landing NOW is its whole purpose. + var level = urgent ? (states[media.cacheKey]?.qualityIndex ?? 0) : 0 + if slice != nil { level = min(Self.qualities.count - 1, level + 1) } let width = buildWidth(level: level, media: media) let fpsDiv = Self.qualities[min(max(0, level), Self.qualities.count - 1)].fpsDivisor let fps = max(1, Int((media.fps / Double(fpsDiv)).rounded())) + // Admission gate: the cap is enforced BEFORE bytes hit the disk. + // No reservation, no build — first try to make room by evicting + // strictly-colder bytes (closed projects for any tier; open + // documents' cold chunks only for window builds), and if nothing + // colder exists, this tier starves until the budget changes. + let est = estimateChunkBytes(media: media, width: width, index: index, + seconds: slice?.dur) + let ceiling = tier == .window ? MediaPipeline.shared.maxCacheBytes + : MediaPipeline.shared.lowWatermarkBytes + if !MediaPipeline.shared.tryReserve(bytes: est, upTo: ceiling) { + if Self.buildLog { + SeqLog.log("[cache] admission blocked %@#%d est=%.0fMB tier=%@", + String(media.cacheKey.prefix(8)), index, + Double(est) / 1e6, tier == .window ? "window" : "fill") + } + // Try to make room by evicting strictly-colder bytes: closed + // projects' dirs for any tier; open documents' cold chunks + // only for window builds (fill must never displace a chunk — + // it stops at the watermark instead, which is what keeps + // fill and eviction from fighting over the same bytes). + MediaPipeline.shared.evictToFit(need: est, openDocChunks: tier == .window, + upTo: ceiling) { [weak self] ok in + guard let self, !self.stopped, !ok else { return } + // Eviction couldn't free enough (or one is already running + // — resolved via budgetChanged when it lands): starve the + // tier so pump stops retrying until the budget moves. + if tier == .window { self.windowStarved = true } + else { self.fillStarved = true } + NotificationCenter.default.post(name: .mediaStatusChanged, object: nil) + } + break + } states[media.cacheKey]?.inFlight.insert(index) - states[media.cacheKey]?.attempted[index] = width + if slice == nil { + states[media.cacheKey]?.attempted[index] = width + } else { + states[media.cacheKey]?.rescueAttempted.insert(index) + } activeBuilds += 1 + if Self.buildLog { + SeqLog.log("[cache] start %@#%d w=%d urgent=%d tier=%@%@", + String(media.cacheKey.prefix(8)), index, width, urgent ? 1 : 0, + tier == .window ? "window" : "fill", + slice != nil ? " rescue" : "") + } DispatchQueue.global(qos: .userInitiated).async { [self] in - let r = buildChunk(media: media, index: index, width: width, fps: fps) + let r = buildChunk(media: media, index: index, width: width, fps: fps, + slice: slice) DispatchQueue.main.async { + if !r.ok { + SeqLog.log("[cache] build FAILED %@#%d w=%d%@", + String(media.cacheKey.prefix(8)), index, width, + slice != nil ? " (rescue)" : "") + } + MediaPipeline.shared.commitBuild( + reserved: est, delta: r.ok ? r.newBytes - r.oldBytes : 0) + if r.ok, r.newBytes > 0, slice == nil { + // Fold the measured size into the estimator (clamped so + // one weird chunk can't poison admissions). + let raw = self.rawChunkEstimate(media: media, width: width, index: index) + if raw > 0 { + let ratio = Double(r.newBytes) / raw + Self.chunkRateEMA = min(10, max(0.1, + Self.chunkRateEMA * 0.7 + ratio * 0.3)) + } + } self.activeBuilds -= 1 var s = self.states[media.cacheKey] ?? MediaState() s.inFlight.remove(index) - if r.ok { + if r.ok, let slice { + // Slice landed: cover the playhead NOW; the chunk still + // reads as unbuilt so the full encode queues right behind. + s.partial[index] = Rescue(offset: slice.offset, dur: r.dur, + width: width) + s.failed.remove(index) + s.version += 1 + SeqLog.log("[cache] rescue %@#%d %.0fs@%.0fs w=%d in %.1fs", + String(media.cacheKey.prefix(8)), index, r.dur, + slice.offset, width, r.wall) + } else if r.ok { s.built[index] = width s.failed.remove(index) s.version += 1 - } else if s.built[index] == nil { + // The full chunk supersedes any rescue slice under it. + if s.partial.removeValue(forKey: index) != nil { + let rURL = self.rescueURL(key: media.cacheKey, index: index) + DispatchQueue.global(qos: .utility).async { + let sz = (try? FileManager.default.attributesOfItem( + atPath: rURL.path)[.size] as? NSNumber)?.int64Value ?? 0 + try? FileManager.default.removeItem(at: rURL) + if sz > 0 { + DispatchQueue.main.async { + MediaPipeline.shared.noteBytesAdded(-sz) + } + } + } + } + } else if s.built[index] == nil, slice == nil { // Only a hard failure when we have NOTHING; a failed // upgrade just keeps the existing lower-quality chunk. + // (A failed rescue is not a failure — the full build + // is still queued and gets its own attempt.) s.failed.insert(index) } self.states[media.cacheKey] = s - if r.ok { + if r.ok, slice == nil { self.persistWidths(key: media.cacheKey) // Only realtime (urgent) builds inform the realtime // controller — a slow, quality-first background build @@ -462,9 +1120,19 @@ final class ChunkManager { } } - struct BuildResult { var ok: Bool; var wall: Double; var dur: Double; var isNetwork: Bool } + struct BuildResult { + var ok: Bool; var wall: Double; var dur: Double; var isNetwork: Bool + /// Bytes of the finished chunk file / of the file it replaced (an + /// upgrade re-encode) — the ledger records the difference. + var newBytes: Int64 = 0; var oldBytes: Int64 = 0 + } - private func buildChunk(media: MediaItem, index: Int, width: Int, fps: Int) -> BuildResult { + /// Seconds of content a rescue slice encodes — enough to watch while the + /// full chunk builds behind it, small enough to land in a few seconds. + private static let rescueSeconds = 10.0 + + private func buildChunk(media: MediaItem, index: Int, width: Int, fps: Int, + slice: (offset: Double, dur: Double)? = nil) -> BuildResult { let isNet = Self.isNetworkPath(media.path) func fail(_ wall: Double = 0, _ dur: Double = 0) -> BuildResult { BuildResult(ok: false, wall: wall, dur: dur, isNetwork: isNet) @@ -472,11 +1140,14 @@ final class ChunkManager { guard let ffmpeg = MediaPipeline.findExecutable("ffmpeg") else { return fail() } let dir = chunksDir(media.cacheKey) try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) - let final = chunkURL(key: media.cacheKey, index: index) - let tmp = dir.appendingPathComponent(String(format: ".c%06d.partial.mov", index)) + let final = slice == nil ? chunkURL(key: media.cacheKey, index: index) + : rescueURL(key: media.cacheKey, index: index) + let tmp = dir.appendingPathComponent(String( + format: slice == nil ? ".c%06d.partial.mov" : ".r%06d.partial.mov", index)) try? FileManager.default.removeItem(at: tmp) - let start = Double(index) * Self.chunkSeconds - let dur = min(Self.chunkSeconds, media.duration - start) + let start = Double(index) * Self.chunkSeconds + (slice?.offset ?? 0) + var dur = min(Self.chunkSeconds - (slice?.offset ?? 0), media.duration - start) + if let slice { dur = min(dur, slice.dur) } guard dur > 0.01 else { return fail() } func args(encoder: String) -> [String] { @@ -485,7 +1156,12 @@ final class ChunkManager { "-i", media.path, "-t", String(format: "%.3f", dur), "-map", "0:v:0", - "-vf", "scale='min(\(width),iw)':-2,fps=\(fps)", + // Lanczos downscale: swscale's default (bicubic) softens the + // hard edges of up-scaled/pixel-art content into an annoying + // blur; lanczos keeps the downscaled proxy crisp (not + // nearest-neighbour "pixely", just sharp) — which matters more + // than resolution for editing legibility. + "-vf", "scale=w='min(\(width),iw)':h=-2:flags=lanczos,fps=\(fps)", "-c:v", encoder, "-profile:v", "proxy"] if media.hasAudio { a += ["-map", "0:a:0", "-c:a", "pcm_s16le"] } a.append(tmp.path) @@ -498,10 +1174,16 @@ final class ChunkManager { } let wall = Date().timeIntervalSince(t0) if res.exitCode == 0 { + func size(_ url: URL) -> Int64 { + (try? FileManager.default.attributesOfItem(atPath: url.path)[.size] + as? NSNumber)?.int64Value ?? 0 + } + let newBytes = size(tmp), oldBytes = size(final) try? FileManager.default.removeItem(at: final) do { try FileManager.default.moveItem(at: tmp, to: final) } catch { return fail(wall, dur) } - return BuildResult(ok: true, wall: wall, dur: dur, isNetwork: isNet) + return BuildResult(ok: true, wall: wall, dur: dur, isNetwork: isNet, + newBytes: newBytes, oldBytes: oldBytes) } try? FileManager.default.removeItem(at: tmp) return fail(wall, dur) @@ -611,16 +1293,30 @@ final class ChunkManager { return (AVMutableComposition(), -2) } + /// One stitched piece of a media's composition: a full chunk (offset 0, + /// dur nil) or a rescue slice sitting `offset` seconds into its grid slot. + private struct CompPart { + let url: URL + let offset: Double + let dur: Double? + } + private func kickCompositionBuild(media: MediaItem) { let key = media.cacheKey guard !compBuilding.contains(key) else { return } compBuilding.insert(key) let s = state(for: media) let version = s.version - let chunkURLs = Dictionary(uniqueKeysWithValues: - s.built.keys.map { ($0, chunkURL(key: key, index: $0)) }) + var parts: [Int: CompPart] = [:] + for i in s.built.keys { + parts[i] = CompPart(url: chunkURL(key: key, index: i), offset: 0, dur: nil) + } + for (i, p) in s.partial where parts[i] == nil { + parts[i] = CompPart(url: rescueURL(key: key, index: i), + offset: p.offset, dur: p.dur) + } Task.detached(priority: .userInitiated) { - let (comp, playable) = await Self.assemble(media: media, chunkURLs: chunkURLs) + let (comp, playable) = await Self.assemble(media: media, parts: parts) await MainActor.run { [self] in self.compBuilding.remove(key) var s = self.states[key] ?? MediaState() @@ -634,8 +1330,24 @@ final class ChunkManager { } } + /// A chunk asset's tracks, loaded and ready to insert. `asset` is what + /// keeps the tracks alive: an AVAssetTrack does NOT retain its asset, and + /// inserting a track whose asset has been deallocated fails with + /// -11800/-12780 — silently under `try?`, leaving a black GAP in the + /// composition for a chunk that's perfectly healthy on disk. (Bit us for + /// real: the task-group refactor returned bare tracks, and whether a slot + /// went black depended on autorelease timing.) + private struct LoadedPart { + let index: Int + let asset: AVURLAsset + let v: AVAssetTrack + let a: AVAssetTrack? + let duration: CMTime + let offset: Double + } + private static func assemble(media: MediaItem, - chunkURLs: [Int: URL]) async -> (AVComposition, Bool) { + parts: [Int: CompPart]) async -> (AVComposition, Bool) { let comp = AVMutableComposition() guard let vTrack = comp.addMutableTrack( withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) @@ -647,6 +1359,51 @@ final class ChunkManager { let origV = try? await original.loadTracks(withMediaType: .video).first let origA = try? await original.loadTracks(withMediaType: .audio).first + // Load every chunk asset's tracks CONCURRENTLY (bounded), then insert + // in order. The old one-await-per-chunk loop made a media with + // hundreds of built chunks take seconds to reassemble — and a + // reassembly runs every time a chunk lands, right when the user is + // waiting to see it. + let wantAudio = aTrack != nil + var loaded: [Int: LoadedPart] = [:] + await withTaskGroup(of: LoadedPart?.self) { group in + var pending = Array(parts).sorted { $0.key < $1.key }[...] + var inFlight = 0 + func addNext() { + guard let (i, part) = pending.first else { return } + pending = pending.dropFirst() + inFlight += 1 + group.addTask { + let chunk = AVURLAsset(url: part.url) + guard let v = try? await chunk.loadTracks(withMediaType: .video).first + else { return nil } + let d = (try? await chunk.load(.duration)) ?? .zero + let a = wantAudio + ? try? await chunk.loadTracks(withMediaType: .audio).first : nil + return LoadedPart(index: i, asset: chunk, v: v, a: a, duration: d, + offset: part.offset) + } + } + for _ in 0..<8 { addNext() } + while inFlight > 0 { + guard let r = await group.next() else { break } + inFlight -= 1 + if let r { loaded[r.index] = r } + addNext() + } + } + + func fillFromOriginal(_ range: CMTimeRange) { + if let origV { + try? vTrack.insertTimeRange(range, of: origV, at: range.start) + if let aTrack, let origA { + try? aTrack.insertTimeRange(range, of: origA, at: range.start) + } + } else { + vTrack.insertEmptyTimeRange(range) + } + } + let n = Self.chunkCount(duration: media.duration) for i in 0.. 0.001 else { break } let at = CMTime(seconds: startSec, preferredTimescale: 600) let dur = CMTime(seconds: durSec, preferredTimescale: 600) - var inserted = false - if let url = chunkURLs[i] { - let chunk = AVURLAsset(url: url) - if let v = try? await chunk.loadTracks(withMediaType: .video).first { - let chunkDuration = (try? await chunk.load(.duration)) ?? .zero - let r = CMTimeRange(start: .zero, duration: min(dur, chunkDuration)) - try? vTrack.insertTimeRange(r, of: v, at: at) - if let aTrack, let a = try? await chunk.loadTracks(withMediaType: .audio).first { - try? aTrack.insertTimeRange(r, of: a, at: at) - } - inserted = true - } + guard let part = loaded[i] else { + fillFromOriginal(CMTimeRange(start: at, duration: dur)) + continue } - if !inserted { - if let origV { - let r = CMTimeRange(start: at, duration: dur) - try? vTrack.insertTimeRange(r, of: origV, at: at) - if let aTrack, let origA { - try? aTrack.insertTimeRange(r, of: origA, at: at) - } - } else { - vTrack.insertEmptyTimeRange(CMTimeRange(start: at, duration: dur)) - } + let sliceAt = CMTime(seconds: startSec + part.offset, preferredTimescale: 600) + let sliceDur = min(part.duration, + CMTime(seconds: durSec - part.offset, preferredTimescale: 600)) + // Rescue slice: original (or empty) leads in, the slice covers the + // playhead's neighborhood, original (or empty) fills the tail. + if part.offset > 0.001 { + fillFromOriginal(CMTimeRange(start: at, end: sliceAt)) + } + let r = CMTimeRange(start: .zero, duration: sliceDur) + do { + try vTrack.insertTimeRange(r, of: part.v, at: sliceAt) + } catch { + // A healthy chunk that fails to stitch plays back as a BLACK + // gap — never let that be silent again (a dropped asset + // reference made every insert fail exactly this way once). + SeqLog.log("[cache] comp insert FAILED %@#%d dur=%.2f: %@", + String(media.cacheKey.prefix(8)), i, sliceDur.seconds, + String(describing: error)) + } + if let aTrack, let a = part.a { + try? aTrack.insertTimeRange(r, of: a, at: sliceAt) + } + let sliceEnd = sliceAt + sliceDur + let slotEnd = at + dur + if sliceEnd + CMTime(seconds: 0.001, preferredTimescale: 600) < slotEnd { + fillFromOriginal(CMTimeRange(start: sliceEnd, end: slotEnd)) } } return (comp, origV != nil) diff --git a/sequencer/Sources/Sequencer/Document.swift b/sequencer/Sources/Sequencer/Document.swift index 1ff497fe81bd5a65561617f00718b3117964d38f..8ae89db69b9e726aa8f0e9117d865bb535c5118d 100644 --- a/sequencer/Sources/Sequencer/Document.swift +++ b/sequencer/Sources/Sequencer/Document.swift @@ -12,7 +12,7 @@ import AppKit /// and force the document type for any `.sq` URL — regardless of what /// LaunchServices believes — so opening always resolves to `ProjectDocument`. final class ProjectDocumentController: NSDocumentController { - private static let projectType = "com.clover.sequencer.project" + private static let projectType = "net.paperclover.sequencer.project" private let sqPanelDelegate = SQOpenPanelDelegate() /// Pin the document type for `.sq` URLs so it never resolves to a folder, @@ -41,6 +41,21 @@ final class ProjectDocumentController: NSDocumentController { } } } + + /// Opening a project from a pristine untitled window replaces that window + /// instead of leaving an empty one behind. + override func openDocument(withContentsOf url: URL, display displayDocument: Bool, + completionHandler: @escaping (NSDocument?, Bool, Error?) -> Void) { + let blanks = documents.compactMap { $0 as? ProjectDocument }.filter(\.isPristineUntitled) + super.openDocument(withContentsOf: url, display: displayDocument) { doc, alreadyOpen, error in + if doc != nil, error == nil { + for blank in blanks where blank.isPristineUntitled && blank !== doc { + blank.close() + } + } + completionHandler(doc, alreadyOpen, error) + } + } } /// Enables only `.sq` items (package directories or legacy flat files) in the @@ -73,6 +88,21 @@ final class ProjectDocument: NSDocument { /// first explicit save. override class var autosavesInPlace: Bool { true } + /// Never saved, never edited, and nothing on the timeline — safe to close + /// when a real project opens over it. + var isPristineUntitled: Bool { + fileURL == nil && !isDocumentEdited + && ctx.store.project.media.isEmpty && ctx.store.project.clips.isEmpty + } + + /// Stop per-document services before AppKit tears the document down, so a + /// proxy build or the playback clock finishing after close can't touch the + /// now-dangling `unowned` context. + override func close() { + ctx.shutdown() + super.close() + } + override func makeWindowControllers() { let wc = SequencerWindowController(ctx: ctx) addWindowController(wc) diff --git a/sequencer/Sources/Sequencer/DocumentContext.swift b/sequencer/Sources/Sequencer/DocumentContext.swift index 65dd69d5feb2be0456d26e9edc18574dcf9d39f0..312719d32e18e333e9530d9f939f083508cbf433 100644 --- a/sequencer/Sources/Sequencer/DocumentContext.swift +++ b/sequencer/Sources/Sequencer/DocumentContext.swift @@ -47,6 +47,25 @@ final class DocumentContext { } } + private var didShutdown = false + + /// Tear the document's services down while `self` is still alive — called + /// from `ProjectDocument.close()`. Services hold `unowned var ctx`; an + /// in-flight chunk build or the 60 Hz clock landing a callback AFTER the + /// context deallocs would trap on that dangling reference. Stopping them here + /// (before dealloc) makes every such late callback a no-op. + func shutdown() { + guard !didShutdown else { return } + didShutdown = true + playback.stop() + chunks.stop() + comps.stopWatching() + if let reconcileObserver { + NotificationCenter.default.removeObserver(reconcileObserver) + self.reconcileObserver = nil + } + } + deinit { if let reconcileObserver { NotificationCenter.default.removeObserver(reconcileObserver) } comps.stopWatching() @@ -55,8 +74,22 @@ final class DocumentContext { /// Start the per-document services (playback clock, comps folder watch, /// derived-asset warmup). Called once by the window controller after load. func startServices() { + // Trim the shared media cache under its byte cap now that this project's + // media is registered (so its own cache is protected). Opening a + // document is also the natural moment to re-measure the cache from + // disk (reconcile) so the incremental ledger can't drift for long. + MediaPipeline.shared.evictIfNeeded(reconcile: true) MediaPipeline.shared.ensureDerivedAssets(for: store.project) - chunks.ensure(for: store.project) + // NOTE: the whole-project proxy pre-build (`chunks.ensure`) is NOT kicked + // here. macOS state restoration reopens *every* previously-open project on + // launch, and each one running `ensure` would transcode its full ProRes + // proxy set in parallel — every open project's media counts as "in use", so + // eviction can't trim any of it and the shared cache blows past its cap and + // fills the disk. Instead the fill is triggered when a document's window + // becomes main (SequencerWindowController.windowDidBecomeMain): the + // frontmost project fills immediately, a background/restored project fills + // only once you switch to it. On-demand playhead builds (`want`) still run + // for whatever is actually playing. comps.rescan() comps.startWatching() playback.start() @@ -69,10 +102,14 @@ final class DocumentContext { /// (Settings, Export, cache eviction) that operate on whichever project is /// frontmost. Falls back to the headless context when nothing is open. static var current: DocumentContext { - if let wc = NSApp.keyWindow?.windowController as? SequencerWindowController { + // `NSApp` is nil in the headless `--selftest` harness (no NSApplication); + // guard it so callers on the build path can ask "am I frontmost?" without + // crashing — with no app, the headless context is by definition current. + guard let app = NSApp else { return headless } + if let wc = app.keyWindow?.windowController as? SequencerWindowController { return wc.ctx } - if let wc = NSApp.mainWindow?.windowController as? SequencerWindowController { + if let wc = app.mainWindow?.windowController as? SequencerWindowController { return wc.ctx } if let doc = NSDocumentController.shared.currentDocument as? ProjectDocument { diff --git a/sequencer/Sources/Sequencer/Export.swift b/sequencer/Sources/Sequencer/Export.swift index 02ac9d57938d0200f791c5d272de098684a0e30a..fc93543fac46c600326219d27303b0d37cec6fc3 100644 --- a/sequencer/Sources/Sequencer/Export.swift +++ b/sequencer/Sources/Sequencer/Export.swift @@ -186,6 +186,7 @@ enum ExportError: Error, LocalizedError { case fusion(String) case intermediateFailed case encodeFailed(String) + case missingMedia([String]) var errorDescription: String? { switch self { @@ -194,6 +195,11 @@ enum ExportError: Error, LocalizedError { case .fusion(let s): return s case .intermediateFailed: return "Could not render the timeline composition." case .encodeFailed(let s): return "ffmpeg failed to encode the output.\n\n\(s)" + case .missingMedia(let names): + let list = names.map { " • \($0)" }.joined(separator: "\n") + return "Export was stopped because this media could not be read — the " + + "output would silently drop those clips. Reconnect the drive or " + + "relink the files and try again:\n\n\(list)" } } } @@ -244,24 +250,34 @@ enum Exporter { let comp = AVMutableComposition() let wantVideo = job.format.isVideo && !segments.isEmpty + // Media that couldn't be read while building the composition. A silent + // gap here means the render quietly drops content, so we collect every + // offending source and abort (below) rather than hand ffmpeg a truncated + // intermediate that looks like a successful export. + var missing = Set() + // Video: one track, segments appended left-to-right with empty gaps. if wantVideo, let vTrack = comp.addMutableTrack( withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) { var cursor = 0.0 for seg in segments { - guard let media = project.media(seg.mediaId) else { continue } + guard let media = project.media(seg.mediaId) else { + missing.insert("(missing media reference)"); continue + } if seg.start > cursor + 1e-6 { vTrack.insertEmptyTimeRange(cmRange(cursor, seg.start - cursor)) cursor = seg.start } let asset = AVURLAsset(url: media.url) guard let src = loadTracksSync(asset, mediaType: .video).first else { + missing.insert(media.displayName) vTrack.insertEmptyTimeRange(cmRange(cursor, seg.duration)); cursor += seg.duration; continue } let srcDur = seg.duration * seg.speed let range = cmRange(seg.srcIn, srcDur) let at = cm(cursor) - try? vTrack.insertTimeRange(range, of: src, at: at) + do { try vTrack.insertTimeRange(range, of: src, at: at) } + catch { missing.insert(media.displayName) } if abs(seg.speed - 1) > 1e-6 { // Nothing has been appended after `at` yet, so scaling this // range back to timeline duration is safe. @@ -280,9 +296,12 @@ enum Exporter { withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) else { continue } let asset = AVURLAsset(url: media.url) - guard let src = loadTracksSync(asset, mediaType: .audio).first else { continue } + guard let src = loadTracksSync(asset, mediaType: .audio).first else { + missing.insert(media.displayName); continue + } let srcDur = clip.duration * clip.speed - try? aTrack.insertTimeRange(cmRange(clip.srcIn, srcDur), of: src, at: cm(clip.start)) + do { try aTrack.insertTimeRange(cmRange(clip.srcIn, srcDur), of: src, at: cm(clip.start)) } + catch { missing.insert(media.displayName) } if abs(clip.speed - 1) > 1e-6 { aTrack.scaleTimeRange(cmRange(clip.start, srcDur), toDuration: cm(clip.duration)) } @@ -298,6 +317,13 @@ enum Exporter { mixParams.append(p) } + // Any unreadable source means the render would be silently truncated — + // stop and tell the user exactly which files, rather than produce a + // partial export that looks complete. + if !missing.isEmpty { + throw ExportError.missingMedia(missing.sorted()) + } + // Render the composition to an intermediate the encoder can read. let tmp = tempDir() defer { try? FileManager.default.removeItem(at: tmp) } diff --git a/sequencer/Sources/Sequencer/FrameCache.swift b/sequencer/Sources/Sequencer/FrameCache.swift new file mode 100644 index 0000000000000000000000000000000000000000..47b0bf406d646f151a4e4c2c3e545f5455f1df9c --- /dev/null +++ b/sequencer/Sources/Sequencer/FrameCache.swift @@ -0,0 +1,106 @@ +import Foundation +import AVFoundation +import QuartzCore + +/// RAM cache of decoded, full-quality stand-in frames. +/// +/// The players keep only ~1s of decoded video buffered, so the instant of a +/// clip boundary (an item swap, or a long seek inside a stitched composition) +/// has nothing exact to show for a beat — the viewer fell back to a 240px +/// filmstrip thumb, or the "Loading Media…" spinner when even that missed. +/// This cache holds the *exact* frames those moments need: cut heads and +/// tails near the playhead are warmed before the boundary arrives, and any +/// frame the viewer is currently unable to show gets decoded on demand from +/// the sharpest thing on disk (chunk, rescue slice, or playable original). +/// +/// Budget comes from the `maxRAMGB` default (Settings → Global, 2 GB when +/// unset); a 1080p frame is ~8 MB, so even the default holds a couple of +/// hundred boundaries. Frames are keyed on 0.25s buckets — a stand-in a +/// fraction of a second off is indistinguishable during the sub-second hold +/// it covers. +final class FrameCache { + static let shared = FrameCache() + private static let bucket = 0.25 + + static var ramGB: Int { + let gb = UserDefaults.standard.integer(forKey: "maxRAMGB") + return gb > 0 ? gb : 2 + } + + private let cache = NSCache() + /// Decodes in flight / recently failed (source vanished mid-decode, or a + /// truncated file) — failures back off so a hopeless frame isn't retried + /// every tick. Main-thread only. + private var inFlight = Set() + private var failedAt: [String: Double] = [:] + private let genQueue = DispatchQueue(label: "sequencer.framecache", + qos: .userInitiated) + + init() { refreshBudget() } + + /// Re-read the budget after the Settings field changes. + func refreshBudget() { + cache.totalCostLimit = Self.ramGB * 1_000_000_000 + } + + private func key(_ mediaKey: String, bucket: Int) -> NSString { + "\(mediaKey)@\(bucket)" as NSString + } + private func bucketIndex(_ src: Double) -> Int { + Int((src / Self.bucket).rounded()) + } + + /// The cached frame nearest `src`, if any (this bucket or a neighbor). + func image(media: MediaItem, at src: Double) -> CGImage? { + let b = bucketIndex(src) + for d in [0, -1, 1] { + if let img = cache.object(forKey: key(media.cacheKey, bucket: b + d)) { + return img + } + } + return nil + } + + /// Decode the frame at `src` into the cache if it isn't there already. + /// `source` is a playable (file, time-in-file) pair for this moment, + /// resolved on the main thread by `ChunkManager.frameSource` — nil (no + /// chunk, no playable original) is a silent no-op. Posts + /// `.viewerNeedsRefresh` when the frame lands so a waiting cell picks it + /// up. Cheap and self-deduping; safe to call every tick. + /// `SEQ_NOWARM=1` disables decodes (diagnostic kill switch). + private static let disabled = ProcessInfo.processInfo.environment["SEQ_NOWARM"] != nil + + func warm(mediaKey: String, at src: Double, source: (url: URL, time: Double)?) { + guard !Self.disabled, let source else { return } + let k = key(mediaKey, bucket: bucketIndex(src)) + let ks = k as String + guard cache.object(forKey: k) == nil, !inFlight.contains(ks) else { return } + let now = CACurrentMediaTime() + if let failed = failedAt[ks], now - failed < 5 { return } + inFlight.insert(ks) + genQueue.async { [self] in + let asset = AVURLAsset(url: source.url) + let gen = AVAssetImageGenerator(asset: asset) + gen.appliesPreferredTrackTransform = true + // Exact going in, a hair of slack after: the frame ON the cut is + // what a boundary needs, but a keyframe-snap decode is far cheaper + // than a precise reverse walk. + gen.requestedTimeToleranceBefore = .zero + gen.requestedTimeToleranceAfter = CMTime(seconds: 0.2, preferredTimescale: 600) + let t = CMTime(seconds: max(0, source.time), preferredTimescale: 60000) + let img = try? gen.copyCGImage(at: t, actualTime: nil) + DispatchQueue.main.async { + self.inFlight.remove(ks) + if let img { + self.failedAt.removeValue(forKey: ks) + self.cache.setObject(img, forKey: k, + cost: img.bytesPerRow * img.height) + NotificationCenter.default.post(name: .viewerNeedsRefresh, + object: nil) + } else { + self.failedAt[ks] = CACurrentMediaTime() + } + } + } + } +} diff --git a/sequencer/Sources/Sequencer/HangMonitor.swift b/sequencer/Sources/Sequencer/HangMonitor.swift new file mode 100644 index 0000000000000000000000000000000000000000..38406a66cc10f8577b8e5175c886efcd2e489d8f --- /dev/null +++ b/sequencer/Sources/Sequencer/HangMonitor.swift @@ -0,0 +1,190 @@ +import Foundation +import Darwin + +/// Main-thread hang watchdog. A background thread pings the main queue every +/// 20ms; when the pong stops coming back for >100ms, the main thread is +/// stalled — exactly the "microhang" that makes playback video stutter while +/// audio (decoded off-main by CoreAudio) keeps going. While the stall lasts, +/// the watchdog suspends the main thread for microseconds at a time, walks its +/// frame pointers, and symbolicates — so the log names the culprit, not just +/// the duration. Lines go to ~/Library/Logs/Sequencer.log via SeqLog: +/// +/// [hang] main thread 0.34s during playback (rate 1.0) — 3 samples, +/// top: ViewerGridView.update() ← CA::Transaction::commit ← … +/// +/// Cost when healthy: one trivial main-queue block per 20ms. Disable with +/// SEQ_NOHANGWATCH=1. +/// SEQ_HANGTEST target: a recognizable frame that should appear in the +/// sampled stack. Spins (not sleeps) so the pc sits in our own code. +@inline(never) +func hangTestStall() { + let until = CFAbsoluteTimeGetCurrent() + 0.4 + var sink = 0.0 + while CFAbsoluteTimeGetCurrent() < until { sink += sin(sink) + 1 } + _ = sink +} + +enum HangMonitor { + /// Written from PlaybackController.setRate (main), read by the watchdog + /// thread. Benign race — it only annotates log lines. + nonisolated(unsafe) static var playbackRate: Double = 0 + + private nonisolated(unsafe) static var mainThread: thread_t = 0 + private nonisolated(unsafe) static var lastPong = CFAbsoluteTimeGetCurrent() + private nonisolated(unsafe) static var pingInFlight = false + private static let lock = NSLock() + private static let threshold = 0.1 // report stalls longer than this + + /// Call once from the main thread at startup. + static func start() { + guard ProcessInfo.processInfo.environment["SEQ_NOHANGWATCH"] == nil else { return } + mainThread = pthread_mach_thread_np(pthread_self()) + let t = Thread { watch() } + t.name = "sequencer.hangwatch" + t.qualityOfService = .userInitiated + t.start() + } + + private static func watch() { + while true { + usleep(20_000) + lock.lock() + let age = CFAbsoluteTimeGetCurrent() - lastPong + let busy = pingInFlight + if !busy { + pingInFlight = true + lock.unlock() + DispatchQueue.main.async { + lock.lock() + lastPong = CFAbsoluteTimeGetCurrent() + pingInFlight = false + lock.unlock() + } + } else { + lock.unlock() + } + if busy, age > threshold { observeStall(begunAge: age) } + } + } + + /// Main thread has been unresponsive for `begunAge` already. Sample its + /// stack periodically until it recovers, then log one line. + private static func observeStall(begunAge: Double) { + let start = CFAbsoluteTimeGetCurrent() - begunAge + var samples: [[String]] = [] + while true { + if samples.count < 5 { + let frames = sampleMainStack() + if !frames.isEmpty { samples.append(frames) } + } + usleep(100_000) + lock.lock() + let stillStalled = pingInFlight && lastPong < start + lock.unlock() + if !stillStalled { break } + } + // Wait for the pong to actually land so the duration is honest. + var duration = CFAbsoluteTimeGetCurrent() - start + for _ in 0..<200 { // give the queued pong up to 2s to run + lock.lock(); let pong = lastPong; lock.unlock() + if pong >= start { duration = pong - start; break } + usleep(10_000) + } + guard duration > threshold else { return } + let rate = playbackRate + let during = rate != 0 ? String(format: " during playback (rate %.1f)", rate) : "" + let top = samples.first?.prefix(8).joined(separator: " ← ") ?? "no stack (sampling failed)" + SeqLog.log("[hang] main thread %.2fs%@ — %d sample%@, top: %@", + duration, during, samples.count, samples.count == 1 ? "" : "s", top) + for extra in samples.dropFirst() where extra.first != samples.first?.first { + SeqLog.log("[hang] also seen: %@", extra.prefix(5).joined(separator: " ← ")) + } + } + + // MARK: stack sampling (arm64 frame-pointer walk) + + /// Fixed buffers, touched only by the watchdog thread. They exist so the + /// suspend window below performs ZERO allocations: if the main thread is + /// suspended while holding the malloc lock, any malloc here deadlocks the + /// whole app (watchdog waits on the lock, suspended main can never release + /// it, thread_resume never runs). This happened — main frozen mid free() + /// in drawRuler, watchdog frozen in Array.append → permanent freeze. + private static let maxFrames = 50 + private nonisolated(unsafe) static var pcBuf = [UInt64](repeating: 0, count: maxFrames) + private nonisolated(unsafe) static var pairBuf = [UInt64](repeating: 0, count: 2) + + private static func sampleMainStack() -> [String] { + guard mainThread != 0, thread_suspend(mainThread) == KERN_SUCCESS else { return [] } + // ---- suspend window: no allocation, no locks, no ObjC/Swift runtime + // calls that might take either. Only mach syscalls and raw stores. ---- + var n = 0 + var state = arm_thread_state64_t() + var count = mach_msg_type_number_t(MemoryLayout.size + / MemoryLayout.size) + let kr = withUnsafeMutablePointer(to: &state) { ptr in + ptr.withMemoryRebound(to: natural_t.self, capacity: Int(count)) { + thread_get_state(mainThread, ARM_THREAD_STATE64, $0, &count) + } + } + if kr == KERN_SUCCESS { + pcBuf[n] = arm64PC(state); n += 1 + let lr = arm64LR(state) + var fp = arm64FP(state) + if lr != 0 { pcBuf[n] = lr; n += 1 } + // Frame layout: [fp] = caller fp, [fp+8] = return address. + while n < maxFrames { + guard fp != 0, fp & 0x7 == 0 else { break } + var outSize: mach_vm_size_t = 16 + let r = pairBuf.withUnsafeMutableBytes { buf in + mach_vm_read_overwrite(mach_task_self_, mach_vm_address_t(fp), 16, + mach_vm_address_t(UInt(bitPattern: buf.baseAddress)), + &outSize) + } + guard r == KERN_SUCCESS, pairBuf[1] != 0 else { break } + pcBuf[n] = pairBuf[1]; n += 1 + guard pairBuf[0] > fp else { break } // stacks grow down; fp chain grows up + fp = pairBuf[0] + } + } + thread_resume(mainThread) + // ---- end suspend window; symbolication may allocate freely. ---- + return (0.. UInt64 { s.__pc } + private static func arm64LR(_ s: arm_thread_state64_t) -> UInt64 { + s.__lr & 0x0000_7FFF_FFFF_FFFF // strip ptrauth bits + } + private static func arm64FP(_ s: arm_thread_state64_t) -> UInt64 { s.__fp } + + private typealias DemangleFn = @convention(c) ( + UnsafePointer?, Int, UnsafeMutablePointer?, + UnsafeMutablePointer?, UInt32) -> UnsafeMutablePointer? + private static let demangleFn: DemangleFn? = { + guard let sym = dlsym(dlopen(nil, RTLD_NOW), "swift_demangle") else { return nil } + return unsafeBitCast(sym, to: DemangleFn.self) + }() + + private static func symbolicate(_ pc: UInt64) -> String? { + let stripped = pc & 0x0000_7FFF_FFFF_FFFF + var info = Dl_info() + guard dladdr(UnsafeRawPointer(bitPattern: UInt(stripped)), &info) != 0 else { return nil } + var name: String + if let sname = info.dli_sname { + name = String(cString: sname) + if name.hasPrefix("$s") || name.hasPrefix("_$s"), let fn = demangleFn, + let d = fn(name, name.utf8.count, nil, nil, 0) { + name = String(cString: d) + free(d) + // Demangled Swift names are long; keep the signature-free head. + if let paren = name.firstIndex(of: "(") { name = String(name[.. [ …]`. +/// Builds the same chunk+original stitched compositions the app plays and +/// shows each in a bare AVPlayerLayer tile — isolating "do N of these render +/// concurrently" from every other moving part of the app. Seeks all tiles to +/// SEQ_LAYERTEST_T (default 16.6). +@MainActor +func runLayerTest(specs: [String]) { + let t = Double(ProcessInfo.processInfo.environment["SEQ_LAYERTEST_T"] ?? "") ?? 16.6 + let cols = specs.count + let tileW = 420.0, tileH = 260.0 + let win = NSWindow(contentRect: NSRect(x: 100, y: 300, + width: tileW * Double(cols), height: tileH), + styleMask: [.titled], backing: .buffered, defer: false) + win.title = "LayerTest t=\(t)" + win.contentView!.wantsLayer = true + win.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + + for (col, spec) in specs.enumerated() { + let bits = spec.split(separator: ":", maxSplits: 1).map(String.init) + guard bits.count == 2 else { continue } + let key = bits[0], orig = bits[1] + let player = AVPlayer() + player.automaticallyWaitsToMinimizeStalling = false + let layer = AVPlayerLayer(player: player) + layer.frame = NSRect(x: Double(col) * tileW, y: 0, width: tileW, height: tileH) + layer.videoGravity = .resizeAspect + layer.backgroundColor = NSColor.purple.cgColor // un-rendered = purple + win.contentView!.layer!.addSublayer(layer) + let chunkDir = MediaPipeline.shared.cacheRoot + .appendingPathComponent(key).appendingPathComponent("chunks") + Task { @MainActor in + let comp = AVMutableComposition() + let vTrack = comp.addMutableTrack(withMediaType: .video, + preferredTrackID: kCMPersistentTrackID_Invalid)! + let original = AVURLAsset(url: URL(fileURLWithPath: orig)) + let origV = try? await original.loadTracks(withMediaType: .video).first + let origDur = (try? await original.load(.duration))?.seconds ?? 0 + for i in 0.. 5_000_000 { + let old = url.deletingPathExtension().appendingPathExtension("old.log") + try? FileManager.default.removeItem(at: old) + try? FileManager.default.moveItem(at: url, to: old) + } + if let h = FileHandle(forWritingAtPath: url.path) { + defer { try? h.close() } + _ = try? h.seekToEnd() + try? h.write(contentsOf: Data(dated.utf8)) + } else { + try? Data(dated.utf8).write(to: url) + } + } + } +} diff --git a/sequencer/Sources/Sequencer/MediaPipeline.swift b/sequencer/Sources/Sequencer/MediaPipeline.swift index 6879a9acd16135f47970a205a70c2645cff3999f..4a89a14956238f1d6dcf6a962b749fcd1d088ae0 100644 --- a/sequencer/Sources/Sequencer/MediaPipeline.swift +++ b/sequencer/Sources/Sequencer/MediaPipeline.swift @@ -17,8 +17,8 @@ final class MediaPipeline { static let shared = MediaPipeline() let cacheRoot: URL - /// LRU cap in bytes (default 50 GB). Override with `defaults write - /// com.sequencer maxCacheGB -int 100`. + /// LRU cap in bytes (default 50 GB). Override in Settings, or `defaults + /// write net.paperclover.Sequencer maxCacheGB -int 100`. var maxCacheBytes: Int64 { let gb = UserDefaults.standard.integer(forKey: "maxCacheGB") return Int64(gb > 0 ? gb : 50) * 1_000_000_000 @@ -28,7 +28,7 @@ final class MediaPipeline { private let ffprobe: String? private let workQueue = OperationQueue() private var statuses: [UUID: MediaStatus] = [:] // main-thread only - private let thumbCache = NSCache() + private let thumbCache = NSCache() private var stripInfoCache: [String: (interval: Double, count: Int)] = [:] private var lruTouched: [String: Date] = [:] @@ -44,6 +44,12 @@ final class MediaPipeline { ffprobe = Self.findExecutable("ffprobe") workQueue.maxConcurrentOperationCount = 2 thumbCache.countLimit = 2000 + // A previous instance killed mid-build (rebuild relaunch, force quit) + // leaves orphaned ffmpeg encoders holding shared VideoToolbox decode + // sessions — enough of them and every AVPlayer here renders black. + Self.reapOrphans(cacheRoot: cacheRoot) + // Measure the cache once at launch; chunk builds wait on `budgetReady`. + DispatchQueue.main.async { self.reconcileLedger() } } static func findExecutable(_ name: String) -> String? { @@ -68,6 +74,18 @@ final class MediaPipeline { return s } + private var offlineCache: [String: (offline: Bool, until: Date)] = [:] // main-thread only + /// Whether the media's ORIGINAL file is currently unreachable (moved, or on + /// an unmounted NAS). Cached briefly so the viewer can call it every frame + /// without a `stat` each time, and so it auto-recovers when the drive returns. + func isOffline(_ media: MediaItem) -> Bool { + let now = Date() + if let c = offlineCache[media.cacheKey], c.until > now { return c.offline } + let off = !FileManager.default.fileExists(atPath: media.path) + offlineCache[media.cacheKey] = (off, now.addingTimeInterval(2)) + return off + } + // MARK: - Cache paths /// The shape `cacheKey(for:)` produces: exactly 16 lowercase hex chars. A @@ -236,25 +254,71 @@ final class MediaPipeline { "aformat=channel_layouts=mono,showwavespic=s=2048x200:colors=white", "-frames:v", "1", waveformURL(media.cacheKey).path, ]) + let bytes = (try? FileManager.default.attributesOfItem( + atPath: waveformURL(media.cacheKey).path)[.size] as? NSNumber)?.int64Value ?? 0 DispatchQueue.main.async { self.touchLRU(media.cacheKey) if res.exitCode == 0 { - NotificationCenter.default.post(name: .mediaStatusChanged, object: nil) + self.noteBytesAdded(bytes) + self.imgStateLock.lock() + self.waveformMissing.remove(media.cacheKey) + self.imgStateLock.unlock() + // `scene: true` — a waveform image changes timeline pixels + // (the tile cache flushes on it; plain chunk churn must not). + NotificationCenter.default.post(name: .mediaStatusChanged, object: nil, + userInfo: ["scene": true]) } } } - private let waveformCache = NSCache() + private let waveformCache = NSCache() - func waveformImage(for media: MediaItem) -> NSImage? { - let key = media.cacheKey as NSString - if let img = waveformCache.object(forKey: key) { return img } - let url = waveformURL(media.cacheKey) + /// Decode an image file straight into the display's raster format (BGRA + /// premultiplied, screen colorspace). A plain NSImage/CGImageSource image + /// keeps the file's own format (RGB JPEG, generic colorspace), and Quartz + /// then converts it on EVERY draw — per thumbnail, per frame. Converting + /// once at load makes the timeline's image blits plain memory copies. + static func displayImage(contentsOf url: URL) -> CGImage? { + guard let src = CGImageSourceCreateWithURL(url as CFURL, nil), + let raw = CGImageSourceCreateImageAtIndex( + src, 0, [kCGImageSourceShouldCache: false] as CFDictionary) + else { return nil } + let space = NSScreen.main?.colorSpace?.cgColorSpace + ?? CGColorSpace(name: CGColorSpace.sRGB)! + guard let ctx = CGContext( + data: nil, width: raw.width, height: raw.height, + bitsPerComponent: 8, bytesPerRow: 0, space: space, + bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue + | CGBitmapInfo.byteOrder32Little.rawValue) else { return raw } + ctx.draw(raw, in: CGRect(x: 0, y: 0, width: raw.width, height: raw.height)) + return ctx.makeImage() ?? raw + } + + /// Same negative/in-flight bookkeeping as thumbnails: a missing waveform + /// must not re-dispatch a load per audio clip per frame. + private var waveformMissing = Set() + private var waveformLoading = Set() + + func waveformImage(for media: MediaItem) -> CGImage? { + let key = media.cacheKey + if let img = waveformCache.object(forKey: key as NSString) { return img } + imgStateLock.lock() + let skip = waveformMissing.contains(key) || waveformLoading.contains(key) + if !skip { waveformLoading.insert(key) } + imgStateLock.unlock() + guard !skip else { return nil } + let url = waveformURL(key) DispatchQueue.global(qos: .utility).async { - guard let img = NSImage(contentsOf: url) else { return } + let img = Self.displayImage(contentsOf: url) DispatchQueue.main.async { - self.waveformCache.setObject(img, forKey: key) - self.notifyThumbsCoalesced() + self.imgStateLock.lock() + self.waveformLoading.remove(key) + if img == nil { self.waveformMissing.insert(key) } + self.imgStateLock.unlock() + if let img { + self.waveformCache.setObject(img, forKey: key as NSString) + self.notifyThumbsCoalesced() + } } } return nil @@ -279,41 +343,72 @@ final class MediaPipeline { try? d.write(to: stripInfoURL(media.cacheKey)) } } + let bytes = Self.directorySize(dir) DispatchQueue.main.async { var s = self.status(for: media) s.filmstripReady = res.exitCode == 0 && count > 0 self.statuses[media.id] = s self.touchLRU(media.cacheKey) - NotificationCenter.default.post(name: .mediaStatusChanged, object: nil) + self.noteBytesAdded(bytes) + self.imgStateLock.lock() + self.thumbMissing.removeAll() // a fresh strip supersedes misses + self.imgStateLock.unlock() + NotificationCenter.default.post(name: .mediaStatusChanged, object: nil, + userInfo: ["scene": true]) } } // MARK: - Filmstrip access func filmstripInfo(_ key: String) -> (interval: Double, count: Int)? { - if let c = stripInfoCache[key] { return c } + imgStateLock.lock() + let cached = stripInfoCache[key] + imgStateLock.unlock() + if let cached { return cached } guard let data = try? Data(contentsOf: stripInfoURL(key)), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let interval = json["interval"] as? Double, let count = json["count"] as? Int else { return nil } + imgStateLock.lock() stripInfoCache[key] = (interval, count) + imgStateLock.unlock() return (interval, count) } /// Cached thumbnail nearest to `seconds`; loads async and posts a single /// coalesced .mediaStatusChanged when thumbs land (a post per thumb /// cascades into an app-wide refresh storm while a filmstrip streams in). - func filmstripImage(for media: MediaItem, at seconds: Double) -> NSImage? { + /// Thumbs that failed to load (file absent/evicted) or are mid-load. + /// Without these a dense timeline re-dispatches a load per missing thumb + /// per FRAME — hundreds of no-op queue hops every draw. Misses are + /// forgotten whenever fresh thumbs land (strips may have regenerated). + /// Lock-guarded: the timeline rasterizes lanes on parallel threads. + private var thumbMissing = Set() + private var thumbLoading = Set() + private let imgStateLock = NSLock() + + func filmstripImage(for media: MediaItem, at seconds: Double) -> CGImage? { guard let info = filmstripInfo(media.cacheKey) else { return nil } let index = min(info.count, max(1, Int(seconds / info.interval) + 1)) - let cacheId = "\(media.cacheKey)/\(index)" as NSString - if let img = thumbCache.object(forKey: cacheId) { return img } + let cacheId = "\(media.cacheKey)/\(index)" + if let img = thumbCache.object(forKey: cacheId as NSString) { return img } + imgStateLock.lock() + let skip = thumbMissing.contains(cacheId) || thumbLoading.contains(cacheId) + if !skip { thumbLoading.insert(cacheId) } + imgStateLock.unlock() + guard !skip else { return nil } let url = stripDir(media.cacheKey).appendingPathComponent(String(format: "%06d.jpg", index)) DispatchQueue.global(qos: .utility).async { - guard let img = NSImage(contentsOf: url) else { return } + let img = Self.displayImage(contentsOf: url) DispatchQueue.main.async { - self.thumbCache.setObject(img, forKey: cacheId) - self.notifyThumbsCoalesced() + self.imgStateLock.lock() + self.thumbLoading.remove(cacheId) + if img == nil { self.thumbMissing.insert(cacheId) } + self.imgStateLock.unlock() + if let img { + self.thumbCache.setObject(img, forKey: cacheId as NSString) + self.notifyThumbsCoalesced() + } } } return nil @@ -325,7 +420,11 @@ final class MediaPipeline { thumbNotifyPending = true DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.thumbNotifyPending = false - NotificationCenter.default.post(name: .mediaStatusChanged, object: nil) + self.imgStateLock.lock() + self.thumbMissing.removeAll() // strips may have (re)generated + self.imgStateLock.unlock() + NotificationCenter.default.post(name: .mediaStatusChanged, object: nil, + userInfo: ["scene": true]) } } @@ -341,52 +440,202 @@ final class MediaPipeline { private var evicting = false - /// LRU eviction under the byte cap. Size scan and deletion run off-main - /// (the cache walk is I/O); never touches the current project's media. - func evictIfNeeded() { - guard !evicting else { return } + // MARK: - Cache budget ledger + + /// Approximate cache size in bytes, maintained incrementally (builds add, + /// evictions subtract) and reconciled against a real walk at launch and on + /// document open. With the ledger, staying under the cap is enforced BEFORE + /// bytes hit the disk (`tryReserve`), and the per-build full-cache walk the + /// old eviction did is gone. Main-thread only. + private(set) var ledgerBytes: Int64 = 0 + /// Bytes promised to in-flight chunk builds; released on completion. + private var reservedBytes: Int64 = 0 + /// False until the first reconcile walk finishes. Admission stays closed + /// while false, so an unmeasured cache can never be built past the cap. + private(set) var budgetReady = false + private var reconciling = false + + /// When eviction runs at all it frees down to here (not just under the + /// cap), so it works in batches instead of one chunk per build. + var lowWatermarkBytes: Int64 { Int64(Double(maxCacheBytes) * 0.92) } + + /// Re-measure the cache and set the ledger to ground truth. Called at + /// launch, on document open, and when the cap changes; incremental updates + /// keep it honest in between. + /// Anything on disk with an mtime before this was left by a previous + /// session — the test for purging session-transient files (rescue slices, + /// crashed builds' .partial temps) without touching live ones. + private static let processStart = Date() + + func reconcileLedger() { + guard !reconciling else { return } + reconciling = true + let root = cacheRoot + DispatchQueue.global(qos: .utility).async { + Self.purgeSessionTransients(root: root) + let total = Self.directorySize(root) + DispatchQueue.main.async { + NSLog("[cache] reconcile: %.2f GB on disk (cap %.0f GB)", + Double(total) / 1e9, Double(self.maxCacheBytes) / 1e9) + self.ledgerBytes = total + self.budgetReady = true + self.reconciling = false + if self.ledgerBytes + self.reservedBytes > self.maxCacheBytes { + self.evictToFit(need: 0) { _ in } + } + for c in DocumentContext.allLive { c.chunks.budgetChanged() } + } + } + } + + /// Reserve room for a build about to start. The reservation counts against + /// the ceiling alongside bytes already on disk, so two concurrent builds + /// can't both squeeze into the same headroom. Window builds reserve up to + /// the cap; background fill only up to the low watermark — the band in + /// between is slack for playhead work, so fill can never trigger (or + /// refight) an eviction. Main thread. + func tryReserve(bytes: Int64, upTo ceiling: Int64) -> Bool { + guard budgetReady, ledgerBytes + reservedBytes + bytes <= ceiling else { return false } + reservedBytes += bytes + return true + } + + /// A reserved build finished: release its reservation and record what + /// actually landed on disk (new file minus any replaced one; 0 on failure). + func commitBuild(reserved: Int64, delta: Int64) { + reservedBytes = max(0, reservedBytes - reserved) + ledgerBytes = max(0, ledgerBytes + delta) + } + + /// Bytes written outside the reservation flow (filmstrips, waveforms — + /// small, but the ledger should still see them between reconciles). + func noteBytesAdded(_ bytes: Int64) { ledgerBytes += bytes } + + /// Cheap budget check for the old trigger sites (build completions, doc + /// open, manual). `reconcile: true` re-walks the disk first — use it when + /// ground truth matters (doc open, cap change, menu action). + func evictIfNeeded(reconcile: Bool = false) { + if reconcile || !budgetReady { reconcileLedger(); return } + if ledgerBytes + reservedBytes > maxCacheBytes { + evictToFit(need: 0) { _ in } + } + } + + /// Free cache space so `need` more bytes fit under `ceiling` (the cap for + /// playhead work, the low watermark for fill), evicting down to the low + /// watermark once it runs at all. Coldness order: whole cache dirs of + /// projects no window has open (LRU) first, then — when `openDocChunks` — + /// individual cold proxy chunks of open documents, never touching any + /// document's working set (see ChunkManager.evictionCandidates). + /// `completion(true)` on main once the space exists. + func evictToFit(need: Int64, openDocChunks: Bool = true, + upTo ceiling: Int64? = nil, + completion: @escaping (Bool) -> Void) { + let ceiling = ceiling ?? maxCacheBytes + guard budgetReady, !evicting else { completion(false); return } + let deficit = (ledgerBytes + reservedBytes + need) - lowWatermarkBytes + guard deficit > 0 else { completion(true); return } evicting = true - // Protect the media of every open document (not just the front one) so - // eviction can't drop cache another window is still using. + // Whole-dir eviction must not touch any open document's media — those + // dirs also hold filmstrips/waveforms other windows are showing. Their + // cold CHUNKS are reclaimed individually via the candidates instead. let inUse = Set(DocumentContext.allLive.flatMap { $0.store.project.media.map(\.cacheKey) }) + var chunkCands: [ChunkManager.EvictionCandidate] = [] + if openDocChunks { + for c in DocumentContext.allLive { chunkCands += c.chunks.evictionCandidates() } + chunkCands.sort { $0.coldness > $1.coldness } + } let root = cacheRoot - let cap = maxCacheBytes DispatchQueue.global(qos: .utility).async { let fm = FileManager.default - var evicted: [String] = [] - defer { - DispatchQueue.main.async { - for k in evicted { - self.stripInfoCache[k] = nil - self.enqueued.remove(k) + var freed: Int64 = 0 + var evictedDirs: [String] = [] + var evictedChunks: [String: [Int]] = [:] + // 1. Closed projects' whole cache dirs, least recently used first. + if let keys = try? fm.contentsOfDirectory(atPath: root.path) { + var entries: [(key: String, lastUsed: Double)] = [] + for key in keys where !inUse.contains(key) { + let dir = root.appendingPathComponent(key, isDirectory: true) + var isDir: ObjCBool = false + guard fm.fileExists(atPath: dir.path, isDirectory: &isDir), isDir.boolValue else { continue } + let lastUsed = Double((try? String(contentsOf: dir.appendingPathComponent("lastUsed"), encoding: .utf8)) ?? "") ?? 0 + entries.append((key, lastUsed)) + } + for e in entries.sorted(by: { $0.lastUsed < $1.lastUsed }) { + guard freed < deficit else { break } + let dir = root.appendingPathComponent(e.key, isDirectory: true) + let bytes = Self.directorySize(dir) + try? fm.removeItem(at: dir) + evictedDirs.append(e.key) + freed += bytes + } + } + // 2. Cold chunks of open documents, coldest (farthest from any + // playhead / off-timeline) first. + for c in chunkCands { + guard freed < deficit else { break } + let bytes = (try? fm.attributesOfItem(atPath: c.url.path)[.size] as? NSNumber)?.int64Value ?? 0 + guard bytes > 0 else { continue } + try? fm.removeItem(at: c.url) + evictedChunks[c.key, default: []].append(c.index) + freed += bytes + } + DispatchQueue.main.async { + self.ledgerBytes = max(0, self.ledgerBytes - freed) + NSLog("[cache] evict: freed %.2f GB (%d dirs, %d chunks) → ledger %.2f GB", + Double(freed) / 1e9, evictedDirs.count, + evictedChunks.values.map(\.count).reduce(0, +), + Double(self.ledgerBytes) / 1e9) + self.imgStateLock.lock() + for k in evictedDirs { + self.stripInfoCache[k] = nil + } + self.imgStateLock.unlock() + for k in evictedDirs { + self.enqueued.remove(k) + } + for c in DocumentContext.allLive { + c.chunks.forget(keys: evictedDirs) + for (key, idxs) in evictedChunks { + c.chunks.noteEvicted(key: key, indices: idxs) } - for c in DocumentContext.allLive { c.chunks.forget(keys: evicted) } - self.evicting = false } + self.evicting = false + let ok = self.ledgerBytes + self.reservedBytes + need <= ceiling + if !evictedDirs.isEmpty || !evictedChunks.isEmpty { + NotificationCenter.default.post(name: .mediaStatusChanged, object: nil) + // Only re-pump when something was actually freed — a no-op + // eviction re-pumping would loop pump → evict → pump forever. + for c in DocumentContext.allLive { c.chunks.budgetChanged() } + } + completion(ok) } - guard let keys = try? fm.contentsOfDirectory(atPath: root.path) else { return } - var entries: [(key: String, bytes: Int64, lastUsed: Double)] = [] - var total: Int64 = 0 - for key in keys { - let dir = root.appendingPathComponent(key, isDirectory: true) - var isDir: ObjCBool = false - guard fm.fileExists(atPath: dir.path, isDirectory: &isDir), isDir.boolValue else { continue } - let bytes = Self.directorySize(dir) - let lastUsed = Double((try? String(contentsOf: dir.appendingPathComponent("lastUsed"), encoding: .utf8)) ?? "") ?? 0 - total += bytes - entries.append((key, bytes, lastUsed)) - } - guard total > cap else { return } - for e in entries.sorted(by: { $0.lastUsed < $1.lastUsed }) where !inUse.contains(e.key) { - try? fm.removeItem(at: root.appendingPathComponent(e.key, isDirectory: true)) - evicted.append(e.key) - total -= e.bytes - if total <= cap { break } - } } } - private static func directorySize(_ url: URL) -> Int64 { + /// Delete leftovers no session references anymore: rescue-slice files + /// (`rNNNNNN.mov` — their state is memory-only, so a relaunch can't know + /// which part of the chunk they cover) and `.partial.mov` temps from + /// builds a crash interrupted. Only files from BEFORE this process + /// started — a slice the current session just built stays. + private static func purgeSessionTransients(root: URL) { + let fm = FileManager.default + guard let en = fm.enumerator(at: root, + includingPropertiesForKeys: [.contentModificationDateKey]) + else { return } + for case let f as URL in en { + let name = f.lastPathComponent + let isRescue = name.hasPrefix("r") && name.hasSuffix(".mov") && name.count == 11 + && f.deletingLastPathComponent().lastPathComponent == "chunks" + let isTemp = name.hasSuffix(".partial.mov") + guard isRescue || isTemp else { continue } + let m = (try? f.resourceValues(forKeys: [.contentModificationDateKey]) + .contentModificationDate) ?? .distantPast + if m < processStart { try? fm.removeItem(at: f) } + } + } + + static func directorySize(_ url: URL) -> Int64 { var total: Int64 = 0 if let en = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.fileSizeKey]) { for case let f as URL in en { @@ -400,6 +649,46 @@ final class MediaPipeline { struct RunResult { var exitCode: Int32; var stdout: String } + /// Every child process currently running, so app teardown can take them + /// down too. A killed Sequencer otherwise leaves its in-flight ffmpeg + /// encodes running as orphans — and each one holds VideoToolbox decode + /// sessions from a SHARED machine-wide pool. Enough accumulated orphans + /// (a few rebuild-relaunch cycles' worth) and every AVPlayer in the next + /// app instance silently renders BLACK: items park, seeks land, + /// isReadyForDisplay says true, no error anywhere. Diagnosed 2026-07-11 + /// after the viewer went black with all state reporting healthy. + private static var liveChildren: [Int32: Process] = [:] + private static let childLock = NSLock() + + /// Terminate every live child (normal quit AND SIGTERM — run.sh pkills + /// the app on every rebuild, which is exactly how orphans were minted). + static func terminateChildren() { + childLock.lock() + let children = Array(liveChildren.values) + childLock.unlock() + for p in children where p.isRunning { p.terminate() } + } + + /// Kill orphaned ffmpeg processes from a PREVIOUS Sequencer instance + /// (crash, force-quit, kill -9 — anything terminateChildren couldn't + /// catch). Identified by their command line referencing our cache root, + /// so nothing else on the machine can match. Runs once at launch. + static func reapOrphans(cacheRoot: URL) { + let marker = cacheRoot.path + DispatchQueue.global(qos: .utility).async { + let r = run("/usr/bin/pgrep", ["-fl", "ffmpeg"]) + var killed = 0 + for line in r.stdout.split(separator: "\n") where line.contains(marker) { + guard let pid = Int32(line.prefix(while: \.isNumber)) else { continue } + kill(pid, SIGKILL) + killed += 1 + } + if killed > 0 { + SeqLog.log("[cache] reaped %d orphaned ffmpeg encoder(s) from a previous instance", killed) + } + } + } + @discardableResult static func run(_ path: String, _ args: [String], duration: Double? = nil, @@ -426,7 +715,13 @@ final class MediaPipeline { } do { try p.run() + childLock.lock() + liveChildren[p.processIdentifier] = p + childLock.unlock() p.waitUntilExit() + childLock.lock() + liveChildren.removeValue(forKey: p.processIdentifier) + childLock.unlock() } catch { return RunResult(exitCode: -1, stdout: "") } diff --git a/sequencer/Sources/Sequencer/Model.swift b/sequencer/Sources/Sequencer/Model.swift index ce47af71fd7e7de224c801c257cd34aa9e9e4a05..9594318e3641ac1096a492efe17812512929d01b 100644 --- a/sequencer/Sources/Sequencer/Model.swift +++ b/sequencer/Sources/Sequencer/Model.swift @@ -142,16 +142,22 @@ struct ViewState: Codable, Equatable { var laneScale: Double = 1 var snapping = true var showFilmstrips = true + var subtitles = true // clover-transcript captions overlay var previewsOnLeft = false var priorityPane: TrackRef? = nil var fusionHidden = false var fusionFocus = false + /// Where the playhead was parked at save. Reopening resumes here, which + /// also tells the proxy cache which neighbourhood to keep warm/protected + /// for this project while its window is in the background. + var playhead: Double = 0 init() {} enum CodingKeys: String, CodingKey { case hiddenTracks, focusedTracks, trackHeights, laneScale, snapping, - showFilmstrips, previewsOnLeft, priorityPane, fusionHidden, fusionFocus + showFilmstrips, subtitles, previewsOnLeft, priorityPane, + fusionHidden, fusionFocus, playhead } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) @@ -161,10 +167,12 @@ struct ViewState: Codable, Equatable { laneScale = try c.decodeIfPresent(Double.self, forKey: .laneScale) ?? 1 snapping = try c.decodeIfPresent(Bool.self, forKey: .snapping) ?? true showFilmstrips = try c.decodeIfPresent(Bool.self, forKey: .showFilmstrips) ?? true + subtitles = try c.decodeIfPresent(Bool.self, forKey: .subtitles) ?? true previewsOnLeft = try c.decodeIfPresent(Bool.self, forKey: .previewsOnLeft) ?? false priorityPane = try c.decodeIfPresent(TrackRef.self, forKey: .priorityPane) fusionHidden = try c.decodeIfPresent(Bool.self, forKey: .fusionHidden) ?? false fusionFocus = try c.decodeIfPresent(Bool.self, forKey: .fusionFocus) ?? false + playhead = try c.decodeIfPresent(Double.self, forKey: .playhead) ?? 0 } } @@ -341,8 +349,10 @@ struct MediaItem: Codable, Equatable, Identifiable { var isAudio: Bool = false // audio-only file (no video stream) var cacheKey: String = "" - var url: URL { URL(fileURLWithPath: path) } - var displayName: String { url.lastPathComponent } + // isDirectory:false skips URL's hidden stat() — path lives on the NAS, + // and per-draw stats of a cold network volume freeze the whole UI. + var url: URL { URL(fileURLWithPath: path, isDirectory: false) } + var displayName: String { (path as NSString).lastPathComponent } init(path: String) { self.path = path } @@ -418,6 +428,14 @@ struct Clip: Codable, Equatable, Identifiable { /// Source time for a timeline moment inside the clip. func sourceTime(at t: Double) -> Double { srcIn + (t - start) * speed } + /// Inverse of `sourceTime(at:)`: the timeline moment inside the clip that + /// shows source second `s`. Clamped to the clip's own span so it can't jump + /// the playhead onto a neighbour. + func timelineTime(forSource s: Double) -> Double { + let t = speed != 0 ? start + (s - srcIn) / speed : start + return min(max(t, start), end) + } + init(mediaId: UUID?, track: TrackRef, start: Double, srcIn: Double, duration: Double, kind: ClipKind = .video, linkId: UUID? = nil, board: Board? = nil) { @@ -572,14 +590,23 @@ extension ProjectModel { by: \.track) for (tref, arr) in grouped { if let ref, tref != ref { continue } - let sorted = arr.sorted { ($0.start, $0.id.uuidString) < ($1.start, $1.id.uuidString) } + // Tiebreak on the id only when starts collide — building a + // uuidString per comparison made sorting a dense lane allocate + // thousands of strings. + let sorted = arr.sorted { + $0.start != $1.start ? $0.start < $1.start + : $0.id.uuidString < $1.id.uuidString + } for i in 0..= a.end - 1e-9 { break } + out.append(ClipOverlap(a: a, b: b, track: tref, + start: b.start, end: min(a.end, b.end))) } } } diff --git a/sequencer/Sources/Sequencer/PerfTest.swift b/sequencer/Sources/Sequencer/PerfTest.swift index 339bd30a2d46a3b191520fca213e3698e28891ab..af76a2f20c380ece79137f6731bfe36fc9290b08 100644 --- a/sequencer/Sources/Sequencer/PerfTest.swift +++ b/sequencer/Sources/Sequencer/PerfTest.swift @@ -44,7 +44,6 @@ func runPerfTest(path: String) { // runloop until the loads land, so we then measure real thumbnail blits. func warmThumbnails() { ctx.session.showFilmstrips = true - timeline.testSetScrolling(false) NSGraphicsContext.saveGraphicsState() NSGraphicsContext.current = gctx for _ in 0..<5 { @@ -57,20 +56,34 @@ func runPerfTest(path: String) { NSGraphicsContext.restoreGraphicsState() } - // Pan across the first half of the timeline over `frames` steps, timing each - // direct draw. Returns mean ms/frame. - func measure(scrolling: Bool, frames: Int = 120) -> Double { - timeline.testSetScrolling(scrolling) + // Pan the timeline over `frames` steps, timing each direct draw. Returns + // (median, p90) ms/frame — the median resists the I/O-contention spikes + // that async thumbnail loads inject into a mean. + // + // Two pan patterns: `panPxPerFrame` nil teleports across half the project + // (cache-hostile — approximates fresh zooms and scrubber jumps); a value + // pans that many px per frame like a real scroll gesture. + func measure(frames: Int = 120, panPxPerFrame: Double? = nil) + -> (median: Double, p90: Double) { NSGraphicsContext.saveGraphicsState() NSGraphicsContext.current = gctx defer { NSGraphicsContext.restoreGraphicsState() } for _ in 0..<3 { timeline.testRedraw() } // warm up - let t0 = Date() + var times: [Double] = [] + times.reserveCapacity(frames) for i in 0.. 0.5 swap = sharperProxy && had == nil - && !ctx.chunks.originalPlayable(media: media) + && (!ctx.chunks.originalPlayable(media: media) || parkedOff) } else { // PLAYING: adopt the proxy the moment it covers the playhead // (a NAS original may stutter), throttled so rapid @@ -352,7 +366,11 @@ final class TrackPlayer { /// pass) seeks it to the live position and resumes. Without the hold it /// blips wrong content from the file's head — audibly so on the audio track. private func installItem(_ item: AVPlayerItem) { - item.preferredForwardBufferDuration = lenientSync ? 8 : 1 + // Video buffer depth scales with the RAM budget (Settings → Global): + // the default 2 GB keeps the old 1s; a workstation-sized budget buys + // deeper read-ahead, which is what smooths NAS originals. + item.preferredForwardBufferDuration = lenientSync ? 8 + : Double(min(8, max(1, FrameCache.ramGB / 2))) let wasPlaying = player.rate != 0 player.replaceCurrentItem(with: item) if wasPlaying { player.rate = 0 } @@ -438,8 +456,13 @@ final class VideoTrackPlayer { var currentClipId: UUID? { front.currentClipId } /// Spin up the next clip's buffer this many seconds before its cut — enough - /// to load + decode the first frame even off a NAS original. - private static let preroll = 1.5 + /// to load + decode the first frame even off a NAS original or a stitched + /// composition whose seek has to open a fresh chunk file. + private static let preroll = 4.0 + /// Warm exact cut frames (heads AND tails) into the RAM frame cache when + /// their boundary is within this many seconds of the playhead, so even a + /// swap that outruns the preroll has the right frame to stand in. + private static let warmRadius = 10.0 func sync(ref: TrackRef, playhead: Double, rate: Double, playheadMoved: Bool, force: Bool) { @@ -462,9 +485,11 @@ final class VideoTrackPlayer { } } - // BACK: preroll the next different-media clip when its cut is imminent, - // parked (rate 0) and muted at its first frame. Forward playback only — - // a reverse/scrub cut falls back to the plain (tiny-gap) swap. + // BACK: preroll the boundary clip when its cut is imminent, parked + // (rate 0) and muted at the frame the cut lands on: forward playback + // prerolls the NEXT different-media clip at its first frame; reverse + // prerolls the PREVIOUS one at its last, so a backwards cut is just as + // gapless. var buffering = false if rate >= 0, let next = nextDifferentClip(on: ref, after: playhead, current: cur), next.start - playhead <= Self.preroll, @@ -475,10 +500,46 @@ final class VideoTrackPlayer { back.player.isMuted = true back.syncTime(expected: next.srcIn, rate: 0, force: false) buffering = true + } else if rate < 0, let prev = prevDifferentClip(on: ref, before: playhead, current: cur), + playhead - prev.end <= Self.preroll, + let prevMedia = project.media(prev.mediaId) { + let tail = max(prev.srcIn, prev.sourceTime(at: prev.end) - 0.05) + if back.currentClipId != prev.id { + back.setClip(prev, media: prevMedia, sourceTime: tail) + } + back.player.isMuted = true + back.syncTime(expected: tail, rate: 0, force: false) + buffering = true } if !buffering, back.currentClipId != nil { back.setClip(nil, media: nil) // release the idle buffer } + + warmBoundaryFrames(ref: ref, playhead: playhead, project: project) + } + + /// Warm the exact frames every nearby cut will need into the RAM frame + /// cache — clip heads for forward crossings, clip tails for reverse — so + /// the beat between "boundary crossed" and "player ready" shows the real + /// frame instead of a filmstrip thumb or a spinner. Self-deduping (the + /// cache remembers, decodes are backgrounded), so per-tick calls are cheap. + private func warmBoundaryFrames(ref: TrackRef, playhead: Double, project: ProjectModel) { + for clip in project.clips where clip.track == ref && clip.kind == .video { + guard abs(clip.start - playhead) <= Self.warmRadius + || abs(clip.end - playhead) <= Self.warmRadius, + let media = project.media(clip.mediaId), !media.isAudio else { continue } + if abs(clip.start - playhead) <= Self.warmRadius { + FrameCache.shared.warm( + mediaKey: media.cacheKey, at: clip.srcIn, + source: ctx.chunks.frameSource(media: media, sourceTime: clip.srcIn)) + } + if abs(clip.end - playhead) <= Self.warmRadius { + let tail = max(clip.srcIn, clip.sourceTime(at: clip.end) - 0.05) + FrameCache.shared.warm( + mediaKey: media.cacheKey, at: tail, + source: ctx.chunks.frameSource(media: media, sourceTime: tail)) + } + } } /// The next clip on this track (in time) whose media differs from `current` @@ -492,6 +553,16 @@ final class VideoTrackPlayer { .min { $0.start < $1.start } } + /// The mirror for reverse playback: the previous clip (in time) whose media + /// differs — the cut a rewinding playhead will cross next. + private func prevDifferentClip(on ref: TrackRef, before t: Double, + current: Clip?) -> Clip? { + ctx.store.project.clips + .filter { $0.track == ref && $0.kind == .video && $0.end < t + 1e-6 + && $0.mediaId != current?.mediaId } + .max { $0.end < $1.end } + } + func clear() { a.player.replaceCurrentItem(with: nil) b.player.replaceCurrentItem(with: nil) diff --git a/sequencer/Sources/Sequencer/SessionState.swift b/sequencer/Sources/Sequencer/SessionState.swift index 477b017dfb8c893afc83e0fa6bf446b869bf74e4..e7077fe778fa300cfe7b7df20a07d95831d6151e 100644 --- a/sequencer/Sources/Sequencer/SessionState.swift +++ b/sequencer/Sources/Sequencer/SessionState.swift @@ -12,6 +12,10 @@ final class SessionState { var snapping = true { didSet { postViewOptions() } } var showFilmstrips = true { didSet { postViewOptions() } } + /// Show the clover-recording transcript as synchronized captions over the + /// viewer (only ever visible when a `cam.mov` with a sibling `transcript.json` + /// is under the playhead). + var subtitlesEnabled = true { didSet { postViewOptions() } } /// Vertical zoom: multiplies the base lane height for all tracks. var laneScale: CGFloat = 1 { didSet { @@ -24,6 +28,11 @@ final class SessionState { var hiddenTracks: Set = [] { didSet { postViewOptions() } } var focusedTracks: Set = [] { didSet { postViewOptions() } } + /// The timeline span currently on screen (set by TimelineView as it + /// scrolls/zooms). The proxy builder treats it as a heat anchor — the + /// user scrubs inside what they can see. Not persisted; no notification + /// (read lazily by the chunk scheduler). + var visibleTimeRange: ClosedRange? /// The Fusion comps band gets its own hide/focus. var fusionHidden = false { didSet { postViewOptions() } } var fusionFocus = false { didSet { postViewOptions() } } @@ -101,10 +110,12 @@ final class SessionState { v.laneScale = Double(laneScale) v.snapping = snapping v.showFilmstrips = showFilmstrips + v.subtitles = subtitlesEnabled v.previewsOnLeft = previewsOnLeft v.priorityPane = priorityPane v.fusionHidden = fusionHidden v.fusionFocus = fusionFocus + v.playhead = ctx.playback.playhead return v } @@ -117,10 +128,12 @@ final class SessionState { laneScale = CGFloat(v.laneScale) snapping = v.snapping showFilmstrips = v.showFilmstrips + subtitlesEnabled = v.subtitles previewsOnLeft = v.previewsOnLeft priorityPane = v.priorityPane fusionHidden = v.fusionHidden fusionFocus = v.fusionFocus + if v.playhead > 0 { ctx.playback.seek(to: v.playhead) } } private func postViewOptions() { diff --git a/sequencer/Sources/Sequencer/Store.swift b/sequencer/Sources/Sequencer/Store.swift index 4a245b1f786f08a86a9b485f15f495ec6fbeed62..2f5f8e4ea8ca535d09b311d710d59c47ce96cbfe 100644 --- a/sequencer/Sources/Sequencer/Store.swift +++ b/sequencer/Sources/Sequencer/Store.swift @@ -1,4 +1,4 @@ -import Foundation +import AppKit extension Notification.Name { static let projectChanged = Notification.Name("projectChanged") @@ -36,8 +36,18 @@ final class Store { var playhead: Double } - private var undoStack: [Snapshot] = [] - private var redoStack: [Snapshot] = [] + /// One step on the undo timeline. Model edits and storyboard DRAWING edits + /// share this single stack so ⌘Z / ⌘⇧Z step through them in the exact order + /// they happened — regardless of which window is focused — and drawing is + /// redoable like everything else. (Raster pixels live in `BoardStore`, out of + /// the value-type model; the entry just carries the image to restore.) + private enum UndoEntry { + case model(Snapshot) + case raster(boardId: UUID, image: NSImage?) // nil image = board was blank + } + + private var undoStack: [UndoEntry] = [] + private var redoStack: [UndoEntry] = [] private var gestureBase: Snapshot? var canUndo: Bool { !undoStack.isEmpty || gestureBase != nil } @@ -70,6 +80,7 @@ final class Store { project = copy pruneSelection() changed() + noteEditLocus(before: before.model, after: copy) } /// Continuous-gesture mutations (drags): one undo entry for the whole @@ -97,24 +108,58 @@ final class Store { /// base snapshot and would discard the gesture's changes. func endGesture(finalize: ((inout ProjectModel) -> Void)? = nil) { guard let base = gestureBase else { return } + // Did the drag move anything on screen (mid-gesture posts drew it)? + let diverged = project != base.model var copy = project finalize?(©) copy.normalizeStoryboards() // A drag that vacated a bottom lane leaves it as an ephemeral drop // target — collapse it (interior lanes and the clip's own lane stay). copy.pruneTrailingEmptyTracks() - if copy != project { - project = copy - post(.projectChanged) - } + project = copy gestureBase = nil + // Post `.projectChanged` exactly once. A net change goes through + // `changed()` (undo + dirty + the post). A net-ZERO drag that still moved + // things mid-gesture (dragged out and back, ephemeral lane pruned) has no + // undo entry, but the view is drawing the mid-gesture state — refresh it. + // A no-op gesture (a click that never dragged) posts nothing. if project != base.model { pushUndo(base) + noteEditLocus(before: base.model, after: project) pruneSelection() changed() + } else if diverged { + post(.projectChanged) } } + /// Report where an edit landed on the timeline — the proxy builder keeps + /// chunks around recent edit sites warm (built early, evicted late), since + /// editors scrub and re-play around where they're cutting. + private func noteEditLocus(before: ProjectModel, after: ProjectModel) { + var old: [UUID: Clip] = [:] + for c in before.clips { old[c.id] = c } + var times: [Double] = [] + func add(_ t: Double) { + guard times.count < 4, + !times.contains(where: { abs($0 - t) < 30 }) else { return } + times.append(t) + } + for c in after.clips where c.kind == .video && old[c.id] != c { + if times.count >= 4 { break } + guard let o = old[c.id] else { add(c.start); continue } // new clip + // Warm the EDGE that moved: a right-trim on a long clip should + // anchor at its out point, not the (possibly minutes-away) head. + let durChanged = abs(o.duration - c.duration) > 1e-9 + if durChanged, abs(o.end - c.end) > 1e-9 { add(c.end) } // tail edit + if abs(o.srcIn - c.srcIn) > 1e-9 || abs(o.start - c.start) > 1e-9 + || !durChanged { + add(c.start) // head edit, slip, move, or non-geometry change + } + } + ctx.chunks.noteEdits(times: times) + } + /// Live, non-undoable edit for a floating control that has no discrete /// start/end (the colour picker). Unlike a gesture it holds no open state, /// so timeline edits mid-preview can't trip the gesture precondition. @@ -143,19 +188,45 @@ final class Store { func undo() { if gestureBase != nil { cancelGesture(); return } - guard let prev = undoStack.popLast() else { return } - redoStack.append(snapshot()) - restore(prev) + guard let entry = undoStack.popLast() else { return } + switch entry { + case .model(let snap): + redoStack.append(.model(snapshot())) + restore(snap) + case .raster(let boardId, let image): + // Swap: what's on screen now becomes the redo target; restore the + // stored (pre-edit) drawing. + redoStack.append(.raster(boardId: boardId, image: ctx.boards.rasterSnapshot(boardId))) + ctx.boards.applyRaster(image, boardId: boardId) + } } func redo() { - guard let next = redoStack.popLast() else { return } - undoStack.append(snapshot()) - restore(next) + guard let entry = redoStack.popLast() else { return } + switch entry { + case .model(let snap): + undoStack.append(.model(snapshot())) + restore(snap) + case .raster(let boardId, let image): + undoStack.append(.raster(boardId: boardId, image: ctx.boards.rasterSnapshot(boardId))) + ctx.boards.applyRaster(image, boardId: boardId) + } + } + + /// Register a storyboard drawing edit on the shared undo timeline — called by + /// `BoardStore.endStroke` after it commits a stroke. `before` is the drawing + /// as it stood BEFORE the stroke, so undo restores it. + func recordRasterEdit(boardId: UUID, before: NSImage?) { + undoStack.append(.raster(boardId: boardId, image: before)) + if undoStack.count > 500 { undoStack.removeFirst() } + redoStack.removeAll() + // The stroke already marked the document dirty (via noteRasterChanged); + // this refreshes the Undo menu's enabled state. + post(.documentStateChanged) } private func pushUndo(_ snapshot: Snapshot) { - undoStack.append(snapshot) + undoStack.append(.model(snapshot)) if undoStack.count > 500 { undoStack.removeFirst() } redoStack.removeAll() } diff --git a/sequencer/Sources/Sequencer/Storyboard.swift b/sequencer/Sources/Sequencer/Storyboard.swift index 2d74e2d3042536c7837cd3e700b9b58a05080548..539c911b6cf9650f42dac21dfb7dc1fa1d54f987 100644 --- a/sequencer/Sources/Sequencer/Storyboard.swift +++ b/sequencer/Sources/Sequencer/Storyboard.swift @@ -134,9 +134,9 @@ final class BoardStore { // Board coordinates everywhere: top-left origin, y down. The engine owns // the y-flip into image space so callers never think about it. private var workingRasters: [UUID: NSImage] = [:] - private var strokeUndo: [UUID: [NSImage?]] = [:] - /// Boards in the order strokes were committed (global ⌘Z routing). - private(set) var strokeHistory: [UUID] = [] + /// Pre-stroke drawing captured at `beginStroke`, used by `endStroke` to push + /// one undo entry onto the shared `Store` timeline. + private var pendingStrokeBefore: [UUID: NSImage?] = [:] /// Raster to DISPLAY: the in-progress stroke image when one is active. func displayRaster(_ boardId: UUID) -> NSImage? { @@ -153,10 +153,9 @@ final class BoardStore { } func beginStroke(board: Board) { - var stack = strokeUndo[board.id] ?? [] - stack.append(rasterImage(board.id)?.copy() as? NSImage) - if stack.count > 24 { stack.removeFirst() } - strokeUndo[board.id] = stack + // Remember the pre-stroke drawing so the commit can register one undo + // step on the shared timeline. + pendingStrokeBefore[board.id] = rasterImage(board.id)?.copy() as? NSImage workingRasters[board.id] = (rasterImage(board.id)?.copy() as? NSImage) ?? blankRaster(size: board.size) } @@ -192,34 +191,23 @@ final class BoardStore { func endStroke(board: Board) { guard let img = workingRasters.removeValue(forKey: board.id) else { return } saveRaster(img, boardId: board.id) - strokeHistory.append(board.id) - if strokeHistory.count > 48 { strokeHistory.removeFirst() } - NotificationCenter.default.post(name: .mediaStatusChanged, object: nil) + // Register the completed stroke on the shared undo timeline. + let before = pendingStrokeBefore.removeValue(forKey: board.id) ?? nil + ctx.store.recordRasterEdit(boardId: board.id, before: before) + NotificationCenter.default.post(name: .mediaStatusChanged, object: nil, userInfo: ["scene": true]) } - func canUndoStroke(_ boardId: UUID) -> Bool { - !(strokeUndo[boardId] ?? []).isEmpty - } - var canUndoAnyStroke: Bool { - strokeHistory.last.map(canUndoStroke) ?? false + /// A copy of the current drawing for the undo timeline (nil = blank board). + func rasterSnapshot(_ boardId: UUID) -> NSImage? { + rasterImage(boardId)?.copy() as? NSImage } - @discardableResult - func undoStroke(_ boardId: UUID) -> Bool { - guard var stack = strokeUndo[boardId], let prev = stack.popLast() else { return false } - strokeUndo[boardId] = stack + /// Restore a drawing captured by `rasterSnapshot` (undo/redo of a stroke), + /// dropping any in-progress stroke and refreshing the editor + viewer. + func applyRaster(_ image: NSImage?, boardId: UUID) { workingRasters.removeValue(forKey: boardId) - saveRaster(prev, boardId: boardId) - if let i = strokeHistory.lastIndex(of: boardId) { strokeHistory.remove(at: i) } - NotificationCenter.default.post(name: .mediaStatusChanged, object: nil) - return true - } - - /// Undo the most recent stroke on any board. - @discardableResult - func undoLastStroke() -> Bool { - guard let boardId = strokeHistory.last else { return false } - return undoStroke(boardId) + saveRaster(image, boardId: boardId) + NotificationCenter.default.post(name: .projectChanged, object: nil) } /// Wipe the drawing layer (undoable as one stroke). diff --git a/sequencer/Sources/Sequencer/StoryboardEditor.swift b/sequencer/Sources/Sequencer/StoryboardEditor.swift index d585ce8ea445702f194bc20b48c1200e59e291dd..97471620000b5cd70e86d3b24ccc42b01b66a9b0 100644 --- a/sequencer/Sources/Sequencer/StoryboardEditor.swift +++ b/sequencer/Sources/Sequencer/StoryboardEditor.swift @@ -132,15 +132,6 @@ final class StoryboardEditor: NSObject, NSWindowDelegate { private var colorWell: NSColorWell? var isKeyEditor: Bool { window != nil && NSApp.keyWindow === window } - var canUndoRaster: Bool { isKeyEditor && (canvas?.canUndoRaster ?? false) } - - /// Raster strokes undo in their own lane while the editor is key; shape - /// edits ride the global Store undo like everything else. - func undoRasterIfKey() -> Bool { - guard canUndoRaster, let canvas else { return false } - canvas.undoRaster() - return true - } /// Open on a panel belonging to `ctx`'s document. The single editor window /// re-targets to whichever document asked for it. @@ -381,9 +372,6 @@ final class BoardCanvas: NSView { // Raster stroke state (pixels live in BoardStore's shared stroke engine) private var strokeActive = false private var lastStrokePoint: CGPoint? - var canUndoRaster: Bool { - board.map { boards.canUndoStroke($0.id) } ?? false - } // Shape gesture state private enum ShapeDrag { case none, create, move, resize } @@ -596,12 +584,6 @@ final class BoardCanvas: NSView { // MARK: Raster strokes (BoardStore's engine does the pixel work) - func undoRaster() { - guard let board else { return } - boards.undoStroke(board.id) - needsDisplay = true - } - private func strokeSegment(from a: CGPoint, to b: CGPoint, pressure: CGFloat) { guard let board, let width = tool.strokeWidth else { return } boards.strokeSegment( diff --git a/sequencer/Sources/Sequencer/Theme.swift b/sequencer/Sources/Sequencer/Theme.swift index 6f8d4633ed6ea53e7cdfdbe21dca26732be17893..97888fb757b13f7ca09227387c73abf9afb2a2c4 100644 --- a/sequencer/Sources/Sequencer/Theme.swift +++ b/sequencer/Sources/Sequencer/Theme.swift @@ -84,4 +84,27 @@ enum Theme { static var dragHint: NSColor { pick(NSColor(calibratedWhite: 0.3, alpha: 1), NSColor(calibratedWhite: 0.55, alpha: 1)) } + + // Optimization strip (the thin band under the ruler). Muted so a mostly + // red/yellow strip informs rather than alarms. + /// Proxy built at the full preview-quality target. + static var stripFull: NSColor { + pick(NSColor(calibratedHue: 0.36, saturation: 0.70, brightness: 0.62, alpha: 1), + NSColor(calibratedHue: 0.36, saturation: 0.65, brightness: 0.60, alpha: 1)) + } + /// Proxy built below target — usable now, an upgrade is still queued. + static var stripUsable: NSColor { + pick(NSColor(calibratedHue: 0.36, saturation: 0.50, brightness: 0.42, alpha: 1), + NSColor(calibratedHue: 0.36, saturation: 0.40, brightness: 0.72, alpha: 1)) + } + /// Building right now (or a rescue slice standing in). + static var stripBuilding: NSColor { + pick(NSColor(calibratedHue: 0.13, saturation: 0.80, brightness: 0.72, alpha: 1), + NSColor(calibratedHue: 0.13, saturation: 0.85, brightness: 0.80, alpha: 1)) + } + /// No proxy on disk (never built, evicted, or failed). + static var stripMissing: NSColor { + pick(NSColor(calibratedHue: 0.01, saturation: 0.65, brightness: 0.48, alpha: 1), + NSColor(calibratedHue: 0.01, saturation: 0.55, brightness: 0.75, alpha: 1)) + } } diff --git a/sequencer/Sources/Sequencer/TimelineView.swift b/sequencer/Sources/Sequencer/TimelineView.swift index e63f820b1a08045e0547d3973f8292be6109bea1..0f15f3c8e770151703fd36ab4624ec3d20a79630 100644 --- a/sequencer/Sources/Sequencer/TimelineView.swift +++ b/sequencer/Sources/Sequencer/TimelineView.swift @@ -7,8 +7,13 @@ enum DrawProf { static var acc: [String: Double] = [:] static var order: [String] = [] static var thumbHits = 0 // filmstrip images actually blitted (cache hits) + static var tileRenders = 0 // scene tiles rasterized (cache misses) + static var tileBlits = 0 // scene tiles blitted (cache hits + fresh) @inline(__always) static func t(_ label: String, _ body: () -> T) -> T { - if !on { return body() } + // Main-thread only: lanes rasterize in parallel, and racing the + // accumulator dictionaries would crash. Off-main work is simply + // not attributed (the enclosing main-thread section still is). + if !on || !Thread.isMainThread { return body() } let t0 = DispatchTime.now().uptimeNanoseconds let r = body() let dt = Double(DispatchTime.now().uptimeNanoseconds - t0) / 1e6 @@ -55,10 +60,26 @@ final class TimelineView: NSView { // New document ⇒ new model; the scene cache must rebuild even though // no `.projectChanged` fires for the context swap itself. sceneDirty = true + stripKey = nil + laneRefsCache = nil + flushTiles() } } private var store: Store { ctx.store } private var project: ProjectModel { ctx.store.project } + + /// The model's `laneRefs` getter scans EVERY clip (`hasStoryboard`) — + /// O(clips). The draw path reaches it per clip drawn (`drawClip → + /// clipRect → laneRect`), which is quadratic on big projects: 98% CPU + /// redraw storms. Cache it; invalidated on `.projectChanged`/ + /// `.viewOptionsChanged` (redraw()) and on document swap (ctx.didSet). + private var laneRefsCache: [TrackRef]? + private var laneRows: [TrackRef] { + if let rows = laneRefsCache { return rows } + let rows = project.laneRefs + laneRefsCache = rows + return rows + } private var playback: PlaybackController { ctx.playback } private var comps: FusionComps { ctx.comps } private var boards: BoardStore { ctx.boards } @@ -72,12 +93,21 @@ final class TimelineView: NSView { override init(frame: NSRect) { super.init(frame: frame) registerForDraggedTypes([.fileURL]) - for name: Notification.Name in [.projectChanged, .selectionChanged, - .mediaStatusChanged, .viewOptionsChanged, - .compsChanged] { + // Only model/view-option changes invalidate the grouped-clip scene + // cache. Selection changes just refresh the link-mate cache, and + // media-status/comps changes (thumbnails landing, comps rescans) only + // need a repaint — none of them regroup or re-sort thousands of clips. + for name: Notification.Name in [.projectChanged, .viewOptionsChanged] { NotificationCenter.default.addObserver(self, selector: #selector(redraw), name: name, object: nil) } + NotificationCenter.default.addObserver(self, selector: #selector(selectionRedraw), + name: .selectionChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(mediaRedraw(_:)), + name: .mediaStatusChanged, object: nil) + // The comps band isn't tiled — a comps rescan is repaint-only. + NotificationCenter.default.addObserver(self, selector: #selector(repaintOnly), + name: .compsChanged, object: nil) // Per-document: bound against the current (headless) ctx here, re-bound // when a real ctx is injected (see `ctx.didSet`). ctx.notify.addObserver(self, selector: #selector(playheadMoved), @@ -88,7 +118,37 @@ final class TimelineView: NSView { required init?(coder: NSCoder) { fatalError() } - @objc private func redraw() { sceneDirty = true; needsDisplay = true } + @objc private func redraw() { + sceneDirty = true + stripKey = nil + laneRefsCache = nil + flushTiles() + needsDisplay = true + } + + @objc private func repaintOnly() { needsDisplay = true } + + /// `.mediaStatusChanged` is posted for two very different things: scene + /// pixels changing (a filmstrip/waveform/board raster landed — posts carry + /// `scene: true`) and proxy-chunk bookkeeping (status bar / viewer badges — + /// no timeline pixels). Only the former invalidates the tile cache; chunk + /// churn during playback must not force full scene re-renders. + @objc private func mediaRedraw(_ note: Notification) { + if note.userInfo?["scene"] != nil { flushTiles() } + stripKey = nil // chunk states changed — recompute the strip's runs + needsDisplay = true + } + + @objc private func selectionRedraw() { + // Link-mates of the selection outline aqua; recompute just that set — + // a selection click must not regroup/re-sort the whole project. + if !sceneDirty { + linkedSelectionCache = project.expandLinks(store.selection) + .subtracting(store.selection) + } + flushTiles() // selection tint/borders are baked into the rasters + needsDisplay = true + } @objc private func playheadMoved() { // Auto-follow while playing. @@ -127,24 +187,31 @@ final class TimelineView: NSView { } private var frameDur: Double { 1.0 / project.fps } + /// The optimization strip: a thin band under the ruler showing, per + /// timeline column, how optimized the media the visible tracks need there + /// is (see drawOptimizeStrip). + private let stripH: CGFloat = 5 private var fusionBandH: CGFloat { comps.visible ? 46 : 0 } - private var lanesTop: CGFloat { rulerH + fusionBandH } + private var fusionTop: CGFloat { rulerH + stripH } + private var lanesTop: CGFloat { rulerH + stripH + fusionBandH } private func laneHeight(_ ref: TrackRef) -> CGFloat { - max(24, baseLaneH * session.laneScale * (session.trackHeights[ref] ?? 1)) + // Device-pixel quantized so lane rasters (scene tiles) blit without + // resampling; the ≤ half-device-pixel rounding is imperceptible. + quantized(max(24, baseLaneH * session.laneScale * (session.trackHeights[ref] ?? 1))) } - private var defaultLaneH: CGFloat { max(24, baseLaneH * session.laneScale) } + private var defaultLaneH: CGFloat { quantized(max(24, baseLaneH * session.laneScale)) } /// Total height of all lanes (for vertical scroll clamping). private var lanesContentHeight: CGFloat { - project.laneRefs.reduce(laneGap) { $0 + laneHeight($1) + laneGap } + laneRows.reduce(laneGap) { $0 + laneHeight($1) + laneGap } } private var maxScrollY: CGFloat { max(0, lanesContentHeight - (bounds.height - lanesTop) + defaultLaneH) } private func laneRect(row: Int) -> NSRect { - let rows = project.laneRefs + let rows = laneRows var y = lanesTop + laneGap - scrollY for (i, ref) in rows.enumerated() { let h = laneHeight(ref) @@ -161,7 +228,7 @@ final class TimelineView: NSView { private func rowAt(y: CGFloat) -> Int? { guard y > lanesTop else { return nil } var yy = lanesTop + laneGap - scrollY - let rows = project.laneRefs + let rows = laneRows for (i, ref) in rows.enumerated() { let h = laneHeight(ref) if y < yy + h + laneGap { return i } @@ -173,7 +240,7 @@ final class TimelineView: NSView { /// Row whose bottom edge is under the cursor (for track-height resizing). private func trackBoundaryAt(y: CGFloat) -> Int? { guard y > lanesTop else { return nil } - let rows = project.laneRefs + let rows = laneRows var yy = lanesTop + laneGap - scrollY for (i, ref) in rows.enumerated() { yy += laneHeight(ref) @@ -191,15 +258,17 @@ final class TimelineView: NSView { /// The lane shown at a row, or nil past the last real lane (ghost rows). private func laneRef(row: Int) -> TrackRef? { - let rows = project.laneRefs + let rows = laneRows return rows.indices.contains(row) ? rows[row] : nil } private func clipAt(point: NSPoint) -> (clip: Clip, row: Int)? { guard let row = rowAt(y: point.y), let ref = laneRef(row: row) else { return nil } + // The cached per-lane arrays are already start-sorted — no need to + // re-filter and re-sort the whole project per mouse event. + rebuildSceneIfNeeded() // Later clips draw on top, so hit-test in reverse. - for clip in project.clips.filter({ $0.track == ref }) - .sorted(by: { $0.start < $1.start }).reversed() { + for clip in (clipsByLane[ref] ?? []).reversed() { if clipRect(clip, row: row).contains(point) { return (clip, row) } } return nil @@ -207,7 +276,8 @@ final class TimelineView: NSView { private func overlapAt(point: NSPoint) -> ClipOverlap? { guard let row = rowAt(y: point.y), let ref = laneRef(row: row) else { return nil } - for o in project.overlaps(on: ref) { + rebuildSceneIfNeeded() + for o in overlapsByLane[ref] ?? [] { let lane = laneRect(row: row) let r = NSRect(x: xFor(o.start), y: lane.minY, width: max(2, xFor(o.end) - xFor(o.start)), height: lane.height) @@ -228,6 +298,16 @@ final class TimelineView: NSView { // large project is now a redraw of cached geometry, not a full recompute. private var clipsByLane: [TrackRef: [Clip]] = [:] private var overlapsByLane: [TrackRef: [ClipOverlap]] = [:] + /// Ids of clips that are part of an overlap, per lane — hoisted out of + /// `drawLane`, which used to rebuild this Set per lane per frame. + private var overlapIdsByLane: [TrackRef: Set] = [:] + /// Longest clip duration per lane — bounds the binary-searched draw/culling + /// window (a clip can start at most this far left of the view and still be + /// visible). + private var maxClipDurByLane: [TrackRef: Double] = [:] + /// `project.timelineDuration` scans every clip; cached here so per-frame + /// chrome (scrollbars, origin clamping) doesn't rescan 4k+ clips. + private var cachedTimelineDuration: Double = 0 private var mediaById: [UUID: MediaItem] = [:] private var sceneDirty = true @@ -239,6 +319,43 @@ final class TimelineView: NSView { .foregroundColor: NSColor(calibratedWhite: 0.92, alpha: 1), ] + /// Clip titles as cached CTLines. `NSString.draw` runs a full CoreText + /// layout per call — ~200 visible labels re-laid-out every cold frame, + /// even though a multicam project has a handful of distinct names. Layout + /// once per distinct title, then `CTLineDraw` is a glyph blit. Capped: + /// titles are media names + panel names, a small closed set in practice. + private static var titleLineCache: [String: (line: CTLine, ascent: CGFloat)] = [:] + private static let titleLineLock = NSLock() // lanes rasterize in parallel + private static func titleLine(_ s: String) -> (line: CTLine, ascent: CGFloat) { + titleLineLock.lock() + defer { titleLineLock.unlock() } + if let c = titleLineCache[s] { return c } + if titleLineCache.count > 512 { titleLineCache.removeAll(keepingCapacity: true) } + let line = CTLineCreateWithAttributedString( + NSAttributedString(string: s, attributes: titleAttrs)) + var ascent: CGFloat = 0 + CTLineGetTypographicBounds(line, &ascent, nil, nil) + let entry = (line, ascent) + titleLineCache[s] = entry + return entry + } + + /// Draw a cached title line with its top-left at `point` in the flipped + /// view space — positioned to match what `NSString.draw(at:)` produced. + private func drawTitle(_ s: String, at point: NSPoint) { + guard let cg = NSGraphicsContext.current?.cgContext else { return } + let (line, ascent) = Self.titleLine(s) + cg.saveGState() + // Flipped context: un-flip locally for glyph drawing. The box-top to + // baseline offset is the line's ascent, matching NSString.draw(at:). + cg.translateBy(x: point.x, y: point.y + ascent) + cg.scaleBy(x: 1, y: -1) + cg.textMatrix = .identity + cg.textPosition = .zero + CTLineDraw(line, cg) + cg.restoreGState() + } + /// SF Symbols, tinted and baked into flat bitmaps, cached by `key`. Building /// an SF Symbol and tinting it (`tinted` does a `lockFocus` composite) *per /// draw* was the single biggest timeline draw cost: nearly every clip is @@ -246,9 +363,12 @@ final class TimelineView: NSView { /// the track-header buttons re-baked every frame too. Cached, drawing a badge /// is a plain blit. Main-thread only (all drawing is). private static var symbolCache: [String: NSImage] = [:] + private static let symbolCacheLock = NSLock() // lanes rasterize in parallel static func bakedSymbol(_ name: String, pointSize: CGFloat = 0, weight: NSFont.Weight = .regular, tint: NSColor, key: String) -> NSImage? { + symbolCacheLock.lock() + defer { symbolCacheLock.unlock() } if let img = symbolCache[key] { return img } var base = NSImage(systemSymbolName: name, accessibilityDescription: name) if pointSize > 0 { @@ -281,25 +401,28 @@ final class TimelineView: NSView { bakedSymbol("link", tint: .white, key: "badge.link") } - /// True while the user is actively scrolling/zooming (cleared ~120 ms after - /// the last scroll event, which triggers one full-detail redraw). On its own - /// it changes nothing — clips draw at full detail while scrolling. It only - /// gates the `lightScroll` fallback: a *very dense* frame (see - /// `denseScrollClips`) sheds the heavy filmstrip/waveform pass mid-scroll so - /// a pathological timeline can't stall, restoring detail the moment it settles. - private var isScrolling = false - private var scrollSettleToken = 0 - - /// Mark a scroll/zoom in progress and schedule the settle redraw. - private func noteScrolling() { - isScrolling = true - scrollSettleToken &+= 1 - let token = scrollSettleToken - DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { [weak self] in - guard let self, self.scrollSettleToken == token else { return } - self.isScrolling = false - self.needsDisplay = true + /// Index range of a lane's (start-sorted) clips that can intersect the + /// time window — binary-searched so a frame's work is proportional to + /// what's on screen, not to how far into a 10-hour project it sits. The + /// lower bound backs off by the lane's longest clip (any clip starting + /// earlier than that has necessarily ended before `left`); the upper bound + /// is the first start past `right`. Callers still skip the odd short clip + /// inside the backoff span with an `end < left` check. + private func visibleIndexRange(_ clips: [Clip], lane: TrackRef, + left: Double, right: Double) -> Range { + let backoff = left - (maxClipDurByLane[lane] ?? 0) + var lo = 0, hi = clips.count + while lo < hi { // first index with start >= backoff + let mid = (lo + hi) / 2 + if clips[mid].start < backoff { lo = mid + 1 } else { hi = mid } } + let first = lo + hi = clips.count + while lo < hi { // first index with start > right + let mid = (lo + hi) / 2 + if clips[mid].start <= right { lo = mid + 1 } else { hi = mid } + } + return first.. right { break } - if c.end < left { continue } + for ref in laneRows { + let clips = clipsByLane[ref] ?? [] + for i in visibleIndexRange(clips, lane: ref, left: left, right: right) + where clips[i].end >= left { n += 1 } } return n } - /// While actively scrolling a *very dense* frame, shed the heavy filmstrip/ - /// waveform pass so the pan stays fluid; it returns the instant the scroll - /// settles. Ordinary-density views keep full detail — thumbnails, waveforms - /// and labels — even mid-scroll, so nothing visibly "drops out" in normal use. - private var lightScroll = false - private static let denseScrollClips = 350 + /// Below this on-screen width a clip physically cannot show its corner + /// radius, title strip text, badges, or a meaningful filmstrip/waveform + /// column — so `drawClip` takes a flat-rect fast path that produces the + /// same pixels for a fraction of the cost. Detail is a function of what's + /// resolvable at the current zoom, never of whether the user is scrolling: + /// this replaces the old `lightScroll` mode, which shed thumbnails and + /// labels mid-pan (visibly) yet saved almost nothing — the real per-clip + /// cost was the path/clip-state chrome that ran for sub-pixel clips. + private static let lodMinWidth: CGFloat = 3 + + // MARK: - Scene tile cache + // + // The scene (lane clips: bodies, filmstrips, waveforms, strips, labels, + // badges, selection tint) is rasterized into per-lane, 512-pt-wide tiles in + // TIMELINE space — a tile's x axis is (t − sliceStart)·pxPerSecond, which + // is independent of the pan origin. Panning and the 60 Hz playhead redraw + // therefore cost a handful of blits plus the (cheap) chrome, instead of + // re-rendering thousands of clips. Anything that changes scene pixels — + // model edits, selection, thumbnails landing, theme, zoom, lane heights — + // flushes; tiles rebuild lazily, visible-first, at direct-render cost. + // Active edit gestures bypass tiles entirely (direct draw), so a drag never + // thrashes the cache mid-gesture. + + private struct TileKey: Hashable { + let ref: TrackRef + let slice: Int + } + /// Everything a tile's pixels depend on besides the model/selection (those + /// flush via notifications). A mismatch flushes the whole cache. + private struct TileParams: Equatable { + var pps: Double + var scale: CGFloat + var light: Bool + var heights: [TrackRef: CGFloat] + var showFilmstrips: Bool + } + private var tiles: [TileKey: CGImage] = [:] + private var tileUse: [TileKey: Int] = [:] + private var tileTick = 0 + private var tileParams: TileParams? + private static let tileW: CGFloat = 512 + /// ~96 tiles ≈ a few viewports of 2× lane strips (~100 MB worst case). + private static let maxTiles = 96 + /// Cap on tile rasterizations per frame. A cold viewport (fresh zoom, big + /// jump) draws the un-cached slices directly this frame — same pixels, + /// direct cost — and fills the cache over the next few frames instead of + /// paying ~25 bitmap allocations in one frame. + private static let tileRendersPerFrame = 6 + private var tileRenderBudget = 0 + private var tileFillPending = false + private var lastDrawOrigin = 0.0 + private var lastDrawPps = 0.0 + /// Escape hatch for A/B measurement and debugging: SEQ_NOTILES=1 forces + /// the direct (Stage-1) render path. Mutable so --perftest can A/B both + /// paths inside one process (immune to thermal drift between runs). + static var tilesDisabled = + ProcessInfo.processInfo.environment["SEQ_NOTILES"] == "1" + + private func flushTiles() { + tiles.removeAll(keepingCapacity: true) + tileUse.removeAll(keepingCapacity: true) + } + + /// Device-pixel scale of the surface actually being drawn into, captured + /// from the context CTM at the top of `draw`. Falling back to the window's + /// backing scale is only right when they agree — an offscreen 1× target + /// (like --perftest's bitmap) on a 2× machine would otherwise get 2× tiles + /// downsampled on every blit. + private var renderScale: CGFloat = 2 + + private var backingScale: CGFloat { + window?.backingScaleFactor ?? NSScreen.main?.backingScaleFactor ?? 2 + } + + /// Quantize a length/offset to the device-pixel grid. Tiles are blitted at + /// integral device pixels; quantizing the shared geometry (origin, scroll, + /// lane heights) keeps the tile and direct paths on the same sub-pixel + /// phase, so their output is pixel-identical and blits never resample. + private func quantized(_ v: CGFloat) -> CGFloat { + (v * renderScale).rounded() / renderScale + } + + private func evictTilesIfNeeded() { + guard tiles.count > Self.maxTiles else { return } + for (key, _) in tileUse.sorted(by: { $0.value < $1.value }) + .prefix(tiles.count - Self.maxTiles) { + tiles[key] = nil + tileUse[key] = nil + } + } + + /// Blit the lane's visible tiles. Missing tiles are rendered up to the + /// per-frame budget; past it, their span draws directly (identical pixels) + /// and a follow-up display pass finishes filling the cache. + private func blitLaneTiles(ref: TrackRef, lane: NSRect) { + guard let cg = NSGraphicsContext.current?.cgContext else { return } + let sliceDur = Double(Self.tileW) / pxPerSecond + let leftSec = secondsFor(headerW) + let rightSec = secondsFor(bounds.width) + var slice = Int(floor(leftSec / sliceDur)) + let last = Int(floor(rightSec / sliceDur)) + while slice <= last { + let x = xFor(Double(slice) * sliceDur) + if let img = tileImage(ref: ref, lane: lane, slice: slice, sliceDur: sliceDur) { + // CGContext blit (NSImage.draw pays rep-matching/colorspace + // overhead per call). Local flip: CGImages draw bottom-up. + cg.saveGState() + cg.translateBy(x: x, y: lane.maxY) + cg.scaleBy(x: 1, y: -1) + cg.draw(img, in: CGRect(x: 0, y: 0, width: Self.tileW, + height: lane.height)) + cg.restoreGState() + if DrawProf.on { DrawProf.tileBlits += 1 } + } else { + // Over budget this frame: draw the slice's span directly. + NSGraphicsContext.current?.saveGraphicsState() + NSRect(x: x, y: lane.minY, width: Self.tileW, + height: lane.height).clip() + drawLaneClips(ref: ref, lane: lane, cullX0: x, cullX1: x + Self.tileW) + NSGraphicsContext.current?.restoreGraphicsState() + scheduleTileFill() + } + slice += 1 + } + } + + /// One coalesced follow-up display pass to keep rasterizing missed tiles + /// after a budget-limited frame. + private func scheduleTileFill() { + guard !tileFillPending else { return } + tileFillPending = true + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.tileFillPending = false + self.needsDisplay = true + } + } + + /// The cached tile for (lane, slice), rendering it via the SAME + /// `drawLaneClips` code the direct path uses — pixel-equivalent by + /// construction. The context is translated so the slice's start lands at + /// x = 0 and the lane's top at y = 0; both offsets cancel the pan origin, + /// which is what makes the raster reusable across scroll positions. + private func tileImage(ref: TrackRef, lane: NSRect, slice: Int, + sliceDur: Double) -> CGImage? { + let key = TileKey(ref: ref, slice: slice) + tileTick += 1 + tileUse[key] = tileTick + if let img = tiles[key] { return img } + guard tileRenderBudget > 0 else { return nil } + tileRenderBudget -= 1 + + let scale = renderScale + let pxW = Int((Self.tileW * scale).rounded()) + let pxH = Int((lane.height * scale).rounded()) + // Native Quartz raster format (BGRA premultiplied, little-endian) in + // the window's own colorspace: blits are then straight memory copies. + // An NSBitmapImageRep here (RGBA, deviceRGB) costs a per-blit swizzle + // + colorspace conversion — ~1.5 ms per tile, wiping out the caching. + let space = window?.colorSpace?.cgColorSpace + ?? CGColorSpace(name: CGColorSpace.sRGB)! + guard pxW > 0, pxH > 0, + let cg = CGContext( + data: nil, width: pxW, height: pxH, bitsPerComponent: 8, + bytesPerRow: 0, space: space, + bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue + | CGBitmapInfo.byteOrder32Little.rawValue) else { return nil } + + NSGraphicsContext.saveGraphicsState() + // Map: device pixels → points, unflipped → flipped (the view draws + // top-down), view space → tile space. Wrapped in a `flipped: true` + // NSGraphicsContext so text and images orient correctly. + cg.scaleBy(x: scale, y: scale) + cg.translateBy(x: 0, y: lane.height) + cg.scaleBy(x: 1, y: -1) + let x0 = xFor(Double(slice) * sliceDur) + cg.translateBy(x: -x0, y: -lane.minY) + NSGraphicsContext.current = NSGraphicsContext(cgContext: cg, flipped: true) + drawLaneClips(ref: ref, lane: lane, cullX0: x0, cullX1: x0 + Self.tileW) + NSGraphicsContext.restoreGraphicsState() + + guard let img = cg.makeImage() else { return nil } + tiles[key] = img + evictTilesIfNeeded() + if DrawProf.on { DrawProf.tileRenders += 1 } + return img + } + + /// Live draw-time telemetry, on when the app is launched with + /// SEQ_DRAWPROF=1 in the environment (run the binary directly with stderr + /// to a file — NSLog is invisible under `open`). Prints one line per + /// second: draws that second and mean/max ms per draw. Purely additive so + /// GUI perf work can be judged against real numbers, like --perftest. + private static let liveProf = ProcessInfo.processInfo.environment["SEQ_DRAWPROF"] == "1" + private var profWindowStart = CACurrentMediaTime() + private var profFrames = 0 + private var profTotalMs = 0.0 + private var profMaxMs = 0.0 override func draw(_ dirtyRect: NSRect) { + let profT0 = Self.liveProf ? CACurrentMediaTime() : 0 + defer { + if Self.liveProf { + let ms = (CACurrentMediaTime() - profT0) * 1000 + profFrames += 1 + profTotalMs += ms + profMaxMs = max(profMaxMs, ms) + let now = CACurrentMediaTime() + if now - profWindowStart >= 1.0 { + FileHandle.standardError.write(Data(String(format: + "[drawprof] %d draws, avg %.2f ms, max %.2f ms\n", + profFrames, profTotalMs / Double(profFrames), + profMaxMs).utf8)) + profWindowStart = now + profFrames = 0 + profTotalMs = 0 + profMaxMs = 0 + } + } + } Theme.timelineBg.setFill() bounds.fill() + // Publish the on-screen time span — a heat anchor for the proxy + // builder (the user scrubs inside what they can see). Cheap enough + // to set every frame; read lazily by the chunk scheduler. + let visibleSpan = Double(bounds.width - headerW) / pxPerSecond + ctx.session.visibleTimeRange = originSecond...(originSecond + max(1, visibleSpan)) + DrawProf.t("scene") { rebuildSceneIfNeeded() } - lightScroll = isScrolling && visibleClipCount() > Self.denseScrollClips - scrollY = min(scrollY, maxScrollY) - let rows = project.laneRefs + DrawProf.t("cull") { + // Match the tile raster scale to the surface being drawn into. + if let ctm = NSGraphicsContext.current?.cgContext + .userSpaceToDeviceSpaceTransform { + let s = abs(ctm.a) + if s > 0.1, s != renderScale { + renderScale = s + flushTiles() + } + } + scrollY = quantized(min(scrollY, maxScrollY)) + // Snap the pan origin to the device-pixel grid (≤ half a device + // pixel, imperceptible) so tile blits land on integral pixels and + // the tile and direct paths share one sub-pixel phase. + let q = pxPerSecond * Double(renderScale) + originSecond = (originSecond * q).rounded() / q + } + // Route the frame: warm frames (rest, pan, playback) blit the tile + // cache; cold frames — a zoom in flight (every tile invalid), a + // teleport jump (a whole viewport of new slices), or a live edit + // gesture (model changes per event) — rasterize all lanes in parallel + // instead, and the cache refills once things settle. + let editing = store.gestureBaseModel != nil || drag.mode == .box + let jumped = abs(originSecond - lastDrawOrigin) * pxPerSecond + > Double(Self.tileW) + let zoomed = pxPerSecond != lastDrawPps + lastDrawOrigin = originSecond + lastDrawPps = pxPerSecond + let tiled = !Self.tilesDisabled && !editing && !zoomed && !jumped + tileRenderBudget = Self.tileRendersPerFrame + if tiled { + let params = TileParams( + pps: pxPerSecond, scale: renderScale, light: Theme.light, + heights: Dictionary(uniqueKeysWithValues: + laneRows.map { ($0, laneHeight($0)) }), + showFilmstrips: session.showFilmstrips) + if params != tileParams { + flushTiles() + tileParams = params + } + } else if zoomed || jumped { + // The gesture invalidated the cache; warm it back up as soon as + // the stream of cold frames stops. + flushTiles() + tileParams = nil + scheduleTileFill() + } + let rows = laneRows DrawProf.t("lanes") { - for row in 0.. (lo: Double, hi: Double) { let viewSec = Double(bounds.width - headerW) / pxPerSecond let lo = min(0, originSecond) - let hi = max(project.timelineDuration + 10, originSecond + viewSec) + rebuildSceneIfNeeded() + let hi = max(cachedTimelineDuration + 10, originSecond + viewSec) return (lo, hi) } @@ -491,33 +894,30 @@ final class TimelineView: NSView { return nil } - private func drawLane(row: Int, ref: TrackRef) { + private func drawLane(row: Int, ref: TrackRef, tiled: Bool) { let lane = laneRect(row: row) guard lane.minY < bounds.maxY, lane.maxY > lanesTop else { return } (ref == .storyboard ? Theme.storyboardLaneBg : Theme.laneBg).setFill() NSBezierPath(roundedRect: lane, xRadius: 4, yRadius: 4).fill() - let overlaps = overlapsByLane[ref] ?? [] - let overlappingIds = Set(overlaps.flatMap { [$0.a.id, $0.b.id] }) - - // Horizontal culling: clips are sorted by start, so once one starts past - // the right edge every later clip does too — stop. Clips ending before - // the left edge are skipped. Keeps the loop proportional to what's - // on-screen, not to the whole (possibly huge) lane. - let leftSec = originSecond - let rightSec = originSecond + Double(bounds.width - headerW) / pxPerSecond - let colors = laneColors(ref) - for clip in clipsByLane[ref] ?? [] { - if clip.start > rightSec { break } - if clip.end < leftSec { continue } - drawClip(clip, row: row, ref: ref, colors: colors, - overlapping: overlappingIds.contains(clip.id)) + if tiled { + blitLaneTiles(ref: ref, lane: lane) + } else { + drawLaneClips(ref: ref, lane: lane, + cullX0: headerW, cullX1: bounds.width) } + drawLaneDecorations(ref: ref, lane: lane) + } + + /// Pinned labels + overlap bands — main-thread, over blits or direct clips. + private func drawLaneDecorations(ref: TrackRef, lane: NSRect) { + drawPinnedLabels(ref: ref, lane: lane) // Bright red overlap ranges on top of the clip bodies. - for o in overlaps { + for o in overlapsByLane[ref] ?? [] { let r = NSRect(x: xFor(o.start), y: lane.minY + 1, width: max(2, xFor(o.end) - xFor(o.start)), height: lane.height - 2) + guard r.maxX > headerW, r.minX < bounds.width else { continue } NSColor.systemRed.withAlphaComponent(0.40).setFill() r.fill() NSColor.systemRed.setStroke() @@ -527,6 +927,123 @@ final class TimelineView: NSView { } } + // MARK: - Parallel direct rendering (cold frames) + + /// Persistent per-lane raster buffers for cold frames; recreated on size/ + /// scale change. ~7 MB per lane at 2× on a wide window — the price of + /// rasterizing a zoom gesture on every core instead of one. + private var laneBufs: [TrackRef: CGContext] = [:] + + /// A cold frame (zoom in flight, teleport jump, live edit gesture) must + /// re-render every visible lane from scratch — but lanes are independent, + /// so rasterize them CONCURRENTLY into per-lane buffers with the exact + /// same `drawLaneClips` code and composite on main. Same rasterizer, same + /// pixels; wall-clock is the slowest lane, not the sum. The storyboard + /// lane draws serially on main (BoardStore's raster cache isn't locked). + private func drawLanesDirectParallel(rows: [TrackRef]) { + var work: [(ref: TrackRef, lane: NSRect)] = [] + for (row, ref) in rows.enumerated() { + let lane = laneRect(row: row) + guard lane.minY < bounds.maxY, lane.maxY > lanesTop else { continue } + (ref == .storyboard ? Theme.storyboardLaneBg : Theme.laneBg).setFill() + NSBezierPath(roundedRect: lane, xRadius: 4, yRadius: 4).fill() + work.append((ref, lane)) + } + guard !work.isEmpty, let mainCG = NSGraphicsContext.current?.cgContext + else { return } + + let scale = renderScale + let pxW = Int((bounds.width * scale).rounded()) + let space = window?.colorSpace?.cgColorSpace + ?? CGColorSpace(name: CGColorSpace.sRGB)! + let ctxs: [CGContext?] = work.map { item in + if item.ref == .storyboard { return nil } + let pxH = Int((item.lane.height * scale).rounded()) + if let c = laneBufs[item.ref], c.width == pxW, c.height == pxH { + return c + } + let c = CGContext( + data: nil, width: pxW, height: pxH, bitsPerComponent: 8, + bytesPerRow: 0, space: space, + bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue + | CGBitmapInfo.byteOrder32Little.rawValue) + laneBufs[item.ref] = c + return c + } + + DispatchQueue.concurrentPerform(iterations: work.count) { i in + guard let cg = ctxs[i] else { return } // storyboard → main below + let (ref, lane) = work[i] + autoreleasepool { + cg.clear(CGRect(x: 0, y: 0, width: CGFloat(cg.width), + height: CGFloat(cg.height))) + cg.saveGState() + cg.scaleBy(x: scale, y: scale) + cg.translateBy(x: 0, y: lane.height) + cg.scaleBy(x: 1, y: -1) + cg.translateBy(x: 0, y: -lane.minY) + // NSGraphicsContext.current is thread-local — but + // concurrentPerform runs one of these iterations ON the + // calling (main) thread, so the view's own context must be + // restored, not nilled. + let prev = NSGraphicsContext.current + NSGraphicsContext.current = NSGraphicsContext(cgContext: cg, flipped: true) + drawLaneClips(ref: ref, lane: lane, + cullX0: headerW, cullX1: bounds.width) + NSGraphicsContext.current = prev + cg.restoreGState() + } + } + + for (i, item) in work.enumerated() { + if let cg = ctxs[i] { + if let img = cg.makeImage() { + mainCG.saveGState() + mainCG.translateBy(x: 0, y: item.lane.maxY) + mainCG.scaleBy(x: 1, y: -1) + mainCG.draw(img, in: CGRect(x: 0, y: 0, width: bounds.width, + height: item.lane.height)) + mainCG.restoreGState() + } + } else { + drawLaneClips(ref: item.ref, lane: item.lane, + cullX0: headerW, cullX1: bounds.width) + } + drawLaneDecorations(ref: item.ref, lane: item.lane) + } + } + + /// One lane's clips into the current context, culled and clamped to the + /// window [cullX0, cullX1] (view coordinates). The window is the view for + /// direct drawing and the tile's span (± bleed) when rendering a tile — + /// the drawing itself is identical, so the two paths are pixel-equivalent. + private func drawLaneClips(ref: TrackRef, lane: NSRect, + cullX0: CGFloat, cullX1: CGFloat) { + let overlappingIds = overlapIdsByLane[ref] ?? [] + // Bleed: fade handles / shot dividers / borders paint a few points + // past a clip's rect, so a clip just outside the window still owns + // pixels inside it. + let bleed: CGFloat = 8 + let leftSec = secondsFor(cullX0 - bleed) + let rightSec = secondsFor(cullX1 + bleed) + let colors = laneColors(ref) + let clips = clipsByLane[ref] ?? [] + // A storyboard panel's left divider depends on whether an earlier panel + // exists on the lane; the array is start-sorted, so any predecessor + // with a strictly smaller start means "has previous". + let range = visibleIndexRange(clips, lane: ref, left: leftSec, right: rightSec) + for i in range { + let clip = clips[i] + if clip.end < leftSec { continue } + let hasPrevPanel = ref == .storyboard && i > 0 + && clips[0].start < clip.start - 1e-6 + drawClip(clip, lane: lane, ref: ref, colors: colors, + overlapping: overlappingIds.contains(clip.id), + hasPrevPanel: hasPrevPanel, + cullX0: cullX0 - bleed, cullX1: cullX1 + bleed) + } + } + /// Circular SF-Symbol button in the header column (hide preview / focus). private func drawHeaderButton(_ symbolName: String, centerY: CGFloat, on: Bool) { drawHeaderButton(symbolName, in: NSRect(x: headerW / 2 - 8.5, y: centerY - 8.5, @@ -637,7 +1154,7 @@ final class TimelineView: NSView { private func drawFusionHeader() { guard fusionBandH > 0 else { return } - let band = NSRect(x: 0, y: rulerH, width: headerW, height: fusionBandH) + let band = NSRect(x: 0, y: fusionTop, width: headerW, height: fusionBandH) FusionComps.yellow.withAlphaComponent(session.fusionHidden ? 0.25 : 0.6).setFill() NSBezierPath(roundedRect: band.insetBy(dx: 2, dy: 2), xRadius: 3, yRadius: 3).fill() let attrs: [NSAttributedString.Key: Any] = [ @@ -656,7 +1173,7 @@ final class TimelineView: NSView { // become tracks. No resident placeholder lane. (Dropped FILES get their // own landing preview in drawFileDropPreview.) var rows: [Int] = [] - let count = project.laneRefs.count + let count = laneRows.count if drag.mode == .move, let row = dragHintRow, row >= count { rows = Array(count...row) } @@ -672,46 +1189,113 @@ final class TimelineView: NSView { } } - private func drawClip(_ clip: Clip, row: Int, ref: TrackRef, colors: LaneColors, - overlapping: Bool) { - let rect = clipRect(clip, row: row) - guard rect.maxX > headerW, rect.minX < bounds.width else { return } + private func drawClip(_ clip: Clip, lane: NSRect, ref: TrackRef, colors: LaneColors, + overlapping: Bool, hasPrevPanel: Bool, + cullX0: CGFloat, cullX1: CGFloat) { + let x0 = xFor(clip.start), x1 = xFor(clip.end) + let rect = NSRect(x: x0, y: lane.minY, width: max(2, x1 - x0), height: lane.height) + guard rect.maxX > cullX0, rect.minX < cullX1 else { return } let media = clip.mediaId.flatMap { mediaById[$0] } let color = colors.base let selected = store.selection.contains(clip.id) + let linkedSel = !selected && linkedSelectionCache.contains(clip.id) // Storyboard panels tile edge-to-edge (they're gapless) so the track // reads as one continuous filmstrip — square corners, no per-panel // card, dividers drawn between shots below. let storyboard = clip.kind == .storyboard + + func bodyFillColor() -> NSColor { + switch clip.kind { + case .storyboard: NSColor(calibratedWhite: 0.88, alpha: 1) + case .audio: colors.audioBody + case .video: media == nil ? NSColor(calibratedWhite: 0.25, alpha: 1) + : colors.videoBody + } + } + + // ---- Fast path: a clip too narrow to resolve any detail. ---- + // Below `lodMinWidth` the rounded corners, strip text, badges and + // filmstrip/waveform columns are physically invisible, so flat rect + // fills produce the same picture without the bezier/clip-state chrome + // that made dense zooms cost hundreds of ms. Everything that still + // reads at this size is kept: body + strip colors, the audio center + // line and fade handles (the texture of dense audio lanes), status + // tint/borders, storyboard dividers. + if rect.width < Self.lodMinWidth { + bodyFillColor().setFill() + rect.fill() + colors.strip.setFill() + NSRect(x: rect.minX, y: rect.minY, width: rect.width, height: 13).fill() + if clip.kind == .audio { + color.withAlphaComponent(0.35).setFill() + NSRect(x: rect.minX, y: rect.midY + 6, width: rect.width, height: 1).fill() + // Fade-handle slivers: the full path clips the 7 px handle + // ovals to the clip body, so at this width only a couple of + // white columns survive — the speckle texture of a dense audio + // lane. Fill the same intersection directly. + (selected ? NSColor.white : NSColor(calibratedWhite: 0.85, alpha: 0.9)).setFill() + let hy = rect.minY + 13.5 + for x in [xFor(clip.start + clip.fadeIn), xFor(clip.end - clip.fadeOut)] { + let sliver = NSRect(x: x - 3.5, y: hy, width: 7, height: 7) + .intersection(rect) + if !sliver.isEmpty { sliver.fill() } + } + } + // Status tint only: at this width the border stroke's inset path is + // degenerate (sub-zero width) and renders nothing today, and the + // red overlap band is painted at lane level. The tint is what reads. + if selected || linkedSel { + (selected ? NSColor.controlAccentColor : NSColor.systemCyan) + .withAlphaComponent(selected ? 0.34 : 0.20).setFill() + rect.fill() + } + if storyboard { drawShotDivider(clip, rect: rect, hasPrev: hasPrevPanel) } + return + } + + // A zoomed-in clip's rect can be literally millions of points wide; + // building paths and image draws at that size costs real time even + // though almost all of it is clipped away. Clamp the card geometry to + // the view plus a margin that keeps the rounded corners and border + // strokes of the clamped edges safely offscreen — the pixels inside + // the view are identical. (The filmstrip keeps the TRUE rect: its + // thumbnail tiling is phase-anchored to the clip's real left edge, and + // it already culls to the visible span internally.) + let clampMargin: CGFloat = 12 + let cardRect: NSRect = { + let cx0 = max(rect.minX, cullX0 - clampMargin) + let cx1 = min(rect.maxX, cullX1 + clampMargin) + return NSRect(x: cx0, y: rect.minY, width: cx1 - cx0, height: rect.height) + }() + let bodyRect = storyboard - ? NSRect(x: rect.minX, y: rect.minY + 0.5, width: rect.width, height: rect.height - 1) - : rect.insetBy(dx: 0.5, dy: 0.5) + ? NSRect(x: cardRect.minX, y: cardRect.minY + 0.5, + width: cardRect.width, height: cardRect.height - 1) + : cardRect.insetBy(dx: 0.5, dy: 0.5) let bodyRadius: CGFloat = storyboard ? 0 : 3 - let body = NSBezierPath(roundedRect: bodyRect, xRadius: bodyRadius, yRadius: bodyRadius) - switch clip.kind { - case .storyboard: - NSColor(calibratedWhite: 0.88, alpha: 1).setFill() - case .audio: - colors.audioBody.setFill() - case .video: - (media == nil ? NSColor(calibratedWhite: 0.25, alpha: 1) : colors.videoBody).setFill() + let body = DrawProf.t("clip.body") { + let p = NSBezierPath(roundedRect: bodyRect, xRadius: bodyRadius, + yRadius: bodyRadius) + bodyFillColor().setFill() + p.fill() + return p } - body.fill() - // Full detail (thumbnails, waveforms, labels) normally — including while - // scrolling. Only a very dense frame mid-scroll drops to bodies + strip + - // border (see `lightScroll`), and only until the pan settles. - let detail = !lightScroll - - if detail { + DrawProf.t("clip.content") { NSGraphicsContext.current?.saveGraphicsState() body.addClip() switch clip.kind { case .video: - if let media, session.showFilmstrips { drawFilmstrip(clip, media: media, rect: rect) } + if let media, session.showFilmstrips { + drawFilmstrip(clip, media: media, rect: rect, + cullX0: cullX0, cullX1: cullX1) + } case .audio: - if let media { drawWaveform(clip, media: media, rect: rect, color: color) } + if let media { + drawWaveform(clip, media: media, rect: rect, color: color, + cullX0: cullX0, cullX1: cullX1) + } drawFades(clip, rect: rect, selected: selected) case .storyboard: drawBoardThumb(clip, rect: rect) @@ -719,23 +1303,22 @@ final class TimelineView: NSView { NSGraphicsContext.current?.restoreGraphicsState() } - let linkedSel = !selected && linkedSelectionCache.contains(clip.id) - // Title strip - var title = media?.displayName - ?? (clip.kind == .storyboard - ? (panelNamesCache[clip.id] ?? "Panel") : "missing media") - if clip.kind != .storyboard && media == nil { title = "⚠︎ " + title } - if clip.kind == .audio { title = "♪ " + title } - // The label is drawn only at rest and only when the clip is wide enough - // to read; a clip narrower than this shows no legible text anyway. - let showLabel = detail && rect.width >= 22 - if detail { NSGraphicsContext.current?.saveGraphicsState(); body.addClip() } + let title = clipTitle(clip, media: media) + // The label is drawn only when the clip is wide enough to read; a clip + // narrower than this shows no legible text anyway. + let showLabel = rect.width >= 22 + DrawProf.t("clip.strip") { + NSGraphicsContext.current?.saveGraphicsState() + body.addClip() colors.strip.setFill() - NSRect(x: rect.minX, y: rect.minY, width: rect.width, height: 13).fill() + NSRect(x: cardRect.minX, y: cardRect.minY, width: cardRect.width, height: 13).fill() if showLabel { - title.draw(at: NSPoint(x: max(rect.minX, headerW) + 5, y: rect.minY + 1), - withAttributes: Self.titleAttrs) + // Unpinned: the label sits at the clip's true left edge. A clip + // scrolled off the left edge gets its label re-pinned to the view + // edge by `drawPinnedLabels` — an overlay pass, so the pinning + // never gets baked into a cached tile. + drawTitle(title, at: NSPoint(x: rect.minX + 5, y: rect.minY + 1)) var badgeX = rect.maxX - 16 if clip.speed != 1 { @@ -759,16 +1342,19 @@ final class TimelineView: NSView { from: .zero, operation: .sourceOver, fraction: 0.7) } } - if detail { NSGraphicsContext.current?.restoreGraphicsState() } + NSGraphicsContext.current?.restoreGraphicsState() + } + DrawProf.t("clip.chrome") { // Selection reads as a full-card tint, not just an outline. Link-mates // of the selection (they act selected) tint aqua. if selected || linkedSel { - if detail { NSGraphicsContext.current?.saveGraphicsState(); body.addClip() } + NSGraphicsContext.current?.saveGraphicsState() + body.addClip() (selected ? NSColor.controlAccentColor : NSColor.systemCyan) .withAlphaComponent(selected ? 0.34 : 0.20).setFill() - (detail ? rect : bodyRect).fill() - if detail { NSGraphicsContext.current?.restoreGraphicsState() } + cardRect.fill() + NSGraphicsContext.current?.restoreGraphicsState() } // Border LAST, on top of the tint: overlap = red, selection = accent @@ -777,7 +1363,7 @@ final class TimelineView: NSView { // outline (selected / linked / overlapping). if !storyboard || selected || linkedSel || overlapping { let radius: CGFloat = storyboard ? 0 : 3 - let border = NSBezierPath(roundedRect: rect.insetBy(dx: 1.25, dy: 1.25), + let border = NSBezierPath(roundedRect: cardRect.insetBy(dx: 1.25, dy: 1.25), xRadius: radius, yRadius: radius) border.lineWidth = selected ? 3.5 : linkedSel ? 3 : (overlapping ? 2 : 1.5) (selected ? Theme.selection @@ -786,42 +1372,141 @@ final class TimelineView: NSView { border.stroke() } - // Shot divider: an opaque line sitting ON the boundary between two - // storyboard panels (they tile gaplessly). A new shot gets a bold - // orange bar; frames within a shot get a thin neutral line. The first - // panel of the track has no divider on its left. - if storyboard { - let hasPrev = project.clips.contains { - $0.id != clip.id && $0.kind == .storyboard - && $0.track == clip.track && $0.start < clip.start - 1e-6 + if storyboard { drawShotDivider(clip, rect: rect, hasPrev: hasPrevPanel) } + } + } + + private func clipTitle(_ clip: Clip, media: MediaItem?) -> String { + var title = media?.displayName + ?? (clip.kind == .storyboard + ? (panelNamesCache[clip.id] ?? "Panel") : "missing media") + if clip.kind != .storyboard && media == nil { title = "⚠︎ " + title } + if clip.kind == .audio { title = "♪ " + title } + return title + } + + /// Re-pin the title of any clip whose left edge is scrolled offscreen: the + /// label follows the view edge (as it always has), but the pinning is an + /// overlay so it is never baked into a scene tile. The patch repaints the + /// clip's own strip pixels under the new label position (covering the + /// baked, unpinned label's tail), then restrokes the border segment it + /// covered — output matches the old single-pass pinned render. + private func drawPinnedLabels(ref: TrackRef, lane: NSRect) { + let leftSec = secondsFor(headerW) + let colors = laneColors(ref) + let clips = clipsByLane[ref] ?? [] + // Only clips STRADDLING the left edge qualify; binary search the + // candidate window instead of scanning every clip left of the view. + for i in visibleIndexRange(clips, lane: ref, left: leftSec, right: leftSec) { + let clip = clips[i] + guard clip.start < leftSec, clip.end > leftSec else { continue } + let x0 = xFor(clip.start), x1 = xFor(clip.end) + let rect = NSRect(x: x0, y: lane.minY, width: max(2, x1 - x0), + height: lane.height) + guard rect.width >= 22 else { continue } + let media = clip.mediaId.flatMap { mediaById[$0] } + let title = clipTitle(clip, media: media) + let selected = store.selection.contains(clip.id) + let linkedSel = !selected && linkedSelectionCache.contains(clip.id) + let overlapping = (overlapIdsByLane[ref] ?? []).contains(clip.id) + + let labelW = title.size(withAttributes: Self.titleAttrs).width + let patch = NSRect(x: headerW, y: rect.minY, + width: min(labelW + 12, rect.maxX - headerW), height: 13) + let storyboard = clip.kind == .storyboard + let cx1 = min(rect.maxX, bounds.width + 12) + let cardRect = NSRect(x: max(rect.minX, headerW - 12), y: rect.minY, + width: cx1 - max(rect.minX, headerW - 12), + height: rect.height) + let bodyRect = storyboard + ? NSRect(x: cardRect.minX, y: cardRect.minY + 0.5, + width: cardRect.width, height: cardRect.height - 1) + : cardRect.insetBy(dx: 0.5, dy: 0.5) + let radius: CGFloat = storyboard ? 0 : 3 + let body = NSBezierPath(roundedRect: bodyRect, xRadius: radius, yRadius: radius) + + NSGraphicsContext.current?.saveGraphicsState() + NSRect(x: patch.minX, y: patch.minY, width: patch.width, + height: patch.height).clip() + body.addClip() + // Rebuild the strip composite: body color under the 0.85-alpha strip. + switch clip.kind { + case .storyboard: NSColor(calibratedWhite: 0.88, alpha: 1).setFill() + case .audio: colors.audioBody.setFill() + case .video: (media == nil ? NSColor(calibratedWhite: 0.25, alpha: 1) + : colors.videoBody).setFill() + } + patch.fill() + colors.strip.setFill() + patch.fill() + drawTitle(title, at: NSPoint(x: headerW + 5, y: rect.minY + 1)) + if selected || linkedSel { + (selected ? NSColor.controlAccentColor : NSColor.systemCyan) + .withAlphaComponent(selected ? 0.34 : 0.20).setFill() + patch.fill() } - if hasPrev { - if clip.newShot { - NSColor.systemOrange.setFill() - NSRect(x: rect.minX - 1.5, y: rect.minY, width: 3, height: rect.height).fill() - } else { - NSColor(calibratedWhite: Theme.light ? 0.45 : 0.30, alpha: 1).setFill() - NSRect(x: rect.minX - 0.5, y: rect.minY, width: 1, height: rect.height).fill() - } + NSGraphicsContext.current?.restoreGraphicsState() + + // Restroke the border segment the patch painted over — clipped to + // the patch only (drawClip strokes the border unclipped, so a + // body clip here would shave its outer antialiasing). + if !storyboard || selected || linkedSel || overlapping { + NSGraphicsContext.current?.saveGraphicsState() + patch.insetBy(dx: 0, dy: -1).clip() + let border = NSBezierPath( + roundedRect: cardRect.insetBy(dx: 1.25, dy: 1.25), + xRadius: radius, yRadius: radius) + border.lineWidth = selected ? 3.5 : linkedSel ? 3 : (overlapping ? 2 : 1.5) + (selected ? Theme.selection + : linkedSel ? NSColor.systemCyan + : overlapping ? NSColor.systemRed : colors.base).setStroke() + border.stroke() + NSGraphicsContext.current?.restoreGraphicsState() } } } - private func drawFilmstrip(_ clip: Clip, media: MediaItem, rect: NSRect) { + /// Shot divider: an opaque line sitting ON the boundary between two + /// storyboard panels (they tile gaplessly). A new shot gets a bold + /// orange bar; frames within a shot get a thin neutral line. The first + /// panel of the track has no divider on its left. + private func drawShotDivider(_ clip: Clip, rect: NSRect, hasPrev: Bool) { + guard hasPrev else { return } + if clip.newShot { + NSColor.systemOrange.setFill() + NSRect(x: rect.minX - 1.5, y: rect.minY, width: 3, height: rect.height).fill() + } else { + NSColor(calibratedWhite: Theme.light ? 0.45 : 0.30, alpha: 1).setFill() + NSRect(x: rect.minX - 0.5, y: rect.minY, width: 1, height: rect.height).fill() + } + } + + private func drawFilmstrip(_ clip: Clip, media: MediaItem, rect: NSRect, + cullX0: CGFloat, cullX1: CGFloat) { let thumbH = rect.height - 14 guard thumbH > 6 else { return } let mediaAspect = media.width > 0 && media.height > 0 ? CGFloat(media.width) / CGFloat(media.height) : 16.0 / 9.0 let thumbW = thumbH * mediaAspect - let visX0 = max(rect.minX, headerW), visX1 = min(rect.maxX, bounds.width) + // Thumb tiling stays phase-anchored to the clip's TRUE left edge (so + // thumbnails never slide as the window changes); the window only culls. + let visX0 = max(rect.minX, cullX0), visX1 = min(rect.maxX, cullX1) + guard let cg = NSGraphicsContext.current?.cgContext else { return } var x = rect.minX + floor((visX0 - rect.minX) / thumbW) * thumbW while x < visX1 { let tlSec = secondsFor(x + thumbW / 2) let srcSec = clip.sourceTime(at: tlSec) if let img = MediaPipeline.shared.filmstripImage(for: media, at: max(0, srcSec)) { if DrawProf.on { DrawProf.thumbHits += 1 } - img.draw(in: NSRect(x: x, y: rect.minY + 14, width: thumbW, height: thumbH), - from: .zero, operation: .sourceOver, fraction: 0.9) + // CGContext blit with a local flip (CGImages draw bottom-up; + // the view is flipped). The images are pre-converted to the + // display's format, so this is a plain copy. + cg.saveGState() + cg.setAlpha(0.9) + cg.translateBy(x: x, y: rect.minY + 14 + thumbH) + cg.scaleBy(x: 1, y: -1) + cg.draw(img, in: CGRect(x: 0, y: 0, width: thumbW, height: thumbH)) + cg.restoreGState() } x += thumbW } @@ -829,20 +1514,36 @@ final class TimelineView: NSView { rect.fill() } - private func drawWaveform(_ clip: Clip, media: MediaItem, rect: NSRect, color: NSColor) { + private func drawWaveform(_ clip: Clip, media: MediaItem, rect: NSRect, color: NSColor, + cullX0: CGFloat, cullX1: CGFloat) { // Center line color.withAlphaComponent(0.35).setFill() NSRect(x: rect.minX, y: rect.midY + 6, width: rect.width, height: 1).fill() guard let img = MediaPipeline.shared.waveformImage(for: media), media.duration > 0 else { return } - let imgW = img.size.width + // Draw only the visible span — a zoomed-in clip's full rect can be + // millions of points wide, and image draws at that size are slow even + // though it's all clipped away. CGImage has no source-rect draw, so + // map the FULL image through the source→dest transform and clip to + // the visible span: pixel-identical to a fractional source rect + // (cropping(to:) would round to whole source pixels), and Quartz only + // rasterizes the clipped part. + let visX0 = max(rect.minX, cullX0 - 2), visX1 = min(rect.maxX, cullX1 + 2) + guard visX1 > visX0, let cg = NSGraphicsContext.current?.cgContext else { return } + let imgW = CGFloat(img.width) let fromX = CGFloat(clip.srcIn / media.duration) * imgW - let fromW = CGFloat(clip.duration / media.duration) * imgW + let fromW = max(1, CGFloat(clip.duration / media.duration) * imgW) let dest = NSRect(x: rect.minX, y: rect.minY + 14, width: rect.width, height: rect.height - 16) - img.draw(in: dest, from: NSRect(x: fromX, y: 0, width: max(1, fromW), - height: img.size.height), - operation: .sourceOver, fraction: 0.85) + let scaleX = dest.width / fromW + cg.saveGState() + cg.setAlpha(0.85) + cg.clip(to: CGRect(x: visX0, y: dest.minY, width: visX1 - visX0, + height: dest.height)) + cg.translateBy(x: dest.minX - fromX * scaleX, y: dest.maxY) + cg.scaleBy(x: 1, y: -1) + cg.draw(img, in: CGRect(x: 0, y: 0, width: imgW * scaleX, height: dest.height)) + cg.restoreGState() } private func drawFades(_ clip: Clip, rect: NSRect, selected: Bool) { @@ -867,7 +1568,16 @@ final class TimelineView: NSView { let xOut = xFor(clip.end - clip.fadeOut) if clip.fadeIn > 0.001 { fadeShape(from: xFor(clip.start), to: xIn, leading: true) } if clip.fadeOut > 0.001 { fadeShape(from: xFor(clip.end), to: xOut, leading: false) } - // Handles (always visible so fades stay discoverable). + drawFadeHandles(clip, rect: rect, selected: selected) + } + + /// Fade handles (always visible so fades stay discoverable). Split out of + /// `drawFades` because narrow (LOD) clips draw the handles — they're the + /// speckled texture of a dense audio lane — without the fade shapes. + private func drawFadeHandles(_ clip: Clip, rect: NSRect, selected: Bool) { + let top = rect.minY + 13 + let xIn = xFor(clip.start + clip.fadeIn) + let xOut = xFor(clip.end - clip.fadeOut) for x in [xIn, xOut] { let r = NSRect(x: x - 3.5, y: top - 3.5 + 4, width: 7, height: 7) (selected ? NSColor.white : NSColor(calibratedWhite: 0.85, alpha: 0.9)).setFill() @@ -891,8 +1601,9 @@ final class TimelineView: NSView { x += w + 2 break // one panel image; boards are one still, no need to tile } - NSImage(systemSymbolName: "pencil.and.outline", accessibilityDescription: nil)? - .tinted(NSColor(calibratedWhite: 0.2, alpha: 1)) + Self.bakedSymbol("pencil.and.outline", + tint: NSColor(calibratedWhite: 0.2, alpha: 1), + key: "board.pencil")? .draw(in: NSRect(x: rect.minX + 4, y: rect.minY + 16, width: 11, height: 11), from: .zero, operation: .sourceOver, fraction: 0.9) } @@ -901,7 +1612,7 @@ final class TimelineView: NSView { private func drawFusionBand() { guard fusionBandH > 0 else { return } - let band = NSRect(x: 0, y: rulerH, width: bounds.width, height: fusionBandH) + let band = NSRect(x: 0, y: fusionTop, width: bounds.width, height: fusionBandH) NSColor(calibratedRed: 0.16, green: 0.14, blue: 0.05, alpha: 1).setFill() band.fill() FusionComps.yellow.withAlphaComponent(0.5).setFill() @@ -943,10 +1654,10 @@ final class TimelineView: NSView { } private func compAt(point: NSPoint) -> FusionComp? { - guard fusionBandH > 0, point.y > rulerH, point.y < rulerH + fusionBandH + guard fusionBandH > 0, point.y > fusionTop, point.y < fusionTop + fusionBandH else { return nil } let fps = project.fps - let band = NSRect(x: 0, y: rulerH, width: bounds.width, height: fusionBandH) + let band = NSRect(x: 0, y: fusionTop, width: bounds.width, height: fusionBandH) for (comp, lane, lanes) in comps.stacked() { let x0 = xFor(comp.startSeconds(fps: fps)) let x1 = xFor(comp.endSeconds(fps: fps)) @@ -958,6 +1669,109 @@ final class TimelineView: NSView { return nil } + // MARK: - Optimization strip + + /// Per-column optimization state for the thin band under the ruler: + /// GREEN — every proxy chunk the visible tracks need at that time is + /// built at the preview-quality target; DIM GREEN — built but below + /// target (usable now, upgrade queued); YELLOW — building right now (or + /// a rescue slice is standing in); RED — no proxy on disk (never built, + /// evicted, or failed). Aggregated worst-wins across visible video + /// tracks, so one glance says whether playing here will be instant. + /// + /// The runs are cached — recomputed only when the view moves (origin/ + /// zoom/width) or something actually changed (.projectChanged / + /// .mediaStatusChanged clear the key) — so playback frames just re-fill + /// a few dozen rects. + private var stripRuns: [(x: CGFloat, w: CGFloat, color: NSColor)] = [] + private var stripKey: (origin: Double, pps: Double, width: CGFloat)? + + private func rebuildStripIfNeeded() { + if let k = stripKey, k.origin == originSecond, k.pps == pxPerSecond, + k.width == bounds.width { return } + stripKey = (originSecond, pxPerSecond, bounds.width) + stripRuns.removeAll(keepingCapacity: true) + rebuildSceneIfNeeded() + + let colW: CGFloat = 2 + let ncols = Int((bounds.width - headerW) / colW) + 1 + guard ncols > 0 else { return } + var vals = [UInt8](repeating: .max, count: ncols) + let leftSec = secondsFor(headerW), rightSec = secondsFor(bounds.width) + var snaps: [String: ChunkManager.StripSnapshot?] = [:] + + func mark(_ lo: Double, _ hi: Double, _ v: UInt8) { + let c0 = max(0, Int((xFor(lo) - headerW) / colW)) + let c1 = min(ncols - 1, Int((xFor(hi) - headerW) / colW)) + guard c1 >= c0 else { return } + for c in c0...c1 where vals[c] > v { vals[c] = v } + } + + for ref in laneRows { + guard ref.videoIndex != nil, !session.hiddenTracks.contains(ref) else { continue } + let clips = clipsByLane[ref] ?? [] + for i in visibleIndexRange(clips, lane: ref, left: leftSec, right: rightSec) { + let clip = clips[i] + guard clip.kind == .video, + let media = project.media(clip.mediaId), !media.isAudio, + media.duration > 0 else { continue } + let a = max(clip.start, leftSec), b = min(clip.end, rightSec) + guard b > a else { continue } + let snap: ChunkManager.StripSnapshot? + if let cached = snaps[media.cacheKey] { snap = cached } + else { snap = ctx.chunks.stripSnapshot(media: media); snaps[media.cacheKey] = snap } + guard let snap else { mark(a, b, 0); continue } // unscanned: unknown + if snap.fullProxy { mark(a, b, 3); continue } // legacy whole-file proxy + let srcA = clip.srcIn + (a - clip.start) * clip.speed + let srcB = clip.srcIn + (b - clip.start) * clip.speed + let ci = min(snap.n - 1, ChunkManager.chunkIndex(forSource: min(srcA, srcB))) + let cj = min(snap.n - 1, ChunkManager.chunkIndex(forSource: max(srcA, srcB) - 1e-6)) + for idx in ci...max(ci, cj) { + let v: UInt8 + if let w = snap.built[idx] { + v = w >= snap.target ? 3 : 2 + } else if snap.inFlight.contains(idx) || snap.partial.contains(idx) { + v = 1 + } else { + v = 0 // missing, evicted, or failed + } + let t0 = clip.start + + (Double(idx) * ChunkManager.chunkSeconds - clip.srcIn) / max(1e-4, clip.speed) + let t1 = clip.start + + (Double(idx + 1) * ChunkManager.chunkSeconds - clip.srcIn) / max(1e-4, clip.speed) + mark(max(min(t0, t1), a), min(max(t0, t1), b), v) + } + } + } + + // Compress equal-valued columns into fill runs. + let palette = [Theme.stripMissing, Theme.stripBuilding, + Theme.stripUsable, Theme.stripFull] + var c = 0 + while c < ncols { + let v = vals[c] + var e = c + 1 + while e < ncols, vals[e] == v { e += 1 } + if v != .max { + stripRuns.append((x: headerW + CGFloat(c) * colW, + w: CGFloat(e - c) * colW, + color: palette[Int(v)])) + } + c = e + } + } + + private func drawOptimizeStrip() { + Theme.timelineBg.setFill() + NSRect(x: 0, y: rulerH, width: bounds.width, height: stripH).fill() + rebuildStripIfNeeded() + let y = rulerH + 1 + for run in stripRuns { + run.color.setFill() + NSRect(x: run.x, y: y, width: run.w, height: stripH - 2).fill() + } + } + // MARK: - Ruler / playhead / indicators private func drawRuler() { @@ -1164,7 +1978,7 @@ final class TimelineView: NSView { // MARK: - Mouse editing private enum DragMode { - case none, scrub, move, trimIn, trimOut, rippleOut, slip, + case none, scrub, move, trimIn, trimOut, rippleOut, rippleIn, slip, stretchIn, stretchOut, fadeIn, fadeOut, box, resizeTrack, hBarPan, hBarLeft, hBarRight, vBarPan, vBarTop, vBarBottom } @@ -1235,7 +2049,7 @@ final class TimelineView: NSView { return } - if p.y < rulerH { + if p.y < rulerH + stripH { // ruler + optimization strip both scrub drag.mode = .scrub playback.setRate(0) playback.seek(to: max(0, quantize(secondsFor(p.x)))) @@ -1243,8 +2057,8 @@ final class TimelineView: NSView { } // Fusion band header: hide preview (top) / focus (bottom) - if fusionBandH > 0, p.x < headerW, p.y > rulerH, p.y < lanesTop { - if p.y < rulerH + fusionBandH * 0.55 { session.fusionHidden.toggle() } + if fusionBandH > 0, p.x < headerW, p.y > fusionTop, p.y < lanesTop { + if p.y < fusionTop + fusionBandH * 0.55 { session.fusionHidden.toggle() } else { session.fusionFocus.toggle() } needsDisplay = true return @@ -1348,13 +2162,17 @@ final class TimelineView: NSView { if drag.mode == .none { let stretch = event.modifierFlags.contains(.command) && clip.kind == .video let opt = event.modifierFlags.contains(.option) - if opt, rect.maxX - p.x < edge { - drag.mode = .rippleOut // ⌥-drag out edge: push everything after + let onIn = p.x - rect.minX < edge + let onOut = rect.maxX - p.x < edge + if opt, onOut { + drag.mode = .rippleOut // ⌥-drag out edge: ripple the tail away + } else if opt, onIn, clip.kind != .storyboard { + drag.mode = .rippleIn // ⌥-drag in edge: ripple-trim the head } else if opt || session.mainTool == .slide { - drag.mode = .slip - } else if p.x - rect.minX < edge { + drag.mode = .slip // ⌥-drag body: slip source under the clip + } else if onIn { drag.mode = stretch ? .stretchIn : .trimIn - } else if rect.maxX - p.x < edge { + } else if onOut { drag.mode = stretch ? .stretchOut : .trimOut } else { drag.mode = .move @@ -1395,7 +2213,7 @@ final class TimelineView: NSView { // Dragging against the view edges pans the timeline (there's no // enclosing scroll view, so the playhead could never leave the screen). - if [.scrub, .move, .trimIn, .trimOut, .rippleOut, .slip, + if [.scrub, .move, .trimIn, .trimOut, .rippleOut, .rippleIn, .slip, .stretchIn, .stretchOut, .fadeIn, .fadeOut].contains(drag.mode) { if p.x > bounds.width - 30 { originSecond += Double(p.x - (bounds.width - 30)) * 0.12 / pxPerSecond @@ -1413,6 +2231,7 @@ final class TimelineView: NSView { case .trimIn: dragTrimIn(dSec: dSec) case .trimOut: dragTrimOut(dSec: dSec) case .rippleOut: dragRippleOut(dSec: dSec) + case .rippleIn: dragRippleIn(dSec: dSec) case .slip: dragSlip(dSec: dSec) case .stretchIn: dragStretch(dSec: dSec, fromStart: true) case .stretchOut: dragStretch(dSec: dSec, fromStart: false) @@ -1458,8 +2277,9 @@ final class TimelineView: NSView { case .box: let r = boxRect() var hit = drag.baseSelection - for (row, ref) in project.laneRefs.enumerated() { - for clip in project.clips(on: ref) + rebuildSceneIfNeeded() + for (row, ref) in laneRows.enumerated() { + for clip in clipsByLane[ref] ?? [] where clipRect(clip, row: row).intersects(r) { hit.insert(clip.id) } @@ -1472,12 +2292,6 @@ final class TimelineView: NSView { session.trackHeights[ref] = min(4, max(0.35, factor)) } } - // Dragging the floating scrollbars is a scroll/zoom — simplify while it - // moves, like the wheel and pinch paths. - if [.hBarPan, .hBarLeft, .hBarRight, - .vBarPan, .vBarTop, .vBarBottom].contains(drag.mode) { - noteScrolling() - } drag.moved = true autoscroll(with: event) needsDisplay = true @@ -1485,7 +2299,7 @@ final class TimelineView: NSView { override func mouseUp(with event: NSEvent) { switch drag.mode { - case .move, .trimIn, .trimOut, .rippleOut, .slip, + case .move, .trimIn, .trimOut, .rippleOut, .rippleIn, .slip, .stretchIn, .stretchOut, .fadeIn, .fadeOut: store.endGesture() default: @@ -1518,7 +2332,6 @@ final class TimelineView: NSView { if maxScrollY > 0 { scrollY = min(max(0, panDrag.origScrollY - (p.y - panDrag.start.y)), maxScrollY) } - noteScrolling() needsDisplay = true } @@ -1532,14 +2345,11 @@ final class TimelineView: NSView { // Vertical retracking only for a lone unlinked clip. let multi = drag.origSelection.count > 1 || store.selection.count > 1 - var delta = dSec - if let adj = snapAdjust(start: orig.start + dSec, duration: orig.duration, - excluding: Set(drag.origSelection.keys)) { - delta += adj.adjust - activeSnapTarget = adj.target - } - // Frame-quantize the moved edge, clamp to t >= 0 for all moved clips. - delta = quantize(orig.start + delta) - orig.start + // Snap+quantize the moved start edge, then clamp to t >= 0 for all moved clips. + let snappedStart = snapAndQuantize(orig.start + dSec, + excluding: Set(drag.origSelection.keys), + duration: orig.duration) + var delta = snappedStart - orig.start let minStart = drag.origSelection.values.map(\.start).min() ?? 0 if minStart + delta < 0 { delta = -minStart } @@ -1625,6 +2435,16 @@ final class TimelineView: NSView { } } + /// A trim/slip drag is exposing source material the clip didn't use + /// before — tell the proxy builder NOW (mid-drag), not at mouse-up, so + /// the newly extended range is often already covered when the user plays + /// it. `direction` is which way the exposure grows in source time. + private func prefetchExposure(_ orig: Clip, sourceTime: Double, direction: Int) { + guard orig.kind == .video, let media = project.media(orig.mediaId) else { return } + ctx.chunks.noteGestureExposure(media: media, sourceTime: sourceTime, + direction: direction) + } + /// Non-audio clips on the same track whose head would be swallowed by /// dragging `orig`'s out-edge to `newEnd` (audio layers freely, so it /// never gets pushed). @@ -1657,12 +2477,7 @@ final class TimelineView: NSView { return } let media = project.media(orig.mediaId) - var desired = orig.end + dSec - if let adj = snapAdjust(start: desired, duration: 0, excluding: [orig.id]) { - desired += adj.adjust - activeSnapTarget = adj.target - } - desired = quantize(desired) + let desired = snapAndQuantize(orig.end + dSec, excluding: [orig.id]) var maxEnd = Double.greatestFiniteMagnitude if let media { maxEnd = orig.start + (media.duration - orig.srcIn) / max(0.001, orig.speed) @@ -1677,6 +2492,10 @@ final class TimelineView: NSView { // The grabbed clip's clamped change is the delta applied to every // selected/linked clip. let delta = newEnd - orig.end + if delta > 0 { // extending the tail exposes fresh source material + prefetchExposure(orig, sourceTime: orig.srcIn + (newEnd - orig.start) * orig.speed, + direction: 1) + } let targets = drag.origSelection.values.filter { $0.kind != .storyboard } let single = targets.count <= 1 store.updateGesture { model in @@ -1703,12 +2522,7 @@ final class TimelineView: NSView { private func dragTrimIn(dSec: Double) { guard let orig = drag.origClip else { return } - var desired = orig.start + dSec - if let adj = snapAdjust(start: desired, duration: 0, excluding: [orig.id]) { - desired += adj.adjust - activeSnapTarget = adj.target - } - desired = quantize(desired) + let desired = snapAndQuantize(orig.start + dSec, excluding: [orig.id]) // Storyboard panels are pure start positions (no source media), so the // in-edge just slides the panel's start either way — bounded only by the // previous panel (a frame of clearance) and this panel's own end. @@ -1732,6 +2546,10 @@ final class TimelineView: NSView { minStart = max(0, minStart) let maxStart = orig.end - frameDur let newStart = min(max(desired, minStart), maxStart) + if newStart < orig.start { // extending the head exposes earlier source + prefetchExposure(orig, sourceTime: orig.srcIn + (newStart - orig.start) * orig.speed, + direction: -1) + } let base = store.gestureBaseModel ?? project // The grabbed clip's clamped change is the delta applied to every // selected/linked clip. A storyboard in-edge just moves that panel's @@ -1768,12 +2586,7 @@ final class TimelineView: NSView { private func dragRippleOut(dSec: Double) { guard let orig = drag.origClip else { return } let base = store.gestureBaseModel ?? project - var desired = quantize(orig.end + dSec) - if let adj = snapAdjust(start: desired, duration: 0, excluding: [orig.id]) { - desired += adj.adjust - activeSnapTarget = adj.target - desired = quantize(desired) - } + let desired = snapAndQuantize(orig.end + dSec, excluding: [orig.id]) var newEnd = max(desired, orig.start + frameDur) if orig.kind == .video, let media = base.media(orig.mediaId) { newEnd = min(newEnd, orig.start + (media.duration - orig.srcIn) / max(0.001, orig.speed)) @@ -1785,6 +2598,11 @@ final class TimelineView: NSView { // Never push anything below t = 0. let minStart = followers.map(\.start).min() ?? 0 let clampedDelta = max(delta, -minStart) + if clampedDelta > 0 { // ripple-extending the tail exposes fresh source + prefetchExposure(orig, + sourceTime: orig.srcIn + (orig.end + clampedDelta - orig.start) * orig.speed, + direction: 1) + } store.updateGesture { model in if orig.kind != .storyboard, let i = model.clips.firstIndex(where: { $0.id == orig.id }) { @@ -1797,10 +2615,54 @@ final class TimelineView: NSView { } } + /// ⌥-drag a clip's IN edge: ripple-trim the head while pinning the clip's + /// start. The in-point (`srcIn`) and length change — + /// the clip's OUT edge moves — and every later clip on the track shifts by the + /// same amount so the timeline stays gapless. Unlike a normal trim-in (which + /// slides the clip's start and opens blank space *before* it), the start stays + /// put and the space is taken out of the track. Dragging right trims the head + /// (clip shrinks, followers pull in); left extends it (clip grows, followers + /// push out). The out edge — the source out-point — never moves. + private func dragRippleIn(dSec: Double) { + guard let orig = drag.origClip, orig.kind != .storyboard else { return } + let base = store.gestureBaseModel ?? project + let followers = base.clips.filter { + $0.id != orig.id && $0.track == orig.track && $0.start >= orig.end - 1e-9 + } + // Snap the moving OUT edge to static references only — the followers ride + // along with it, so they can't be snap targets. d > 0 trims the head. + var exclude = Set(followers.map(\.id)); exclude.insert(orig.id) + let newEnd = snapAndQuantize(orig.end - dSec, excluding: exclude) + var d = orig.end - newEnd + d = min(d, orig.duration - frameDur) // keep ≥ one frame + d = max(d, -orig.srcIn / max(0.001, orig.speed)) // head ≥ source start + if let firstStart = followers.map(\.start).min() { // followers ≥ t = 0 + d = min(d, firstStart) + } + if d < 0 { // ripple-extending the head exposes earlier source + prefetchExposure(orig, sourceTime: orig.srcIn + d * orig.speed, direction: -1) + } + store.updateGesture { model in + guard let i = model.clips.firstIndex(where: { $0.id == orig.id }) else { return } + model.clips[i].srcIn = orig.srcIn + d * orig.speed + model.clips[i].duration = orig.duration - d + for f in followers { + guard let j = model.clips.firstIndex(where: { $0.id == f.id }) else { continue } + model.clips[j].start = f.start - d + } + } + } + private func dragSlip(dSec: Double) { guard let orig = drag.origClip, let media = project.media(orig.mediaId) else { return } let maxIn = max(0, media.duration - orig.sourceLength) let newIn = min(max(orig.srcIn - dSec * orig.speed, 0), maxIn) + // Slipping reveals source on the side the content is sliding from. + if newIn < orig.srcIn { + prefetchExposure(orig, sourceTime: newIn, direction: -1) + } else if newIn > orig.srcIn { + prefetchExposure(orig, sourceTime: newIn + orig.sourceLength, direction: 1) + } store.updateGesture { model in guard let i = model.clips.firstIndex(where: { $0.id == drag.clipId }) else { return } model.clips[i].srcIn = newIn @@ -1847,6 +2709,21 @@ final class TimelineView: NSView { } } + /// The single place edge alignment is decided: snap `desired` to nearby clip + /// edges / playhead / 0 (unless snapping is off), record the snap indicator, + /// then frame-quantize — snap FIRST, quantize ONCE. Every drag handler routes + /// through here so trims, moves and ripples align identically (previously each + /// re-implemented this, and the ripple handler quantized twice out of order). + private func snapAndQuantize(_ desired: Double, excluding: Set, + duration: Double = 0) -> Double { + var v = desired + if let adj = snapAdjust(start: v, duration: duration, excluding: excluding) { + v += adj.adjust + activeSnapTarget = adj.target + } + return quantize(v) + } + private func snapAdjust(start: Double, duration: Double, excluding: Set) -> (adjust: Double, target: Double)? { // Holding ⇧ mid-drag temporarily inverts snapping. @@ -1882,36 +2759,50 @@ final class TimelineView: NSView { } override func mouseMoved(with event: NSEvent) { - let p = convert(event.locationInWindow, from: nil) - lastMousePoint = p - var cursor = NSCursor.arrow + lastMousePoint = convert(event.locationInWindow, from: nil) + cursor(for: event.modifierFlags).set() + } + + // Holding a modifier changes what a drag would do, so refresh the cursor even + // when the pointer is still (mouseMoved won't fire on a bare key press). + override func flagsChanged(with event: NSEvent) { + cursor(for: event.modifierFlags).set() + } + + /// The cursor for the pointer's current spot, given the held modifiers, so it + /// previews the gesture a drag would start: ⌥ over a clip body slips it, ⌥/⌘ + /// or a plain hover over an edge resizes (trim / ripple / stretch). + private func cursor(for mods: NSEvent.ModifierFlags) -> NSCursor { + let p = lastMousePoint if let barMode = scrollbarHit(p) { switch barMode { - case .hBarLeft, .hBarRight: cursor = .resizeLeftRight - case .vBarTop, .vBarBottom: cursor = .resizeUpDown - default: break - } - } else if trackBoundaryAt(y: p.y) != nil, p.y > lanesTop { - cursor = .resizeUpDown - } else if let (clip, row) = clipAt(point: p) { - if session.mainTool == .blade { - cursor = .crosshair - } else if session.mainTool == .slide { - cursor = .openHand - } else { - let rect = clipRect(clip, row: row) - if p.x - rect.minX < 7 || rect.maxX - p.x < 7 { - cursor = .resizeLeftRight - } + case .hBarLeft, .hBarRight: return .resizeLeftRight + case .vBarTop, .vBarBottom: return .resizeUpDown + default: return .arrow } } - cursor.set() + if trackBoundaryAt(y: p.y) != nil, p.y > lanesTop { return .resizeUpDown } + guard let (clip, row) = clipAt(point: p) else { return .arrow } + if session.mainTool == .blade { return .crosshair } + if session.mainTool == .slide { return .openHand } + let rect = clipRect(clip, row: row) + let onEdge = p.x - rect.minX < 7 || rect.maxX - p.x < 7 + if mods.contains(.option), !onEdge { return .openHand } // ⌥ body: slip + return onEdge ? .resizeLeftRight : .arrow } // MARK: - Keyboard (fallbacks; the menu bar owns the canonical bindings) override func keyDown(with event: NSEvent) { let pc = playback + // A key pressed WHILE a mouse drag holds an open gesture must not run — + // a mutating shortcut (split/delete/nudge) calls `store.mutate`, whose + // `precondition(gestureBase == nil)` would trap mid-drag. Ignore keys + // until the drag ends; Escape still cancels it. + if store.gestureBaseModel != nil { + if event.keyCode == 53 { cancelOperation(nil) } // Esc → cancel drag + return + } switch event.charactersIgnoringModifiers?.lowercased() { case " ": pc.togglePlay() case "j": pc.shuttle(-1) @@ -2745,7 +3636,7 @@ final class TimelineView: NSView { add("Rescan Comps", #selector(ctxRescanComps)) return menu } - if fusionBandH > 0, p.y > rulerH, p.y < lanesTop { + if fusionBandH > 0, p.y > fusionTop, p.y < lanesTop { add(session.fusionHidden ? "Show Fusion Preview" : "Hide Fusion Preview", #selector(ctxToggleFusionHidden)) add(session.fusionFocus ? "Unfocus Fusion" : "Focus Fusion", @@ -2888,7 +3779,8 @@ final class TimelineView: NSView { /// past the last clip (the playhead may live out there), but bounded. private func clampOrigin(_ o: Double) -> Double { let overscroll = 600.0 / pxPerSecond - return min(max(o, -overscroll), project.timelineDuration + 120) + rebuildSceneIfNeeded() + return min(max(o, -overscroll), cachedTimelineDuration + 120) } override func scrollWheel(with event: NSEvent) { @@ -2905,12 +3797,10 @@ final class TimelineView: NSView { originSecond = clampOrigin(originSecond - Double(dx) / pxPerSecond) } } - noteScrolling() needsDisplay = true } override func magnify(with event: NSEvent) { - noteScrolling() zoom(by: 1 + event.magnification, anchorX: convert(event.locationInWindow, from: nil).x) } @@ -2969,7 +3859,7 @@ final class TimelineView: NSView { let streams = expandDropStreams(droppableFiles(from: sender)) guard !streams.isEmpty else { fileDropPreview = nil; needsDisplay = true; return } let p = convert(sender.draggingLocation, from: nil) - let count = project.laneRefs.count + let count = laneRows.count fileDropPreview = FileDropPreview( streams: streams, minOffset: streams.map(\.offset).min() ?? 0, @@ -3001,7 +3891,7 @@ final class TimelineView: NSView { let files = droppableFiles(from: sender) guard !files.isEmpty else { return false } let dropSec = max(0, quantize(secondsFor(p.x))) - let row = rowAt(y: p.y).flatMap { $0 < project.laneRefs.count ? $0 : nil } + let row = rowAt(y: p.y).flatMap { $0 < laneRows.count ? $0 : nil } importFiles(files, atSecond: dropSec, targetRow: row) return true } @@ -3011,7 +3901,7 @@ final class TimelineView: NSView { /// track exactly the way dragging an existing clip down does. private func drawFileDropPreview() { guard let dp = fileDropPreview else { return } - let count = project.laneRefs.count + let count = laneRows.count for (i, s) in dp.streams.enumerated() { let row = dp.baseRow + i let lane = laneRect(row: row) @@ -3167,7 +4057,6 @@ final class TimelineView: NSView { func testVThumb() -> NSRect { vThumbRect() } func testSetOrigin(_ sec: Double) { originSecond = sec } func testSetPxPerSecond(_ p: Double) { pxPerSecond = p } - func testSetScrolling(_ b: Bool) { isScrolling = b } /// Draw straight into the current graphics context (set by the harness), /// bypassing `cacheDisplay`'s per-call bitmap allocation so we time `draw`. func testRedraw() { draw(bounds) } diff --git a/sequencer/Sources/Sequencer/Tools.swift b/sequencer/Sources/Sequencer/Tools.swift index 126647268685303ee95fc4368f7866d3effb5510..d5e48259f4cb98378275ee63e4d43a7edcf643dd 100644 --- a/sequencer/Sources/Sequencer/Tools.swift +++ b/sequencer/Sources/Sequencer/Tools.swift @@ -220,6 +220,8 @@ final class SettingsWindow: NSObject { private let compsLabel = NSTextField(labelWithString: "—") // Global tab private let cacheField = NSTextField(string: "") + private let ramField = NSTextField(string: "") + private let usageLabel = NSTextField(labelWithString: "") private let cacheLabel = NSTextField(labelWithString: "") // Aspect label → concrete storyboard resolution (stored in the model as @@ -231,9 +233,9 @@ final class SettingsWindow: NSObject { func show() { buildIfNeeded() - sync() - window?.makeKeyAndOrderFront(nil) + window?.makeKeyAndOrderFront(nil) // visible first — sync() guards on it NSApp.activate(ignoringOtherApps: true) + sync() } private func label(_ s: String) -> NSTextField { @@ -242,9 +244,17 @@ final class SettingsWindow: NSObject { return l } + /// Key (fields need focus) but never MAIN: `DocumentContext.current` + /// resolves through the main window, so a main Settings window would read + /// a blank headless project (fps/comps/estimate all defaults) AND starve + /// the front-document build gate while it's open. + private final class SettingsPanel: NSWindow { + override var canBecomeMain: Bool { false } + } + private func buildIfNeeded() { guard window == nil else { return } - let w = NSWindow( + let w = SettingsPanel( contentRect: NSRect(x: 0, y: 0, width: 560, height: 300), styleMask: [.titled, .closable], backing: .buffered, defer: false) @@ -300,13 +310,27 @@ final class SettingsWindow: NSObject { cacheField.widthAnchor.constraint(equalToConstant: 60).isActive = true let cacheRow = NSStackView(views: [cacheField, NSTextField(labelWithString: "GB")]) cacheRow.spacing = 4 + ramField.target = self + ramField.action = #selector(ramChanged) + ramField.widthAnchor.constraint(equalToConstant: 60).isActive = true + let ramRow = NSStackView(views: [ramField, NSTextField(labelWithString: "GB")]) + ramRow.spacing = 4 + let ramNote = NSTextField(labelWithString: + "Decoded stand-in frames + player read-ahead; more = fewer hiccups.") + ramNote.textColor = .secondaryLabelColor + ramNote.font = .systemFont(ofSize: 11) let reveal = NSButton(title: "Reveal Cache", target: self, action: #selector(revealCache)) reveal.controlSize = .small + usageLabel.textColor = .secondaryLabelColor + usageLabel.font = .systemFont(ofSize: 11) cacheLabel.textColor = .secondaryLabelColor cacheLabel.font = .systemFont(ofSize: 11) let globalGrid = NSGridView(views: [ [label("Proxy cache limit"), cacheRow], + [label("RAM frame cache"), ramRow], + [NSView(), ramNote], + [NSView(), usageLabel], [NSView(), reveal], [NSView(), cacheLabel], ]) @@ -343,7 +367,9 @@ final class SettingsWindow: NSObject { } @objc private func sync() { - guard window != nil else { return } + // The estimate below walks each media's chunk dir — fine on demand, + // wasteful on every .projectChanged while the window is closed. + guard let window, window.isVisible else { return } let project = DocumentContext.current.store.project // Rebuild the fps popup: presets plus the project's own rate when it's // not a preset (e.g. 29.50 fps probed from a screen recording). @@ -363,7 +389,14 @@ final class SettingsWindow: NSObject { compsLabel.stringValue = project.compsFolder ?? "not set" let gb = UserDefaults.standard.integer(forKey: "maxCacheGB") cacheField.stringValue = "\(gb > 0 ? gb : 50)" - cacheLabel.stringValue = "Cache: \(MediaPipeline.shared.cacheRoot.path)" + ramField.stringValue = "\(FrameCache.ramGB)" + let pipeline = MediaPipeline.shared + let est = DocumentContext.current.chunks.optimizeEstimate(for: project) + usageLabel.stringValue = String( + format: "Using %.1f of %.0f GB · this project fully optimized ≈ %.1f GB (%.1f GB built)", + Double(pipeline.ledgerBytes) / 1e9, Double(pipeline.maxCacheBytes) / 1e9, + Double(est.total) / 1e9, Double(est.built) / 1e9) + cacheLabel.stringValue = "Cache: \(pipeline.cacheRoot.path)" } @objc private func fpsChanged() { @@ -393,7 +426,14 @@ final class SettingsWindow: NSObject { @objc private func cacheChanged() { let gb = Int(cacheField.stringValue) ?? 50 UserDefaults.standard.set(max(1, gb), forKey: "maxCacheGB") - MediaPipeline.shared.evictIfNeeded() + MediaPipeline.shared.evictIfNeeded(reconcile: true) + sync() + } + @objc private func ramChanged() { + let gb = Int(ramField.stringValue) ?? 2 + UserDefaults.standard.set(max(1, gb), forKey: "maxRAMGB") + FrameCache.shared.refreshBudget() + sync() } @objc private func revealCache() { NSWorkspace.shared.activateFileViewerSelecting([MediaPipeline.shared.cacheRoot]) diff --git a/sequencer/Sources/Sequencer/Transcript.swift b/sequencer/Sources/Sequencer/Transcript.swift new file mode 100644 index 0000000000000000000000000000000000000000..7dd5a57f8d1b4179b7912f0c1a165c6f2a2df40b --- /dev/null +++ b/sequencer/Sources/Sequencer/Transcript.swift @@ -0,0 +1,469 @@ +import AppKit + +// Clover-recorder transcript integration: when a `cam.mov` has a sibling +// `transcript.json` (the format the recorder writes), the viewer shows +// synchronized two-line karaoke captions over the picture. +// +// All transcript times are in the media's OWN (source) timebase — seconds from +// the start of cam.mov — so a clip's `sourceTime(at:)` maps the playhead into +// them, respecting trim (srcIn) and speed. + +/// One spoken word with its timing (source seconds) and which segment +/// (sentence) it came from — segment changes force a line break so sentences +/// don't run together. +struct TranscriptWord { + var text: String + var start: Double + var end: Double + var seg: Int +} + +/// A parsed clover `transcript.json`: words flattened across segments in time +/// order. `key` is the file path — its identity for layout caching. +struct Transcript { + var words: [TranscriptWord] + var key: String + + // MARK: Decoding shapes (tolerant — unknown/missing fields are skipped) + + private struct RawWord: Decodable { var word: String?; var start: Double?; var end: Double? } + private struct RawSeg: Decodable { + var start: Double?; var end: Double?; var text: String?; var words: [RawWord]? + } + private struct RawTranscript: Decodable { var segments: [RawSeg]? } + + /// Parse raw JSON. Returns nil if there's nothing usable to show. + static func parse(_ data: Data, key: String) -> Transcript? { + guard let raw = try? JSONDecoder().decode(RawTranscript.self, from: data), + let segments = raw.segments else { return nil } + var words: [TranscriptWord] = [] + for (si, seg) in segments.enumerated() { + let wordList = seg.words ?? [] + var added = false + for rw in wordList { + guard let start = rw.start else { continue } + let text = (rw.word ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + guard !text.isEmpty else { continue } + words.append(TranscriptWord(text: text, start: start, + end: max(start, rw.end ?? start), seg: si)) + added = true + } + // A segment with no per-word timing still shows as one block spanning + // the segment, so nothing goes silently missing. + if !added, let start = seg.start { + let text = (seg.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + if !text.isEmpty { + words.append(TranscriptWord(text: text, start: start, + end: max(start, seg.end ?? start), seg: si)) + } + } + } + guard !words.isEmpty else { return nil } + // Recorder output is monotonic in start time; sort defensively (stable, + // so an already-sorted file keeps its segment contiguity) so the + // active-word binary search below is always valid. + words.sort { $0.start < $1.start } + return Transcript(words: words, key: key) + } + + // MARK: Sibling-file loading (cached) + + private static var cache: [String: Transcript?] = [:] + + /// The transcript for a media file, or nil unless it is named `cam.mov` and + /// has a readable `transcript.json` in the same folder. Cached by path so + /// the per-frame visibility probe is cheap. + static func load(forVideo url: URL) -> Transcript? { + guard url.lastPathComponent.lowercased() == "cam.mov" else { return nil } + let path = url.deletingLastPathComponent() + .appendingPathComponent("transcript.json").path + if let hit = cache[path] { return hit } + let parsed = (try? Data(contentsOf: URL(fileURLWithPath: path))) + .flatMap { parse($0, key: path) } + cache[path] = parsed + return parsed + } + + /// Drop cached parses so a re-imported/edited transcript is re-read. + static func clearCache() { cache.removeAll() } + + // MARK: Lookup + + /// Index of the word active at source time `s` — the last word that has + /// started. A word stays "current" until the next one begins, so there are + /// no gaps. Returns -1 before the first word. + func activeIndex(at s: Double) -> Int { + var lo = 0, hi = words.count - 1, res = -1 + while lo <= hi { + let mid = (lo + hi) / 2 + if words[mid].start <= s { res = mid; lo = mid + 1 } else { hi = mid - 1 } + } + return res + } +} + +/// Two-line synchronized captions drawn over the viewer: white text on a black +/// clipped background, the active word in magenta, always on the TOP line. +/// New lines rise from the bottom and leave above the top as speech advances. +/// +/// **The whole thing is a pure function of the playhead.** Nothing here uses a +/// timer, Core Animation, or wall-clock tween: the vertical scroll offset is +/// computed straight from the source time, so stepping frame-by-frame steps the +/// animation and seeking lands with no motion (exactly what was asked for). +final class SubtitleOverlay: NSView { + var ctx: DocumentContext = .headless { + didSet { + guard oldValue !== ctx else { return } + oldValue.notify.removeObserver(self, name: .playheadChanged, object: nil) + ctx.notify.addObserver(self, selector: #selector(sync), + name: .playheadChanged, object: nil) + sync() + } + } + private var store: Store { ctx.store } + private var playback: PlaybackController { ctx.playback } + private var session: SessionState { ctx.session } + + /// The transcript being shown and the source time to render it at — both + /// recomputed on every playhead move in `sync()`. + private var transcript: Transcript? + private var sourceTime: Double = 0 + /// The clip the transcript is being shown for — kept so a word click can map + /// the word's source time back to a timeline moment (inverse of the scroll). + private var clip: Clip? + + /// Laid-out lines (each an array of positioned words) cached per transcript + /// + box width. `lineOfWord[i]` is the line word `i` landed on. + private struct LaidWord { var index: Int; var text: String; var x: CGFloat; var width: CGFloat } + private var lines: [[LaidWord]] = [] + private var lineOfWord: [Int] = [] + /// Natural content width (points) of each laid-out line — drives the black + /// backing that hugs the visible two lines. + private var lineWidths: [CGFloat] = [] + private var laidKey: String? + private var laidWidth: CGFloat = -1 + + /// What to mark at the current source time: a spoken word painted magenta, + /// or — during a real pause between two words — a magenta caret sitting + /// after the last spoken word, signalling that nothing is being said. + private enum Mark { case word(Int); case caret(after: Int); case none } + + /// Gaps up to this (seconds) hold the previous word lit instead of blinking + /// off; longer gaps show the caret. Small inter-word silences are noise and + /// shouldn't flicker the highlight. + private static let extendGap = 0.25 + + private static let magenta = NSColor(srgbRed: 1.0, green: 0.22, blue: 0.86, alpha: 1) + + override init(frame: NSRect) { + super.init(frame: frame) + wantsLayer = true + layer?.backgroundColor = NSColor.clear.cgColor + isHidden = true + NotificationCenter.default.addObserver(self, selector: #selector(sync), + name: .projectChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(sync), + name: .viewOptionsChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(sync), + name: .viewerNeedsRefresh, object: nil) + ctx.notify.addObserver(self, selector: #selector(sync), + name: .playheadChanged, object: nil) + } + required init?(coder: NSCoder) { fatalError() } + + override var isFlipped: Bool { true } // y grows downward: top line at small y + + // Swallow clicks only when they land on a word — a word click scrubs the + // playhead to that word (see `mouseDown`). Clicks on the box's gaps/padding + // pass through to the cell beneath so clip selection still works there. + override func hitTest(_ point: NSPoint) -> NSView? { + guard !isHidden, transcript != nil else { return nil } + return wordIndex(atLocal: convert(point, from: superview)) != nil ? self : nil + } + + /// Scrub to the clicked word's start. + override func mouseDown(with event: NSEvent) { + let local = convert(event.locationInWindow, from: nil) + guard let transcript, let clip, + let idx = wordIndex(atLocal: local) else { return } + playback.seek(to: clip.timelineTime(forSource: transcript.words[idx].start)) + } + + /// The transcript word whose drawn rect contains `p` (this view's flipped, + /// local coords), or nil. Mirrors `draw`'s layout so hit-testing and painting + /// never disagree. + private func wordIndex(atLocal p: NSPoint) -> Int? { + guard transcript != nil, bounds.width > 40 else { return nil } + rebuildLayoutIfNeeded() + guard !lines.isEmpty else { return nil } + let m = Metrics(width: bounds.width) + // Only the visible two-line band is clickable (matches what's drawn). + guard p.y >= m.vpad, p.y <= bounds.height - m.vpad else { return nil } + let scroll = scrollLines(at: sourceTime) + for (li, line) in lines.enumerated() { + let topY = m.vpad + (CGFloat(li) - scroll) * m.lineH + guard p.y >= topY, p.y < topY + m.lineH else { continue } + for lw in line where p.x >= lw.x && p.x < lw.x + lw.width { + return lw.index + } + } + return nil + } + + // MARK: Geometry + + /// Box metrics for a given width. Font scales gently with the box so the + /// captions stay legible on small viewers without ballooning on large ones. + struct Metrics { + let fontSize, lineH, vpad, hpad, height: CGFloat + init(width: CGFloat) { + fontSize = min(26, max(14, width / 30)) + lineH = (fontSize * 1.32).rounded(.up) + vpad = (lineH * 0.34).rounded() + hpad = 16 + height = lineH * 2 + vpad * 2 + } + } + + private static func font(_ size: CGFloat) -> NSFont { + .systemFont(ofSize: size, weight: .semibold) + } + + /// Where the caption box sits inside the viewer: centred horizontally, near + /// the bottom. Sized only from the viewer bounds, so the parent can position + /// it in `layout()` without knowing the content. + func preferredFrame(in bounds: NSRect) -> NSRect { + let w = min(max(bounds.width * 0.72, 260), 860) + let m = Metrics(width: w) + let x = ((bounds.width - w) / 2).rounded() + let margin = max(16, bounds.height * 0.045) + let y = max(m.vpad, (bounds.height - m.height - margin).rounded()) + return NSRect(x: x, y: y, width: w, height: m.height) + } + + // MARK: State + + /// Recompute which transcript/clip is under the playhead and at what source + /// time, then redraw. Hidden whenever the toggle is off or nothing under the + /// playhead carries a transcript. + @objc func sync() { + guard session.subtitlesEnabled, let (clip, media) = transcriptClipUnderPlayhead() else { + if !isHidden { isHidden = true } + transcript = nil + self.clip = nil + return + } + transcript = Transcript.load(forVideo: media.url) + self.clip = clip + sourceTime = max(0, clip.sourceTime(at: playback.playhead)) + isHidden = transcript == nil + needsDisplay = true + } + + /// The visible video clip under the playhead whose media is a transcript- + /// bearing `cam.mov`. Mirrors the viewer's own hide/focus visibility rule; + /// the Priority pane wins when it qualifies. + private func transcriptClipUnderPlayhead() -> (Clip, MediaItem)? { + let project = store.project + let t = playback.playhead + let focusActive = !session.focusedTracks.isEmpty || session.fusionFocus + func visible(_ ref: TrackRef) -> Bool { + focusActive ? session.focusedTracks.contains(ref) + : !session.hiddenTracks.contains(ref) + } + var found: (TrackRef, Clip, MediaItem)? + for ref in project.laneRefs where visible(ref) { + guard let clip = project.clipAt(track: ref, time: t, kind: .video), + let m = project.media(clip.mediaId), + Transcript.load(forVideo: m.url) != nil else { continue } + if ref == session.priorityPane { return (clip, m) } + if found == nil { found = (ref, clip, m) } + } + return found.map { ($0.1, $0.2) } + } + + // MARK: Layout + + /// Wrap the transcript into center-aligned lines that fit the box, breaking + /// at segment (sentence) boundaries. Cached until the box width or the + /// transcript changes. + private func rebuildLayoutIfNeeded() { + guard let transcript else { lines = []; lineOfWord = []; lineWidths = []; return } + let m = Metrics(width: bounds.width) + if laidKey == transcript.key && abs(laidWidth - bounds.width) < 0.5 { return } + laidKey = transcript.key + laidWidth = bounds.width + + let font = Self.font(m.fontSize) + let attrs: [NSAttributedString.Key: Any] = [.font: font] + func measure(_ s: String) -> CGFloat { (s as NSString).size(withAttributes: attrs).width } + let space = measure(" ") + let maxW = bounds.width - m.hpad * 2 + + var built: [[LaidWord]] = [] + var widths: [CGFloat] = [] + var lineIdx = [Int](repeating: 0, count: transcript.words.count) + var cur: [LaidWord] = [] + var penX: CGFloat = 0 + + func flush() { + guard !cur.isEmpty else { return } + let lineW = (cur.last?.x ?? 0) + (cur.last?.width ?? 0) + let off = (m.hpad + max(0, (maxW - lineW) / 2)).rounded() + built.append(cur.map { LaidWord(index: $0.index, text: $0.text, + x: $0.x + off, width: $0.width) }) + widths.append(lineW) + cur = [] + penX = 0 + } + + for (gi, w) in transcript.words.enumerated() { + let width = measure(w.text) + let newSegment = gi > 0 && w.seg != transcript.words[gi - 1].seg + let x = cur.isEmpty ? 0 : penX + space + if newSegment || (!cur.isEmpty && x + width > maxW) { + flush() + } + let placeX = cur.isEmpty ? 0 : penX + space + cur.append(LaidWord(index: gi, text: w.text, x: placeX, width: width)) + penX = placeX + width + lineIdx[gi] = built.count // the line this word will land on once flushed + } + flush() + lines = built + lineOfWord = lineIdx + lineWidths = widths + } + + /// Content width (points) to draw the black backing at for a given vertical + /// scroll. At rest on line `k` this is the wider of the two visible lines + /// (`k` and `k+1`); mid-slide it blends the outgoing and incoming pairs, so + /// the box width eases in lockstep with the scroll — same playhead-driven, + /// frame-steppable motion, no Core Animation. + private func contentWidth(atScroll s: CGFloat) -> CGFloat { + guard !lineWidths.isEmpty else { return 0 } + func pair(_ k: Int) -> CGFloat { + let a = (k >= 0 && k < lineWidths.count) ? lineWidths[k] : 0 + let b = (k + 1 >= 0 && k + 1 < lineWidths.count) ? lineWidths[k + 1] : 0 + return max(a, b) + } + let k = Int(s.rounded(.down)) + let f = s - CGFloat(k) + return pair(k) + (pair(k + 1) - pair(k)) * f + } + + /// Continuous vertical scroll, in line units, as a pure function of source + /// time. Holds on the active word's line, then slides up over ~0.3s when a + /// new line begins — so the focused word slides into the top row and stays + /// there. Interpolating over time (not a CA animation) is what makes it + /// frame-steppable and animation-free on a seek. + private func scrollLines(at s: Double) -> CGFloat { + guard let transcript, !lineOfWord.isEmpty else { return 0 } + let i = transcript.activeIndex(at: s) + guard i >= 0 else { return 0 } + let cur = lineOfWord[i] + let prev = i > 0 ? lineOfWord[i - 1] : cur + if cur == prev { return CGFloat(cur) } + let startI = transcript.words[i].start + let nextStart = i + 1 < transcript.words.count + ? transcript.words[i + 1].start : transcript.words[i].end + let slide = min(0.30, max(0.0001, nextStart - startI)) + let t = min(1, max(0, (s - startI) / slide)) + let e = t * t * (3 - 2 * t) // smoothstep + return CGFloat(prev) + (CGFloat(cur) - CGFloat(prev)) * e + } + + /// Resolve what to highlight at source time `s`. While a word is being + /// spoken it lights up. In the gap after it: a *small* gap holds the word lit + /// right up to the next one (so brief silences don't flicker); a *longer* gap + /// shows a caret between the two words, marking the pause. Trailing silence + /// after the final word fades to nothing. + private func mark(at s: Double) -> Mark { + guard let transcript else { return .none } + let i = transcript.activeIndex(at: s) + guard i >= 0 else { return .none } + let w = transcript.words[i] + if s <= w.end { return .word(i) } // still being spoken + if i + 1 < transcript.words.count { + let gap = transcript.words[i + 1].start - w.end + return gap <= Self.extendGap ? .word(i) // tiny gap: extend it + : .caret(after: i) // real pause: caret + } + return s <= w.end + 0.05 ? .word(i) : .none // trailing silence + } + + // MARK: Draw + + override func draw(_ dirty: NSRect) { + guard transcript != nil, bounds.width > 40 else { return } + rebuildLayoutIfNeeded() + guard !lines.isEmpty else { return } + let m = Metrics(width: bounds.width) + + let font = Self.font(m.fontSize) + let scroll = scrollLines(at: sourceTime) + let markResult = mark(at: sourceTime) + var highlight = -1 + if case .word(let idx) = markResult { highlight = idx } + + // Black rounded backing, sized to hug the two visible lines and centred + // in the (fixed, transparent) container — width eases with the scroll. + let boxW = min(bounds.width, (contentWidth(atScroll: scroll) + m.hpad * 2).rounded()) + let boxX = ((bounds.width - boxW) / 2).rounded() + let boxRect = NSRect(x: boxX, y: 0, width: boxW, height: bounds.height) + let bg = NSBezierPath(roundedRect: boxRect, xRadius: 10, yRadius: 10) + NSColor(calibratedWhite: 0, alpha: 0.8).setFill() + bg.fill() + NSGraphicsContext.saveGraphicsState() + bg.addClip() + // Clip text to the inner TWO-line band (inset by the vertical padding), + // not the full padded box. This is what keeps the focused word pinned to + // the top line: without it, a settled neighbour line's descenders bleed + // into the top/bottom padding and the active line reads as the 2nd row. + // Lines still animate through this band — they're simply cut off cleanly + // at its top/bottom edges as they rise away / come up from below. The + // rounded-box clip above also trims lines sliding through a narrower box. + NSBezierPath(rect: NSRect(x: 0, y: m.vpad, + width: bounds.width, + height: bounds.height - m.vpad * 2)).addClip() + + // Vertical inset so the glyphs sit centred in their line box. + let glyphH = font.ascender - font.descender + let textInset = ((m.lineH - glyphH) / 2).rounded() + + // A caret (magenta cursor) sits just after the last spoken word during a + // real pause. Resolve its line/x from that word's laid-out position. + var caretLine = -1 + var caretX: CGFloat = 0 + if case .caret(let after) = markResult, after < lineOfWord.count { + let li = lineOfWord[after] + if li < lines.count, let prev = lines[li].first(where: { $0.index == after }) { + caretLine = li + let rightEdge = prev.x + prev.width + // Centre the caret in the gap to the next word when it shares + // this line; if the next word wrapped away, sit just past this one. + if let next = lines[li].first(where: { $0.index == after + 1 }) { + caretX = ((rightEdge + next.x) / 2).rounded() + } else { + caretX = (rightEdge + 3).rounded() + } + } + } + + for (li, line) in lines.enumerated() { + let topY = m.vpad + (CGFloat(li) - scroll) * m.lineH + if topY > bounds.height || topY + m.lineH < 0 { continue } // fully clipped + let baselineY = topY + textInset + for lw in line { + let color = lw.index == highlight ? Self.magenta : NSColor.white + let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: color] + (lw.text as NSString).draw(at: NSPoint(x: lw.x, y: baselineY), withAttributes: attrs) + } + if li == caretLine { + Self.magenta.setFill() + NSBezierPath(rect: NSRect(x: caretX - 1, y: baselineY, width: 2, height: glyphH)).fill() + } + } + NSGraphicsContext.restoreGraphicsState() + } +} diff --git a/sequencer/Sources/Sequencer/TransportBar.swift b/sequencer/Sources/Sequencer/TransportBar.swift index 20caa18719162f2a3d46515b13d2e18220051028..e335e77c18d0639f770da0a13d5246ddeaa32900 100644 --- a/sequencer/Sources/Sequencer/TransportBar.swift +++ b/sequencer/Sources/Sequencer/TransportBar.swift @@ -90,6 +90,7 @@ final class TransportBar: NSView { private let shapesButton = InstantButton(title: "", target: nil, action: nil) private let colorSwatch = InstantButton(title: "", target: nil, action: nil) private let snapButton = InstantButton(title: "", target: nil, action: nil) + private let subtitleButton = InstantButton(title: "", target: nil, action: nil) private let filmstripButton = InstantButton(title: "", target: nil, action: nil) private let viewerButton = InstantButton(title: "", target: nil, action: nil) @@ -237,6 +238,11 @@ final class TransportBar: NSView { snapButton.target = self snapButton.action = #selector(toggleSnap) styleIconButton(snapButton, tip: "Snap (Y)") + subtitleButton.image = NSImage(systemSymbolName: "captions.bubble", + accessibilityDescription: "subtitles") + subtitleButton.target = self + subtitleButton.action = #selector(toggleSubtitles) + styleIconButton(subtitleButton, tip: "Subtitles") filmstripButton.image = NSImage(systemSymbolName: "film", accessibilityDescription: "clip thumbnails") filmstripButton.target = self @@ -257,8 +263,8 @@ final class TransportBar: NSView { heightSlider.toolTip = "Track height (⌥⌘= / ⌥⌘- / ⌥⌘0)" heightSlider.widthAnchor.constraint(equalToConstant: 80).isActive = true - let rightStack = NSStackView(views: [netWarn, jobs, snapButton, filmstripButton, - viewerButton, heightSlider]) + let rightStack = NSStackView(views: [netWarn, jobs, snapButton, subtitleButton, + filmstripButton, viewerButton, heightSlider]) rightStack.orientation = .horizontal rightStack.spacing = 4 rightStack.setCustomSpacing(8, after: netWarn) @@ -390,6 +396,7 @@ final class TransportBar: NSView { } @objc private func toggleSnap() { session.snapping.toggle() } + @objc private func toggleSubtitles() { session.subtitlesEnabled.toggle() } @objc private func toggleFilmstrips() { session.showFilmstrips.toggle() } @objc private func viewerClicked() { @@ -614,6 +621,7 @@ final class TransportBar: NSView { shapesButton.alphaValue = canDraw ? 1 : 0.3 colorSwatch.layer?.backgroundColor = session.drawColor.cgColor highlight(snapButton, session.snapping) + highlight(subtitleButton, session.subtitlesEnabled) highlight(filmstripButton, session.showFilmstrips) highlight(viewerButton, session.previewsOnLeft || ((window?.windowController as? SequencerWindowController)?.previewsArePopped ?? false)) @@ -667,6 +675,21 @@ final class TransportBar: NSView { jobs.textColor = Theme.subtleLabel jobs.stringValue = "⏸ \(total) chunk\(total == 1 ? "" : "s")" jobs.toolTip = "When paused, clip optimization happens only during playback." + } else if chunks.budgetStarved { + // The cache is at its cap and the project is bigger than it: the + // builder is deliberately NOT trying to finish the queue — it + // maintains a working set around where you play and edit. Say so, + // instead of dangling a queue count that will never drain. + jobs.textColor = Theme.subtleLabel + jobs.stringValue = building > 0 + ? "cache full — optimizing \(building) near playhead" + : "cache full — optimizing on demand" + let gb = Double(MediaPipeline.shared.maxCacheBytes) / 1e9 + jobs.toolTip = String(format: + "The proxy cache is at its %.0f GB cap, so this project can't be " + + "fully optimized at once. Proxies are kept where you play, edit, " + + "and land on clips, and rebuilt on demand elsewhere. Raise the " + + "cap in Settings to fit more. Click to pause optimization.", gb) } else if queued == 0 { jobs.textColor = .systemOrange jobs.stringValue = "optimizing \(building) chunk\(building == 1 ? "" : "s")" diff --git a/sequencer/Sources/Sequencer/UITest.swift b/sequencer/Sources/Sequencer/UITest.swift index c3e8919150c043bd994fd8e80fe6bdff2d1eed13..15b3fc006cd03cdcf7cd8b66814b22402f3cae79 100644 --- a/sequencer/Sources/Sequencer/UITest.swift +++ b/sequencer/Sources/Sequencer/UITest.swift @@ -37,7 +37,8 @@ func runUITest() { // Coordinate helpers mirroring the view's layout constants. func x(_ sec: Double) -> CGFloat { timeline.testXFor(sec) } - func laneY(_ row: Int) -> CGFloat { 26 + CGFloat(row) * (64 + 4) + 4 + 32 } // lane mid + // lane mid: ruler (26) + optimization strip (5) + row offset + func laneY(_ row: Int) -> CGFloat { 26 + 5 + CGFloat(row) * (64 + 4) + 4 + 32 } // NSEvent locationInWindow is bottom-left origin; view is flipped & fills window. func winPoint(_ vx: CGFloat, _ vy: CGFloat) -> NSPoint { NSPoint(x: vx, y: 400 - vy) } @@ -487,6 +488,16 @@ func runUITest() { let top = lum(comp27, 0.5, 0.15), bottom = lum(comp27, 0.5, 0.85) check(top >= 0 && top < 0.5 && bottom > 0.9, "stroke drawn at top STAYS at top (top=\(top), bottom=\(bottom))") + + // 27e. The stroke rides the SHARED undo timeline: ⌘Z clears the drawing, + // ⌘⇧Z brings it back — no separate raster-undo lane. + store.undo() + let undone = lum(DocumentContext.headless.boards.composite(for: board27), 0.5, 0.15) + store.redo() + let redone = lum(DocumentContext.headless.boards.composite(for: board27), 0.5, 0.15) + check(undone > 0.9 && redone < 0.5, + "raster stroke undoes and redoes on the shared undo timeline " + + "(drawn=\(top) undone=\(undone) redone=\(redone))") DocumentContext.headless.boards.saveRaster(nil, boardId: board27.id) // 28. Overlaps ignore audio (layering is allowed). @@ -597,6 +608,40 @@ func runUITest() { check(lcs.count == 2 && abs(lcs[1].start - 20) < 1e-6 && abs(lcs[1].duration - 15) < 1e-6, "⌥← ripple-trims the left side to the playhead") + // 31d-ripple. ⌥-drag a clip's IN edge = ripple trim: the clip's start stays + // pinned, its in-point re-trims, and the follower shifts to stay gapless. + // Media out-point is fixed, so the source range only loses (or regains) + // frames at the head. + var mri = ProjectModel(); mri.fps = 30; mri.media = [media] + mri.tracks = [Track(hue: 0.6)] + let ra = Clip(mediaId: media.id, track: .video(0), start: 20, srcIn: 10, duration: 20) + let rb = Clip(mediaId: media.id, track: .video(0), start: 40, srcIn: 0, duration: 20) + mri.clips = [ra, rb] + store.replaceForTest(mri) + store.selection = [] + DocumentContext.headless.session.snapping = false + timeline.zoomToFit() + // Drag ra's in edge right by 10s: trim the head, pull rb in with it. + drag(from: winPoint(x(20) + 3, laneY(0)), to: winPoint(x(30) + 3, laneY(0)), flags: [.option]) + let ra1 = clip(ra.id)!, rb1 = clip(rb.id)! + check(abs(ra1.start - 20) < 1e-6 && abs(ra1.srcIn - 20) < 0.5 + && abs(ra1.duration - 10) < 0.5 && abs(rb1.start - 30) < 0.5, + "⌥ in-edge ripple: start pinned, head trimmed, follower pulled in " + + "(got start \(ra1.start), srcIn \(ra1.srcIn), dur \(ra1.duration), rb \(rb1.start))") + check(abs((ra1.srcIn + ra1.duration) - (ra.srcIn + ra.duration)) < 0.5, + "⌥ in-edge ripple keeps the source out-point fixed") + store.undo() + check(abs(clip(ra.id)!.srcIn - 10) < 1e-6 && abs(clip(rb.id)!.start - 40) < 1e-6, + "⌥ in-edge ripple undoes as one step") + // Drag left by 10s: extend the head, push the follower out. + drag(from: winPoint(x(20) + 3, laneY(0)), to: winPoint(x(10) + 3, laneY(0)), flags: [.option]) + let ra2 = clip(ra.id)!, rb2 = clip(rb.id)! + check(abs(ra2.start - 20) < 1e-6 && abs(ra2.srcIn - 0) < 0.5 + && abs(ra2.duration - 30) < 0.5 && abs(rb2.start - 50) < 0.5, + "⌥ in-edge ripple (drag left): head extended, follower pushed out " + + "(got start \(ra2.start), srcIn \(ra2.srcIn), dur \(ra2.duration), rb \(rb2.start))") + store.undo() + // 31e. Delete-the-space closes a blank gap at the playhead. var mb = ProjectModel(); mb.fps = 30; mb.media = [media] mb.tracks = [Track(hue: 0.4)] diff --git a/sequencer/Sources/Sequencer/ViewerGridView.swift b/sequencer/Sources/Sequencer/ViewerGridView.swift index ea4f11d7bedc6a37557f7e0e9cd67892cf1c9248..a609d9124d20ff27b39def38e92123c2f042390b 100644 --- a/sequencer/Sources/Sequencer/ViewerGridView.swift +++ b/sequencer/Sources/Sequencer/ViewerGridView.swift @@ -20,6 +20,7 @@ final class ViewerGridView: NSView { // and render nothing but the black cell background. for c in cells.values { c.ctx = ctx } fusionCell?.ctx = ctx + subtitles.ctx = ctx } } private var store: Store { ctx.store } @@ -46,6 +47,11 @@ final class ViewerGridView: NSView { /// one-click way back). Only shown when there are no panes at all. private let placeholder = ViewerPlaceholder() + /// Synchronized clover-transcript captions, drawn on top of the cells. + /// Positions itself (via `preferredFrame`) but hides itself unless a + /// transcript-bearing clip is under the playhead and captions are enabled. + private let subtitles = SubtitleOverlay() + override init(frame: NSRect) { super.init(frame: frame) wantsLayer = true @@ -76,6 +82,11 @@ final class ViewerGridView: NSView { placeholder.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 16), placeholder.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -16), ]) + // Captions ride on top of the cells. Added last so later cells (inserted + // `.below`) stay under it; positioned in `layout()`. + subtitles.ctx = ctx + addSubview(subtitles) + // Dropping media anywhere in the empty viewer imports it (delegating to // the timeline's importer), so the "Drag files…" prompt is real. registerForDraggedTypes([.fileURL]) @@ -304,6 +315,12 @@ final class ViewerGridView: NSView { override func layout() { super.layout() applyFrames(animated: false, appearing: []) + // Keep the caption box centred near the bottom and above every cell. + subtitles.frame = subtitles.preferredFrame(in: bounds) + if subviews.last !== subtitles { + subtitles.removeFromSuperview() + addSubview(subtitles) + } } /// Lay out the panes and animate them into place. Normally a justified-rows @@ -967,11 +984,33 @@ final class ViewerCell: ViewerCellBase { /// those flash; instead we wait out a short grace period and only reveal it /// if the cell is still empty. Any resolved frame cancels it. private var pendingSpinner: DispatchWorkItem? - private func scheduleLoadingOverlay() { - guard !overlayVisible, pendingSpinner == nil else { return } + private var overlayText: String? + + // Miss metrics: every spinner that actually shows is one recorded "miss", + // logged with a diagnosis of WHY the frame wasn't ready (see + // ChunkManager.missDiagnosis) and, on recovery, how long it lasted — + // so "I saw the loading screen" is answerable from the log. + private static var missCount = 0 + private static var missSeconds = 0.0 + private var missStart: Date? + + private func scheduleLoadingOverlay(media: MediaItem?, clip: Clip?) { + guard overlayText == nil, pendingSpinner == nil else { return } let work = DispatchWorkItem { [weak self] in - self?.pendingSpinner = nil - self?.showLoadingOverlay(true) + guard let self else { return } + self.pendingSpinner = nil + self.showOverlay("Loading Media…", spinning: true) + self.missStart = Date() + Self.missCount += 1 + if let media, let clip { + let src = max(0, clip.sourceTime(at: self.playback.playhead)) + SeqLog.log("[miss] spinner ON %@ src=%.1f %@ (miss #%d)", + media.displayName, src, + self.chunks.missDiagnosis(media: media, sourceTime: src), + Self.missCount) + } else { + SeqLog.log("[miss] spinner ON (no clip resolved) (miss #%d)", Self.missCount) + } } pendingSpinner = work DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: work) @@ -979,15 +1018,38 @@ final class ViewerCell: ViewerCellBase { private func cancelLoadingOverlay() { pendingSpinner?.cancel() pendingSpinner = nil - showLoadingOverlay(false) + if let start = missStart { + missStart = nil + let d = Date().timeIntervalSince(start) + Self.missSeconds += d + SeqLog.log("[miss] spinner OFF after %.1fs (session: %d misses, %.0fs total)", + d, Self.missCount, Self.missSeconds) + } + hideOverlay() } - private func showLoadingOverlay(_ show: Bool) { - if show { layoutOverlay(in: bounds) } - guard overlayVisible != show else { return } - overlayVisible = show - overlayBg.isHidden = !show - if show { + /// A persistent (non-transient) overlay — media offline / failed to decode. + /// No debounce and no spinner: it's a steady state, not a "wait a moment." + private func showStateOverlay(_ text: String) { + pendingSpinner?.cancel(); pendingSpinner = nil + if let start = missStart { // spinner resolved INTO a steady state + missStart = nil + let d = Date().timeIntervalSince(start) + Self.missSeconds += d + SeqLog.log("[miss] spinner OFF after %.1fs → %@", d, text) + } + showOverlay(text, spinning: false) + } + + private func showOverlay(_ text: String, spinning: Bool) { + guard overlayText != text else { return } // already showing this + overlayText = text + overlayVisible = true + layoutOverlay(in: bounds) + loadingText.string = text + spinnerLayer.isHidden = !spinning + overlayBg.isHidden = false + if spinning { if spinnerLayer.animation(forKey: "spin") == nil { let a = CABasicAnimation(keyPath: "transform.rotation.z") a.fromValue = 0 @@ -1000,6 +1062,13 @@ final class ViewerCell: ViewerCellBase { spinnerLayer.removeAnimation(forKey: "spin") } } + private func hideOverlay() { + guard overlayText != nil else { return } + overlayText = nil + overlayVisible = false + overlayBg.isHidden = true + spinnerLayer.removeAnimation(forKey: "spin") + } private var currentClipId: UUID? @@ -1285,17 +1354,54 @@ final class ViewerCell: ViewerCellBase { && frontLayer.isReadyForDisplay && onTime var status = covered ? "" : "processing…" + if !itemOK { + // The player can't show this moment yet: start decoding the real + // frame into the RAM cache now (from a chunk, rescue slice, or + // playable original) so the next update can stand in with it — + // exact where the filmstrip is a 240px thumb. Self-deduping. + FrameCache.shared.warm(mediaKey: media.cacheKey, at: src, + source: chunks.frameSource(media: media, sourceTime: src)) + } if itemOK { frontLayer.isHidden = false imageLayer.isHidden = true cancelLoadingOverlay() + } else if let frame = FrameCache.shared.image(media: media, at: src) { + // The RAM frame cache has the exact (full-quality) frame for this + // moment — a warmed cut boundary, or a stand-in decoded on demand + // below. Better than the 240px filmstrip thumb, and it makes the + // hold across an item swap invisible. + frontLayer.isHidden = true + imageLayer.isHidden = false + imageLayer.contents = frame + cancelLoadingOverlay() + if MediaPipeline.shared.isOffline(media) { status = "offline" } } else if let strip = MediaPipeline.shared.filmstripImage(for: media, at: src) { // A filmstrip is a real frame for this moment: stand in with it so - // the transition is filmstrip → video, never black. + // the transition is filmstrip → video, never black. If the original + // is offline the cached strip still previews, but flag it in the + // corner so it's clear playback/export won't work until it's back. frontLayer.isHidden = true imageLayer.isHidden = false imageLayer.contents = strip cancelLoadingOverlay() + if MediaPipeline.shared.isOffline(media) { status = "offline" } + } else if MediaPipeline.shared.isOffline(media) { + // The original file is gone (unmounted NAS / moved). Don't imply it's + // loading — say so plainly, and don't spin forever. + frontLayer.isHidden = true + imageLayer.isHidden = true + imageLayer.contents = nil + showStateOverlay("Media Offline") + status = "offline" + } else if chunks.buildFailed(media: media, sourceTime: src) { + // The proxy chunk hard-failed and the original isn't playable — this + // frame genuinely can't be shown; surface it instead of spinning. + frontLayer.isHidden = true + imageLayer.isHidden = true + imageLayer.contents = nil + showStateOverlay("Can't Decode") + status = "failed" } else { // No live video AND no stand-in. This is usually just a transient // (an item swap or a seek that lands within a few frames), so do @@ -1303,7 +1409,7 @@ final class ViewerCell: ViewerCellBase { // on screen and only escalate to the framed "Loading Media…" // overlay if the empty state actually persists (see // scheduleLoadingOverlay). That kills the paused spinner flashes. - scheduleLoadingOverlay() + scheduleLoadingOverlay(media: media, clip: clip) status = "" } setStatus(status) diff --git a/sequencer/Sources/Sequencer/WindowController.swift b/sequencer/Sources/Sequencer/WindowController.swift index 52f9d043f10ee09a8caa69c3b5eb397b71091b2f..f7f77ca8500d041656517e6f492666e834bd89eb 100644 --- a/sequencer/Sources/Sequencer/WindowController.swift +++ b/sequencer/Sources/Sequencer/WindowController.swift @@ -167,11 +167,23 @@ final class SequencerWindowController: NSWindowController, NSWindowDelegate, } } + /// This project became frontmost — now (and only now) kick the whole-project + /// proxy pre-build so scrubbing anywhere is smooth. Deferring the fill to here + /// rather than document load is what stops state-restored *background* projects + /// from all transcoding their full proxy sets at once and overflowing the + /// shared cache (see DocumentContext.startServices). `ensure` is idempotent — + /// already-built chunks are skipped — so re-focusing a project is essentially + /// free. Ignore the popout window becoming main (it shares this ctx). + func windowDidBecomeMain(_ notification: Notification) { + guard (notification.object as? NSWindow) === window else { return } + ctx.chunks.ensure(for: ctx.store.project) + } + // MARK: - Menu validation (per-document items) func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { switch menuItem.action { - case #selector(undo): return store.canUndo || StoryboardEditor.shared.canUndoRaster + case #selector(undo): return store.canUndo case #selector(redo): return store.canRedo case #selector(deleteSelected), #selector(rippleDeleteSelected): return !store.selection.isEmpty @@ -211,11 +223,9 @@ final class SequencerWindowController: NSWindowController, NSWindowDelegate, // MARK: - Edit / Clip actions - @objc func undo() { - if StoryboardEditor.shared.undoRasterIfKey() { return } - if session.mainTool.isDraw, ctx.boards.undoLastStroke() { return } - store.undo() - } + // Drawing edits now live on the same undo timeline as model edits, so ⌘Z / + // ⌘⇧Z route through the Store regardless of which window is focused. + @objc func undo() { store.undo() } @objc func redo() { store.redo() } @objc func deselectAll() { store.selection = [] diff --git a/sequencer/Sources/Sequencer/main.swift b/sequencer/Sources/Sequencer/main.swift index 6310b1a0be3dfe9f9d901fc1a2f6791c00c96471..bae0c79178aeb475bd2929d0855e3f2616607df6 100644 --- a/sequencer/Sources/Sequencer/main.swift +++ b/sequencer/Sources/Sequencer/main.swift @@ -6,16 +6,59 @@ if CommandLine.arguments.count >= 3, CommandLine.arguments[1] == "--selftest" { exit(0) } +// Headless cache-budget test: sequencer --cachetest [capGB] +if CommandLine.arguments.count >= 3, CommandLine.arguments[1] == "--cachetest" { + let cap = CommandLine.arguments.count >= 4 ? Int(CommandLine.arguments[3]) ?? 0 : 0 + runCacheTest(path: CommandLine.arguments[2], capGB: cap) + exit(0) +} + if CommandLine.arguments.contains("--uitest") { _ = NSApplication.shared // AppKit needs an app instance for views/windows MainActor.assumeIsolated { runUITest() } } +// Diagnostic: bare AVPlayerLayer window on a stitched composition, using the +// app's own bundle/signing — isolates layer rendering from the app machinery. +if CommandLine.arguments.count >= 3, CommandLine.arguments[1] == "--layertest" { + let app = NSApplication.shared + app.setActivationPolicy(.regular) + MainActor.assumeIsolated { + runLayerTest(specs: Array(CommandLine.arguments.dropFirst(2))) + } + app.run() +} + if CommandLine.arguments.count >= 3, CommandLine.arguments[1] == "--perftest" { _ = NSApplication.shared MainActor.assumeIsolated { runPerfTest(path: CommandLine.arguments[2]) } } +// pkill/SIGTERM (run.sh does one per rebuild) must also take the ffmpeg +// children down — the default handler kills only this process, minting the +// orphan encoders that exhaust VideoToolbox and black out the next launch. +// A background queue, NOT main: a wedged main thread must not make the app +// unkillable (pkill looked ignored while main was frozen). +signal(SIGTERM, SIG_IGN) +signal(SIGINT, SIG_IGN) +let signalQueue = DispatchQueue(label: "sequencer.signals", qos: .userInteractive) +let termSource = DispatchSource.makeSignalSource(signal: SIGTERM, queue: signalQueue) +let intSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: signalQueue) +for src in [termSource, intSource] { + src.setEventHandler { + MediaPipeline.terminateChildren() + exit(0) + } + src.resume() +} + +HangMonitor.start() +// SEQ_HANGTEST=1: deliberately stall the main thread once, to verify the +// watchdog end-to-end (a [hang] line with this frame should hit the log). +if ProcessInfo.processInfo.environment["SEQ_HANGTEST"] != nil { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { hangTestStall() } +} + let app = SeqApplication.shared let delegate = AppDelegate() app.delegate = delegate diff --git a/sequencer/build.sh b/sequencer/build.sh index 4a412ae9f9151e8151247d1f5a9966a6cc932866..b4cff8ba91f0b99c3b8b4e20ad86342ec4615a7e 100755 --- a/sequencer/build.sh +++ b/sequencer/build.sh @@ -81,7 +81,7 @@ cat > "$APP/Contents/Info.plist" <<'PLIST' CFBundleExecutable Sequencer CFBundleIdentifier - com.clover.Sequencer + net.paperclover.Sequencer CFBundlePackageType APPL CFBundleShortVersionString @@ -109,7 +109,7 @@ cat > "$APP/Contents/Info.plist" <<'PLIST' Sequencer.ProjectDocument LSItemContentTypes - com.clover.sequencer.project + net.paperclover.sequencer.project CFBundleTypeExtensions @@ -121,7 +121,7 @@ cat > "$APP/Contents/Info.plist" <<'PLIST' UTTypeIdentifier - com.clover.sequencer.project + net.paperclover.sequencer.project UTTypeDescription Sequencer Project UTTypeConformsTo diff --git a/sequencer/run.sh b/sequencer/run.sh index d5753b4a966905ceb25d4764a84dbafeeb822ebf..3514c40647a81e465a6df236ec2a37dccd9a386d 100755 --- a/sequencer/run.sh +++ b/sequencer/run.sh @@ -1,8 +1,19 @@ #!/bin/sh +# Build and relaunch the app. Ships the RELEASE binary — the timeline is +# measurably 4× slower under -Onone, so the app people actually run must be +# optimized. Pass --debug to ship the debug build (e.g. when chasing a crash +# with full assertions/symbols). set -e cd "$(dirname "$0")" -swift build +CONF=release +[ "$1" = "--debug" ] && CONF=debug +swift build -c "$CONF" pkill -x Sequencer 2>/dev/null || true -cp .build/debug/Sequencer Sequencer.app/Contents/MacOS/Sequencer +# The app terminates its ffmpeg children on SIGTERM, but sweep any strays from +# older builds anyway — orphaned encoders exhaust the shared VideoToolbox +# session pool and the next launch's players silently render black. +sleep 1 +pkill -9 -f "Library/Caches/Sequencer" 2>/dev/null || true +cp ".build/$CONF/Sequencer" Sequencer.app/Contents/MacOS/Sequencer codesign --force --deep --sign "Sequencer Dev" Sequencer.app open Sequencer.app -- 2.54.0