authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-07-12 17:28:34-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-07-12 17:29:19-07:00
log3fb1a65292351a32ab3a14059337f9bc8f4d6f96
treea6b0d37be30e59c0669ae6a28c9669b8015ae0d4
parenteb97c459390bdb39eb6ecf071fd0932a8386edac
signaturebadge-check Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

add clover pitch and bugs for sequencer


38 files changed, 4470 insertions(+), 11832 deletions(-)

pitch/Sources/Pitch/AudioEngine.swift+41
...@@ -35,6 +35,9 @@ final class AudioEngine: ObservableObject, @unchecked Sendable {...@@ -35,6 +35,9 @@ final class AudioEngine: ObservableObject, @unchecked Sendable {
35 @Published var isRunning = false35 @Published var isRunning = false
36 @Published var permission: MicPermission = .unknown36 @Published var permission: MicPermission = .unknown
37 @Published var statusMessage: String?37 @Published var statusMessage: String?
38 /// Rolling key estimate (updated on the main thread as notes land). Low
39 /// frequency, so publishing it doesn't churn the SwiftUI tree per frame.
40 @Published var keyEstimate: KeyEstimate?
3841
39 private let engine = AVAudioEngine()42 private let engine = AVAudioEngine()
40 // Sensitive settings: low RMS gate so quiet singing registers, and a43 // Sensitive settings: low RMS gate so quiet singing registers, and a
...@@ -56,6 +59,11 @@ final class AudioEngine: ObservableObject, @unchecked Sendable {...@@ -56,6 +59,11 @@ final class AudioEngine: ObservableObject, @unchecked Sendable {
56 // Small median window to reject single-frame octave/spike errors.59 // Small median window to reject single-frame octave/spike errors.
57 private var recentMidi = [Double]()60 private var recentMidi = [Double]()
5861
62 // Note segmentation ("intentional note" layer) + its guard lock. Fed on the
63 // audio thread; read by the graph on the display thread.
64 private let segmenter = NoteSegmenter()
65 private let notesLock = NSLock()
66
59 // History shared with the UI thread.67 // History shared with the UI thread.
60 private let historyLock = NSLock()68 private let historyLock = NSLock()
61 private var history = [PitchSample]()69 private var history = [PitchSample]()
...@@ -179,6 +187,8 @@ final class AudioEngine: ObservableObject, @unchecked Sendable {...@@ -179,6 +187,8 @@ final class AudioEngine: ObservableObject, @unchecked Sendable {
179 recentMidi.removeAll(keepingCapacity: true)187 recentMidi.removeAll(keepingCapacity: true)
180 framesProcessed = 0188 framesProcessed = 0
181 haveStart = false189 haveStart = false
190 notesLock.lock(); segmenter.reset(); notesLock.unlock()
191 DispatchQueue.main.async { self.keyEstimate = nil }
182 }192 }
183193
184 // MARK: - Audio-thread processing194 // MARK: - Audio-thread processing
...@@ -210,6 +220,7 @@ final class AudioEngine: ObservableObject, @unchecked Sendable {...@@ -210,6 +220,7 @@ final class AudioEngine: ObservableObject, @unchecked Sendable {
210220
211 let sr = detector.sampleRate221 let sr = detector.sampleRate
212 var newSamples = [PitchSample]()222 var newSamples = [PitchSample]()
223 var noteCommitted = false
213224
214 while accumulator.count >= windowSize {225 while accumulator.count >= windowSize {
215 var result: YINDetector.Result?226 var result: YINDetector.Result?
...@@ -225,8 +236,14 @@ final class AudioEngine: ObservableObject, @unchecked Sendable {...@@ -225,8 +236,14 @@ final class AudioEngine: ObservableObject, @unchecked Sendable {
225 newSamples.append(PitchSample(time: windowStartTime, midi: midi, clarity: r.clarity))236 newSamples.append(PitchSample(time: windowStartTime, midi: midi, clarity: r.clarity))
226 setLive(LivePitch(frequency: freq, midi: midi, clarity: r.clarity, level: r.level),237 setLive(LivePitch(frequency: freq, midi: midi, clarity: r.clarity, level: r.level),
227 at: windowStartTime)238 at: windowStartTime)
239 notesLock.lock()
240 if segmenter.feed(time: windowStartTime, midi: midi) { noteCommitted = true }
241 notesLock.unlock()
228 } else {242 } else {
229 recentMidi.removeAll(keepingCapacity: true)243 recentMidi.removeAll(keepingCapacity: true)
244 notesLock.lock()
245 if segmenter.feedSilence(now: windowStartTime) { noteCommitted = true }
246 notesLock.unlock()
230 }247 }
231248
232 accumulator.removeFirst(hop)249 accumulator.removeFirst(hop)
...@@ -234,6 +251,30 @@ final class AudioEngine: ObservableObject, @unchecked Sendable {...@@ -234,6 +251,30 @@ final class AudioEngine: ObservableObject, @unchecked Sendable {
234 }251 }
235252
236 if !newSamples.isEmpty { appendHistory(newSamples) }253 if !newSamples.isEmpty { appendHistory(newSamples) }
254 if noteCommitted { updateKey() }
255 }
256
257 /// Recompute the key from committed notes (duration-weighted). Called only
258 /// when a note lands, so it's cheap; the result is published on the main
259 /// thread for the top-bar readout.
260 private func updateKey() {
261 notesLock.lock()
262 var weights = [Double](repeating: 0, count: 12)
263 for n in segmenter.committed {
264 weights[(((n.midi % 12) + 12) % 12)] += n.duration
265 }
266 notesLock.unlock()
267 let est = Music.estimateKey(weights: weights)
268 DispatchQueue.main.async { self.keyEstimate = est }
269 }
270
271 /// Snapshot of committed notes ending at/after `since`, plus the note being
272 /// sung right now (if any). Read by the graph each frame.
273 func noteSnapshot(since: Double) -> (committed: [NoteEvent], pending: NoteEvent?) {
274 notesLock.lock()
275 defer { notesLock.unlock() }
276 let committed = segmenter.committed.filter { $0.offset >= since }
277 return (committed, segmenter.pending)
237 }278 }
238279
239 /// Median-of-3 over consecutive detections — removes lone octave/spike280 /// Median-of-3 over consecutive detections — removes lone octave/spike
pitch/Sources/Pitch/Music.swift+58
...@@ -73,6 +73,64 @@ enum Music {...@@ -73,6 +73,64 @@ enum Music {
73 }73 }
74}74}
7575
76// MARK: - Key estimation (Krumhansl–Schmuckler)
77
78struct KeyEstimate {
79 let tonic: Int // pitch class 0–11 (concert)
80 let isMajor: Bool
81 let confidence: Double // 0…1, gap between the best and next-best fit
82}
83
84extension Music {
85 // Krumhansl–Kessler tonal-hierarchy profiles: how strongly each scale
86 // degree "belongs" in a major / minor key. Index 0 == the tonic.
87 static let majorProfile: [Double] =
88 [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88]
89 static let minorProfile: [Double] =
90 [6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17]
91
92 /// Estimate key from a 12-bin, duration-weighted pitch-class histogram.
93 /// Correlates the histogram against all 24 key profiles and returns the best
94 /// fit. Returns nil until there's enough distinct pitch material to be useful.
95 static func estimateKey(weights: [Double]) -> KeyEstimate? {
96 guard weights.count == 12 else { return nil }
97 guard weights.filter({ $0 > 0 }).count >= 3 else { return nil }
98
99 func correlate(_ x: [Double], _ p: [Double]) -> Double {
100 let mx = x.reduce(0, +) / 12, mp = p.reduce(0, +) / 12
101 var num = 0.0, dx = 0.0, dp = 0.0
102 for i in 0..<12 {
103 let a = x[i] - mx, b = p[i] - mp
104 num += a * b; dx += a * a; dp += b * b
105 }
106 let den = (dx * dp).squareRoot()
107 return den == 0 ? 0 : num / den
108 }
109
110 var best = (r: -2.0, tonic: 0, major: true)
111 var second = -2.0
112 for tonic in 0..<12 {
113 var rot = [Double](repeating: 0, count: 12)
114 for i in 0..<12 { rot[i] = weights[(i + tonic) % 12] }
115 for (r, isMaj) in [(correlate(rot, majorProfile), true),
116 (correlate(rot, minorProfile), false)] {
117 if r > best.r { second = best.r; best = (r, tonic, isMaj) }
118 else if r > second { second = r }
119 }
120 }
121 guard best.r > 0 else { return nil }
122 return KeyEstimate(tonic: best.tonic, isMajor: best.major,
123 confidence: max(0, min(1, best.r - max(0, second))))
124 }
125
126 /// Display name for a key, honoring the current naming + transpose, e.g.
127 /// "G major".
128 static func keyName(_ key: KeyEstimate, naming: NoteNaming, transpose: Int) -> String {
129 let pc = (((key.tonic + transpose) % 12) + 12) % 12
130 return "\(naming.names[pc]) \(key.isMajor ? "major" : "minor")"
131 }
132}
133
76// MARK: - Vocal / instrument ranges134// MARK: - Vocal / instrument ranges
77135
78struct PitchRange: Identifiable, Hashable {136struct PitchRange: Identifiable, Hashable {
pitch/Sources/Pitch/NoteSegmenter.swift created+120
...@@ -0,0 +1,120 @@
1import Foundation
2
3/// One committed "intentional" note — a stable pitch you actually meant to sing,
4/// with a start, an end, and how sharp/flat you held it.
5struct NoteEvent: Identifiable {
6 let id: Int
7 let midi: Int // integer MIDI (concert pitch, before transpose)
8 let onset: Double // seconds on the CACurrentMediaTime clock
9 var offset: Double // seconds
10 let meanCents: Double // signed average deviation from the integer, in cents
11 var duration: Double { offset - onset }
12}
13
14/// Turns a stream of fractional-MIDI pitch samples into discrete note events —
15/// the piano-roll layer that sits above the raw pitch contour.
16///
17/// This is a lightweight, real-time analogue of pYIN's note HMM:
18/// • a tolerance band around the current note absorbs vibrato and small drift,
19/// so a wobbling held note reads as ONE note instead of flickering;
20/// • when the pitch leaves that band the note is closed and a new one opens;
21/// • notes shorter than `minDuration` are discarded, so scoops and passing
22/// tones on the way to a target don't become spurious notes;
23/// • a silence gap longer than `gapToEnd` closes the held note.
24final class NoteSegmenter {
25 // Tunables. Cents are hundredths of a semitone; a semitone is 100 cents, so
26 // a note "owns" ±centsTolerance around its center — the overlap past the
27 // 50-cent midpoint is the hysteresis that keeps vibrato from splitting notes.
28 private let centsTolerance = 62.0
29 private let minDuration = 0.10 // seconds; below this a region is dropped
30 private let gapToEnd = 0.13 // seconds of silence that ends a held note
31 private let retain = 30.0 // seconds of committed notes kept
32
33 private(set) var committed: [NoteEvent] = []
34
35 // In-progress note.
36 private var pendingMidi: Int?
37 private var pendingOnset = 0.0
38 private var pendingCentsSum = 0.0
39 private var pendingCount = 0
40 private var lastVoiced = 0.0
41 private var nextID = 0
42
43 /// Feed one voiced sample. Returns true iff a note was committed (ended).
44 @discardableResult
45 func feed(time: Double, midi: Double) -> Bool {
46 var didCommit = false
47
48 // A silence gap since the previous voiced sample closes the held note.
49 if pendingMidi != nil, time - lastVoiced > gapToEnd {
50 didCommit = closePending(offset: lastVoiced)
51 }
52
53 if let cur = pendingMidi {
54 let distCents = abs(midi - Double(cur)) * 100
55 if distCents <= centsTolerance {
56 // Still the same note — absorb the sample (this is what swallows
57 // vibrato even as the nearest semitone flips back and forth).
58 pendingCentsSum += (midi - Double(cur)) * 100
59 pendingCount += 1
60 lastVoiced = time
61 } else {
62 // Pitch has moved off the note: close it, open a new one.
63 didCommit = closePending(offset: lastVoiced) || didCommit
64 openPending(time: time, midi: midi)
65 }
66 } else {
67 openPending(time: time, midi: midi)
68 }
69 return didCommit
70 }
71
72 /// Feed a detector "silence" tick so a held note eventually closes even if no
73 /// further voiced sample arrives. (Gaps are also caught lazily in `feed`.)
74 @discardableResult
75 func feedSilence(now: Double) -> Bool {
76 guard pendingMidi != nil, now - lastVoiced > gapToEnd else { return false }
77 return closePending(offset: lastVoiced)
78 }
79
80 /// The note currently being sung, as a provisional event for live rendering.
81 var pending: NoteEvent? {
82 guard let m = pendingMidi, pendingCount > 0 else { return nil }
83 // Only surface it once it's plausibly a note, not a passing scoop.
84 guard lastVoiced - pendingOnset >= minDuration * 0.5 else { return nil }
85 return NoteEvent(id: -1, midi: m, onset: pendingOnset, offset: lastVoiced,
86 meanCents: pendingCentsSum / Double(pendingCount))
87 }
88
89 func reset() {
90 committed.removeAll(keepingCapacity: true)
91 pendingMidi = nil
92 pendingCount = 0
93 }
94
95 // MARK: - Private
96
97 private func openPending(time: Double, midi: Double) {
98 let m = Int(midi.rounded())
99 pendingMidi = m
100 pendingOnset = time
101 pendingCentsSum = (midi - Double(m)) * 100
102 pendingCount = 1
103 lastVoiced = time
104 }
105
106 @discardableResult
107 private func closePending(offset: Double) -> Bool {
108 guard let m = pendingMidi else { return false }
109 pendingMidi = nil
110 guard offset - pendingOnset >= minDuration, pendingCount > 0 else { return false }
111 committed.append(NoteEvent(id: nextID, midi: m, onset: pendingOnset,
112 offset: offset, meanCents: pendingCentsSum / Double(pendingCount)))
113 nextID += 1
114 let cutoff = offset - retain
115 if let first = committed.first, first.offset < cutoff {
116 committed.removeAll { $0.offset < cutoff }
117 }
118 return true
119 }
120}
pitch/Sources/Pitch/PitchGraphView.swift+27
...@@ -78,6 +78,7 @@ private struct TraceCanvas: View {...@@ -78,6 +78,7 @@ private struct TraceCanvas: View {
78 let visible = settings.visibleSeconds78 let visible = settings.visibleSeconds
79 let live = engine.currentLive()79 let live = engine.currentLive()
8080
81 drawNotes(ctx: &ctx, layout: layout, now: now, visible: visible)
81 drawBand(ctx: &ctx, size: size, layout: layout, live: live)82 drawBand(ctx: &ctx, size: size, layout: layout, live: live)
82 drawTrace(ctx: &ctx, layout: layout, now: now, visible: visible)83 drawTrace(ctx: &ctx, layout: layout, now: now, visible: visible)
83 drawPill(ctx: &ctx, size: size, layout: layout, live: live)84 drawPill(ctx: &ctx, size: size, layout: layout, live: live)
...@@ -91,6 +92,32 @@ private struct TraceCanvas: View {...@@ -91,6 +92,32 @@ private struct TraceCanvas: View {
91 private static let clockOffset: Double =92 private static let clockOffset: Double =
92 CACurrentMediaTime() - Date().timeIntervalSinceReferenceDate93 CACurrentMediaTime() - Date().timeIntervalSinceReferenceDate
9394
95 // Piano-roll bars for the segmented "intentional" notes, drawn behind the
96 // live trace. Committed notes are solid; the note being sung right now is
97 // brighter and grows at the leading edge.
98 private func drawNotes(ctx: inout GraphicsContext, layout: PitchLayout,
99 now: Double, visible: Double) {
100 let (committed, pending) = engine.noteSnapshot(since: now - visible - 1)
101 // Bars are taller than the trace (4.2 px) so the note reads as a distinct
102 // block with the pitch line threading through it, not a sliver the trace
103 // hides. A crisp outline defines each note's edges.
104 let h = min(layout.rowHeight * 0.85, 26)
105
106 func bar(_ n: NoteEvent, fill: Color) {
107 let x0 = max(0, layout.x(n.onset, now: now, visible: visible))
108 let x1 = min(layout.plotWidth, layout.x(n.offset, now: now, visible: visible))
109 guard x1 > x0 else { return }
110 let yy = layout.y(Double(n.midi))
111 let rect = CGRect(x: x0, y: yy - h / 2, width: x1 - x0, height: h)
112 let path = Path(roundedRect: rect, cornerRadius: min(5, h / 2))
113 ctx.fill(path, with: .color(fill))
114 ctx.stroke(path, with: .color(palette.noteBarEdge), lineWidth: 1)
115 }
116
117 for n in committed { bar(n, fill: palette.noteBar) }
118 if let p = pending { bar(p, fill: palette.notePending) }
119 }
120
94 // Thin solid coral line marking the nearest note — a couple of pixels121 // Thin solid coral line marking the nearest note — a couple of pixels
95 // thicker than a staff line.122 // thicker than a staff line.
96 private func drawBand(ctx: inout GraphicsContext, size: CGSize,123 private func drawBand(ctx: inout GraphicsContext, size: CGSize,
pitch/Sources/Pitch/Theme.swift+11-2
...@@ -15,6 +15,9 @@ struct Palette {...@@ -15,6 +15,9 @@ struct Palette {
15 var pillText: Color15 var pillText: Color
16 var trace: Color16 var trace: Color
17 var dot: Color17 var dot: Color
18 var noteBar: Color // committed "intentional" note
19 var notePending: Color // the note being sung right now
20 var noteBarEdge: Color // crisp outline so bars read under the trace
1821
19 static func make(_ scheme: ColorScheme) -> Palette {22 static func make(_ scheme: ColorScheme) -> Palette {
20 scheme == .dark ? .dark : .light23 scheme == .dark ? .dark : .light
...@@ -31,7 +34,10 @@ struct Palette {...@@ -31,7 +34,10 @@ struct Palette {
31 pill: Color(red: 0.93, green: 0.49, blue: 0.45),34 pill: Color(red: 0.93, green: 0.49, blue: 0.45),
32 pillText: Color.white,35 pillText: Color.white,
33 trace: Color(red: 0.20, green: 0.24, blue: 0.31),36 trace: Color(red: 0.20, green: 0.24, blue: 0.31),
34 dot: Color(red: 0.20, green: 0.24, blue: 0.31))37 dot: Color(red: 0.20, green: 0.24, blue: 0.31),
38 noteBar: Color(red: 0.36, green: 0.52, blue: 0.90).opacity(0.28),
39 notePending: Color(red: 0.36, green: 0.52, blue: 0.90).opacity(0.44),
40 noteBarEdge: Color(red: 0.28, green: 0.44, blue: 0.85).opacity(0.65))
3541
36 static let dark = Palette(42 static let dark = Palette(
37 background: Color(red: 0.09, green: 0.10, blue: 0.12),43 background: Color(red: 0.09, green: 0.10, blue: 0.12),
...@@ -44,7 +50,10 @@ struct Palette {...@@ -44,7 +50,10 @@ struct Palette {
44 pill: Color(red: 0.90, green: 0.47, blue: 0.44),50 pill: Color(red: 0.90, green: 0.47, blue: 0.44),
45 pillText: Color.white,51 pillText: Color.white,
46 trace: Color(red: 0.93, green: 0.95, blue: 0.99),52 trace: Color(red: 0.93, green: 0.95, blue: 0.99),
47 dot: Color(red: 0.93, green: 0.95, blue: 0.99))53 dot: Color(red: 0.93, green: 0.95, blue: 0.99),
54 noteBar: Color(red: 0.55, green: 0.68, blue: 0.99).opacity(0.26),
55 notePending: Color(red: 0.55, green: 0.68, blue: 0.99).opacity(0.44),
56 noteBarEdge: Color(red: 0.62, green: 0.74, blue: 1.0).opacity(0.7))
48}57}
4958
50/// Shared plot geometry so the static grid layer and the animated trace layer59/// Shared plot geometry so the static grid layer and the animated trace layer
pitch/Sources/Pitch/TopBar.swift+39
...@@ -52,6 +52,7 @@ struct TopBar: View {...@@ -52,6 +52,7 @@ struct TopBar: View {
5252
53 Spacer(minLength: 8)53 Spacer(minLength: 8)
5454
55 KeyPill(engine: engine, settings: settings)
55 StatusPill(engine: engine)56 StatusPill(engine: engine)
56 }57 }
57 .padding(.horizontal, 16)58 .padding(.horizontal, 16)
...@@ -92,6 +93,44 @@ private struct Dropdown<Content: View>: View {...@@ -92,6 +93,44 @@ private struct Dropdown<Content: View>: View {
92 }93 }
93}94}
9495
96/// Live key estimate (Krumhansl–Schmuckler) shown in the top bar. Dims when the
97/// fit is weak so a shaky guess reads as tentative.
98private struct KeyPill: View {
99 @ObservedObject var engine: AudioEngine
100 @ObservedObject var settings: Settings
101
102 var body: some View {
103 HStack(spacing: 6) {
104 Image(systemName: "music.note")
105 .font(.system(size: 11, weight: .semibold))
106 .foregroundColor(.secondary)
107 Text(text)
108 .font(.system(size: 11, weight: .medium))
109 .foregroundColor(.secondary)
110 .lineLimit(1)
111 }
112 .opacity(opacity)
113 .padding(.horizontal, 10)
114 .padding(.vertical, 5)
115 .background(
116 RoundedRectangle(cornerRadius: 8)
117 .fill(Color(nsColor: .controlBackgroundColor))
118 .overlay(RoundedRectangle(cornerRadius: 8).stroke(Color(nsColor: .separatorColor), lineWidth: 1))
119 )
120 .help("Estimated key (Krumhansl–Schmuckler), from the notes you've sung")
121 }
122
123 private var text: String {
124 guard let k = engine.keyEstimate else { return "Key —" }
125 return "Key " + Music.keyName(k, naming: settings.naming, transpose: settings.transpose)
126 }
127
128 private var opacity: Double {
129 guard let k = engine.keyEstimate else { return 0.55 }
130 return 0.6 + 0.4 * min(1, k.confidence / 0.1)
131 }
132}
133
95private struct StatusPill: View {134private struct StatusPill: View {
96 @ObservedObject var engine: AudioEngine135 @ObservedObject var engine: AudioEngine
97136
recorder/engine/Sources/recorder/CaptureEngine.swift+21-3
...@@ -276,15 +276,21 @@ final class CaptureEngine {...@@ -276,15 +276,21 @@ final class CaptureEngine {
276 fallbackDir: safeRoot)276 fallbackDir: safeRoot)
277 }277 }
278278
279 private func makeAudioWriter(name: String, kind: String) throws -> StreamWriter {279 private func makeAudioWriter(name: String, kind: String, channels: Int = 2) throws -> StreamWriter {
280 // AAC in .m4a: ~256 kbps stereo, transparent for voice/desktop and tiny next280 // AAC in .m4a: ~256 kbps stereo, transparent for voice/desktop and tiny next
281 // to the uncompressed PCM we used to write. Audio is the irreplaceable281 // to the uncompressed PCM we used to write. Audio is the irreplaceable
282 // stream and costs ~115 MB/hour, so it records to the safe (internal) disk282 // stream and costs ~115 MB/hour, so it records to the safe (internal) disk
283 // rather than the removable scratch drive.283 // rather than the removable scratch drive.
284 //
285 // `channels` must match the source: system audio is genuinely stereo, but a
286 // mono mic forced into a 2-channel file lands entirely in channel 0 (left)
287 // and leaves the right dead silent — the file then plays only in the left
288 // ear on headphones. Writing a true mono file lets AVFoundation upmix it to
289 // both channels on playback.
284 let settings: [String: Any] = [290 let settings: [String: Any] = [
285 AVFormatIDKey: kAudioFormatMPEG4AAC,291 AVFormatIDKey: kAudioFormatMPEG4AAC,
286 AVSampleRateKey: 48_000,292 AVSampleRateKey: 48_000,
287 AVNumberOfChannelsKey: 2,293 AVNumberOfChannelsKey: channels,
288 AVEncoderBitRateKey: 256_000,294 AVEncoderBitRateKey: 256_000,
289 ]295 ]
290 return try StreamWriter(296 return try StreamWriter(
...@@ -293,6 +299,14 @@ final class CaptureEngine {...@@ -293,6 +299,14 @@ final class CaptureEngine {
293 fallbackDir: safeRoot)299 fallbackDir: safeRoot)
294 }300 }
295301
302 /// Channel count of an audio capture device's active format (1 for a typical
303 /// built-in or USB mic), clamped to at least 1. Used to size the mic writer.
304 private static func channelCount(of device: AVCaptureDevice) -> Int {
305 guard let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(
306 device.activeFormat.formatDescription) else { return 1 }
307 return max(1, Int(asbd.pointee.mChannelsPerFrame))
308 }
309
296 // MARK: AVCapture (mic + camera)310 // MARK: AVCapture (mic + camera)
297311
298 private func startAVCapture(audioUID: String? = nil, videoUID: String? = nil) throws {312 private func startAVCapture(audioUID: String? = nil, videoUID: String? = nil) throws {
...@@ -307,7 +321,11 @@ final class CaptureEngine {...@@ -307,7 +321,11 @@ final class CaptureEngine {
307 guard session.canAddInput(input) else { throw RecorderError("cannot add mic input") }321 guard session.canAddInput(input) else { throw RecorderError("cannot add mic input") }
308 session.addInput(input)322 session.addInput(input)
309323
310 let writer = try makeAudioWriter(name: "mic", kind: "mic")324 // Match the file's channel count to the device so a mono mic records as a
325 // true mono file (which plays in both ears) rather than a stereo file with
326 // a dead right channel. Falls back to mono if the format can't be read.
327 let micChannels = Self.channelCount(of: device)
328 let writer = try makeAudioWriter(name: "mic", kind: "mic", channels: micChannels)
311 writer.deviceUID = device.uniqueID329 writer.deviceUID = device.uniqueID
312 sinks.append(writer)330 sinks.append(writer)
313331
sequencer/CLAUDE.md+37-8
...@@ -5,11 +5,14 @@ Sequencer ("Clover Sequencer") is a native macOS app, not a video editor. It hel...@@ -5,11 +5,14 @@ Sequencer ("Clover Sequencer") is a native macOS app, not a video editor. It hel
5## Build & run5## Build & run
66
7```sh7```sh
8swift build # compile8swift build # compile (debug — for the CLI harnesses below)
9./run.sh # build, kill running instance, copy binary into Sequencer.app, relaunch9./run.sh # build RELEASE, kill running instance, copy into Sequencer.app, relaunch
10./run.sh --debug # same but ship the debug build (crash-chasing only)
10```11```
1112
12`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).13`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.
14
15Launching 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`.
1316
14There is no test target in Package.swift. Verification instead happens through two CLI-flag-driven harnesses baked into `main.swift`:17There is no test target in Package.swift. Verification instead happens through two CLI-flag-driven harnesses baked into `main.swift`:
1518
...@@ -17,11 +20,27 @@ There is no test target in Package.swift. Verification instead happens through t...@@ -17,11 +20,27 @@ There is no test target in Package.swift. Verification instead happens through t
17swift run Sequencer --selftest <mediafile> # headless pipeline check (see Selftest.swift)20swift run Sequencer --selftest <mediafile> # headless pipeline check (see Selftest.swift)
18swift run Sequencer --uitest # offscreen TimelineView harness (see UITest.swift)21swift run Sequencer --uitest # offscreen TimelineView harness (see UITest.swift)
19swift run Sequencer --perftest <file.sq> # offscreen draw-timing harness (see PerfTest.swift)22swift run Sequencer --perftest <file.sq> # offscreen draw-timing harness (see PerfTest.swift)
23swift run Sequencer --cachetest <file.sq> [capGB] # cache-budget invariant check (see Cachetest.swift)
20```24```
2125
22- `--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`.26- `--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`.
23- `--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.27- `--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.
24- `--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.28- `--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.
29- `--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).
30
31Cache/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 <cacheKey:origPath> […]` opens bare AVPlayerLayer tiles playing the real stitched composition(s) (`SEQ_LAYERTEST_T=<sec>` sets the seek) — the isolation tool that cracked the black-viewer bug.
32
33Two 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.
34
35Bundle 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.
36
37`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.
38
39Two 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.
40
41Microhangs/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.
42
43RAM: 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).
2544
26Requires `ffmpeg`/`ffprobe` on PATH for anything touching media (probing, filmstrips, proxies).45Requires `ffmpeg`/`ffprobe` on PATH for anything touching media (probing, filmstrips, proxies).
2746
...@@ -48,7 +67,7 @@ Non-undoable session/UI state (track hide/focus, pane heights, laneScale, snappi...@@ -48,7 +67,7 @@ Non-undoable session/UI state (track hide/focus, pane heights, laneScale, snappi
4867
49### Per-document architecture (`DocumentContext.swift`, `Document.swift`, `WindowController.swift`)68### Per-document architecture (`DocumentContext.swift`, `Document.swift`, `WindowController.swift`)
5069
51Each 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.70Each 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.
5271
53`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`).72`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`).
5473
...@@ -59,7 +78,7 @@ Each open project is a `ProjectDocument: NSDocument` owning a `DocumentContext`...@@ -59,7 +78,7 @@ Each open project is a `ProjectDocument: NSDocument` owning a `DocumentContext`
59This 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):78This 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):
6079
611. **`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.801. **`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.
622. **`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.812. **`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.
633. **`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.823. **`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.
6483
65When 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.84When 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...@@ -71,11 +90,21 @@ When working on playback bugs, the mental model is: `MediaPipeline` produces cac
7190
72### Storyboard (`Storyboard.swift`, `StoryboardEditor.swift`)91### Storyboard (`Storyboard.swift`, `StoryboardEditor.swift`)
7392
74Each `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.93Each `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.
7594
76### Views95### Views
7796
78- `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.97- `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.
98
99 **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:
100 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.
101 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.
102 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.
103 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.
104
105 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.
106
107 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.
79- `ViewerGridView.swift` — multicam grid, one cell per visible track plus a Fusion comps cell.108- `ViewerGridView.swift` — multicam grid, one cell per visible track plus a Fusion comps cell.
80- `TransportBar.swift`, `ExportDialog.swift`, `ColorPicker.swift`, `Theme.swift` (light/dark, follows system appearance, no manual toggle), `Tools.swift` (tool enum + radial quick-picker).109- `TransportBar.swift`, `ExportDialog.swift`, `ColorPicker.swift`, `Theme.swift` (light/dark, follows system appearance, no manual toggle), `Tools.swift` (tool enum + radial quick-picker).
81110
sequencer/Deltarune Ch5.sq/project.json deleted-11285
...@@ -1,11285 +0,0 @@
1{
2 "formatVersion" : 2,
3 "project" : {
4 "boardHeight" : 1080,
5 "boardWidth" : 1920,
6 "clips" : [
7 {
8 "duration" : 8.779661016949152,
9 "fadeIn" : 0,
10 "fadeOut" : 0,
11 "id" : "0856AA33-6A7E-4E96-BFB1-7B40DCC5C3E2",
12 "kind" : "video",
13 "linkId" : "E4929B8D-EE5B-4B9D-850D-B43F8A803E7A",
14 "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040",
15 "muted" : false,
16 "newShot" : false,
17 "speed" : 1,
18 "srcIn" : 0.8026453438086703,
19 "start" : 0,
20 "track" : "v0"
21 },
22 {
23 "duration" : 8.779661016949152,
24 "fadeIn" : 0,
25 "fadeOut" : 0,
26 "id" : "F03A433A-DD0D-43A4-B2E0-77976661B8D5",
27 "kind" : "audio",
28 "linkId" : "E4929B8D-EE5B-4B9D-850D-B43F8A803E7A",
29 "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB",
30 "muted" : true,
31 "newShot" : false,
32 "speed" : 1,
33 "srcIn" : 0.9509002194553415,
34 "start" : 0,
35 "track" : "v1"
36 },
37 {
38 "duration" : 8.779661016949152,
39 "fadeIn" : 0,
40 "fadeOut" : 0,
41 "id" : "DDC04C1A-94A8-4F4D-9568-73F07B3E0304",
42 "kind" : "audio",
43 "linkId" : "E4929B8D-EE5B-4B9D-850D-B43F8A803E7A",
44 "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD",
45 "muted" : false,
46 "newShot" : false,
47 "speed" : 1,
48 "srcIn" : 1.2881355932203389,
49 "start" : 0,
50 "track" : "v2"
51 },
52 {
53 "duration" : 8.779661016949152,
54 "fadeIn" : 0,
55 "fadeOut" : 0,
56 "id" : "856D7E40-8EF3-4242-AB37-B26CBFD4B3F7",
57 "kind" : "video",
58 "linkId" : "E4929B8D-EE5B-4B9D-850D-B43F8A803E7A",
59 "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49",
60 "muted" : false,
61 "newShot" : false,
62 "speed" : 1,
63 "srcIn" : 0.8603196774655755,
64 "start" : 0,
65 "track" : "v3"
66 },
67 {
68 "duration" : 8.779661016949152,
69 "fadeIn" : 0,
70 "fadeOut" : 0,
71 "id" : "ACBD774E-FC95-41F4-B5FB-037577828AD9",
72 "kind" : "video",
73 "linkId" : "E4929B8D-EE5B-4B9D-850D-B43F8A803E7A",
74 "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1",
75 "muted" : false,
76 "newShot" : false,
77 "speed" : 1,
78 "srcIn" : 0.8603078854665893,
79 "start" : 0,
80 "track" : "v4"
81 },
82 {
83 "duration" : 9.288135593220339,
84 "fadeIn" : 0,
85 "fadeOut" : 0,
86 "id" : "21E55168-059F-4CFC-AF14-FF43F7E7A9F8",
87 "kind" : "video",
88 "linkId" : "7ADB32D0-2D02-491A-8BF7-11F6A47A354E",
89 "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040",
90 "muted" : false,
91 "newShot" : false,
92 "speed" : 1,
93 "srcIn" : 11.582306360757823,
94 "start" : 8.779661016949152,
95 "track" : "v0"
96 },
97 {
98 "duration" : 9.288135593220339,
99 "fadeIn" : 0,
100 "fadeOut" : 0,
101 "id" : "FA932A03-4AEE-428C-9593-EB3CFA09BEBE",
102 "kind" : "audio",
103 "linkId" : "7ADB32D0-2D02-491A-8BF7-11F6A47A354E",
104 "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB",
105 "muted" : true,
106 "newShot" : false,
107 "speed" : 1,
108 "srcIn" : 11.730561236404494,
109 "start" : 8.779661016949152,
110 "track" : "v1"
111 },
112 {
113 "duration" : 9.288135593220339,
114 "fadeIn" : 0,
115 "fadeOut" : 0,
116 "id" : "BDB5ABE0-71D2-457D-A7D4-21ADCC3B5743",
117 "kind" : "audio",
118 "linkId" : "7ADB32D0-2D02-491A-8BF7-11F6A47A354E",
119 "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD",
120 "muted" : false,
121 "newShot" : false,
122 "speed" : 1,
123 "srcIn" : 12.067796610169491,
124 "start" : 8.779661016949152,
125 "track" : "v2"
126 },
127 {
128 "duration" : 9.288135593220339,
129 "fadeIn" : 0,
130 "fadeOut" : 0,
131 "id" : "2A859930-C3F8-4E02-BD27-4CF36CF2138E",
132 "kind" : "video",
133 "linkId" : "7ADB32D0-2D02-491A-8BF7-11F6A47A354E",
134 "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49",
135 "muted" : false,
136 "newShot" : false,
137 "speed" : 1,
138 "srcIn" : 11.639980694414728,
139 "start" : 8.779661016949152,
140 "track" : "v3"
141 },
142 {
143 "duration" : 9.288135593220339,
144 "fadeIn" : 0,
145 "fadeOut" : 0,
146 "id" : "197B76CD-FB1C-4DB5-BB64-B260CC9CC769",
147 "kind" : "video",
148 "linkId" : "7ADB32D0-2D02-491A-8BF7-11F6A47A354E",
149 "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1",
150 "muted" : false,
151 "newShot" : false,
152 "speed" : 1,
153 "srcIn" : 11.639968902415742,
154 "start" : 8.779661016949152,
155 "track" : "v4"
156 },
157 {
158 "duration" : 1.322033898305083,
159 "fadeIn" : 0,
160 "fadeOut" : 0,
161 "id" : "30FF8D20-D84E-4A9C-8303-F75C6D488959",
162 "kind" : "video",
163 "linkId" : "DE8705CE-9B40-487A-A252-50C469C56589",
164 "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040",
165 "muted" : false,
166 "newShot" : false,
167 "speed" : 1,
168 "srcIn" : 86.02298432685951,
169 "start" : 18.067796610169495,
170 "track" : "v0"
171 },
172 {
173 "duration" : 1.322033898305083,
174 "fadeIn" : 0,
175 "fadeOut" : 0,
176 "id" : "28F8C52E-77E0-4886-9A6F-49E8524EFDB0",
177 "kind" : "audio",
178 "linkId" : "DE8705CE-9B40-487A-A252-50C469C56589",
179 "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB",
180 "muted" : true,
181 "newShot" : false,
182 "speed" : 1,
183 "srcIn" : 86.17123920250619,
184 "start" : 18.067796610169495,
185 "track" : "v1"
186 },
187 {
188 "duration" : 1.322033898305083,
189 "fadeIn" : 0,
190 "fadeOut" : 0,
191 "id" : "385FB864-B457-4585-A1E2-B07F0B398682",
192 "kind" : "audio",
193 "linkId" : "DE8705CE-9B40-487A-A252-50C469C56589",
194 "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD",
195 "muted" : false,
196 "newShot" : false,
197 "speed" : 1,
198 "srcIn" : 86.50847457627118,
199 "start" : 18.067796610169495,
200 "track" : "v2"
201 },
202 {
203 "duration" : 1.322033898305083,
204 "fadeIn" : 0,
205 "fadeOut" : 0,
206 "id" : "4703FBC9-8E06-4DE3-A460-19695096A67B",
207 "kind" : "video",
208 "linkId" : "DE8705CE-9B40-487A-A252-50C469C56589",
209 "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49",
210 "muted" : false,
211 "newShot" : false,
212 "speed" : 1,
213 "srcIn" : 86.08065866051642,
214 "start" : 18.067796610169495,
215 "track" : "v3"
216 },
217 {
218 "duration" : 1.322033898305083,
219 "fadeIn" : 0,
220 "fadeOut" : 0,
221 "id" : "14297849-4D4F-41D8-A59A-15B5B1D60DF2",
222 "kind" : "video",
223 "linkId" : "DE8705CE-9B40-487A-A252-50C469C56589",
224 "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1",
225 "muted" : false,
226 "newShot" : false,
227 "speed" : 1,
228 "srcIn" : 86.08064686851743,
229 "start" : 18.067796610169495,
230 "track" : "v4"
231 },
232 {
233 "duration" : 1.1525423728813529,
234 "fadeIn" : 0,
235 "fadeOut" : 0,
236 "id" : "5EF423FA-AD82-4C89-8385-335325345015",
237 "kind" : "video",
238 "linkId" : "66157B6F-FF8D-490F-B510-26417942AA98",
239 "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040",
240 "muted" : false,
241 "newShot" : false,
242 "speed" : 1,
243 "srcIn" : 113.98908602177477,
244 "start" : 19.389830508474578,
245 "track" : "v0"
246 },
247 {
248 "duration" : 1.1525423728813529,
249 "fadeIn" : 0,
250 "fadeOut" : 0,
251 "id" : "95F0F7BD-FA8B-48FE-A975-859CBDE2F60A",
252 "kind" : "audio",
253 "linkId" : "66157B6F-FF8D-490F-B510-26417942AA98",
254 "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB",
255 "muted" : true,
256 "newShot" : false,
257 "speed" : 1,
258 "srcIn" : 114.13734089742144,
259 "start" : 19.389830508474578,
260 "track" : "v1"
261 },
262 {
263 "duration" : 1.1525423728813529,
264 "fadeIn" : 0,
265 "fadeOut" : 0,
266 "id" : "236F5693-66F0-4DF2-9C6B-0FC10DC039A0",
267 "kind" : "audio",
268 "linkId" : "66157B6F-FF8D-490F-B510-26417942AA98",
269 "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD",
270 "muted" : false,
271 "newShot" : false,
272 "speed" : 1,
273 "srcIn" : 114.47457627118644,
274 "start" : 19.389830508474578,
275 "track" : "v2"
276 },
277 {
278 "duration" : 1.1525423728813529,
279 "fadeIn" : 0,
280 "fadeOut" : 0,
281 "id" : "08DF237F-434A-4ADB-B77C-F165BA3B9BAE",
282 "kind" : "video",
283 "linkId" : "66157B6F-FF8D-490F-B510-26417942AA98",
284 "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49",
285 "muted" : false,
286 "newShot" : false,
287 "speed" : 1,
288 "srcIn" : 114.04676035543167,
289 "start" : 19.389830508474578,
290 "track" : "v3"
291 },
292 {
293 "duration" : 1.1525423728813529,
294 "fadeIn" : 0,
295 "fadeOut" : 0,
296 "id" : "6CEFB0C1-9FA6-4727-B291-AE95CBF63C1E",
297 "kind" : "video",
298 "linkId" : "66157B6F-FF8D-490F-B510-26417942AA98",
299 "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1",
300 "muted" : false,
301 "newShot" : false,
302 "speed" : 1,
303 "srcIn" : 114.04674856343269,
304 "start" : 19.389830508474578,
305 "track" : "v4"
306 },
307 {
308 "duration" : 5.220338983050851,
309 "fadeIn" : 0,
310 "fadeOut" : 0,
311 "id" : "CBC234E0-371D-4FDA-99B5-0BB7DE1737EB",
312 "kind" : "video",
313 "linkId" : "CEA52F0A-38E4-4F7B-AB96-117218501790",
314 "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040",
315 "muted" : false,
316 "newShot" : false,
317 "speed" : 1,
318 "srcIn" : 397.9212894116052,
319 "start" : 20.54237288135596,
320 "track" : "v0"
321 },
322 {
323 "duration" : 5.220338983050851,
324 "fadeIn" : 0,
325 "fadeOut" : 0,
326 "id" : "A4F89301-A2DE-4597-AD9E-DC51F3507355",
327 "kind" : "audio",
328 "linkId" : "CEA52F0A-38E4-4F7B-AB96-117218501790",
329 "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB",
330 "muted" : true,
331 "newShot" : false,
332 "speed" : 1,
333 "srcIn" : 398.06954428725186,
334 "start" : 20.54237288135596,
335 "track" : "v1"
336 },
337 {
338 "duration" : 5.220338983050851,
339 "fadeIn" : 0,
340 "fadeOut" : 0,
341 "id" : "F00A71B2-649B-4BF6-9C0A-80270331C353",
342 "kind" : "audio",
343 "linkId" : "CEA52F0A-38E4-4F7B-AB96-117218501790",
344 "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD",
345 "muted" : false,
346 "newShot" : false,
347 "speed" : 1,
348 "srcIn" : 398.40677966101686,
349 "start" : 20.54237288135596,
350 "track" : "v2"
351 },
352 {
353 "duration" : 5.220338983050851,
354 "fadeIn" : 0,
355 "fadeOut" : 0,
356 "id" : "31A4F820-D7C2-4CAB-8A20-F52F615C6114",
357 "kind" : "video",
358 "linkId" : "CEA52F0A-38E4-4F7B-AB96-117218501790",
359 "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49",
360 "muted" : false,
361 "newShot" : false,
362 "speed" : 1,
363 "srcIn" : 397.9789637452621,
364 "start" : 20.54237288135596,
365 "track" : "v3"
366 },
367 {
368 "duration" : 5.220338983050851,
369 "fadeIn" : 0,
370 "fadeOut" : 0,
371 "id" : "4B6EF408-0BAE-4495-885B-A0281AFD0B46",
372 "kind" : "video",
373 "linkId" : "CEA52F0A-38E4-4F7B-AB96-117218501790",
374 "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1",
375 "muted" : false,
376 "newShot" : false,
377 "speed" : 1,
378 "srcIn" : 397.9789519532631,
379 "start" : 20.54237288135596,
380 "track" : "v4"
381 },
382 {
383 "duration" : 14.57627118644065,
384 "fadeIn" : 0,
385 "fadeOut" : 0,
386 "id" : "B386A5EC-7B75-421C-941E-9A2E8FC4C303",
387 "kind" : "video",
388 "linkId" : "3688DE28-A2CB-4ED5-9666-9772A7FAF5FD",
389 "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040",
390 "muted" : false,
391 "newShot" : false,
392 "speed" : 1,
393 "srcIn" : 422.73484873363907,
394 "start" : 25.76271186440681,
395 "track" : "v0"
396 },
397 {
398 "duration" : 14.57627118644065,
399 "fadeIn" : 0,
400 "fadeOut" : 0,
401 "id" : "78D95643-9E92-4665-A49E-1640916F222F",
402 "kind" : "audio",
403 "linkId" : "3688DE28-A2CB-4ED5-9666-9772A7FAF5FD",
404 "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB",
405 "muted" : true,
406 "newShot" : false,
407 "speed" : 1,
408 "srcIn" : 422.88310360928574,
409 "start" : 25.76271186440681,
410 "track" : "v1"
411 },
412 {
413 "duration" : 14.57627118644065,
414 "fadeIn" : 0,
415 "fadeOut" : 0,
416 "id" : "3EC46637-04E5-407D-90BD-BB290FB532C6",
417 "kind" : "audio",
418 "linkId" : "3688DE28-A2CB-4ED5-9666-9772A7FAF5FD",
419 "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD",
420 "muted" : false,
421 "newShot" : false,
422 "speed" : 1,
423 "srcIn" : 423.22033898305074,
424 "start" : 25.76271186440681,
425 "track" : "v2"
426 },
427 {
428 "duration" : 14.57627118644065,
429 "fadeIn" : 0,
430 "fadeOut" : 0,
431 "id" : "54D04272-5607-4E88-BBAB-A50A9FEAA953",
432 "kind" : "video",
433 "linkId" : "3688DE28-A2CB-4ED5-9666-9772A7FAF5FD",
434 "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49",
435 "muted" : false,
436 "newShot" : false,
437 "speed" : 1,
438 "srcIn" : 422.792523067296,
439 "start" : 25.76271186440681,
440 "track" : "v3"
441 },
442 {
443 "duration" : 14.57627118644065,
444 "fadeIn" : 0,
445 "fadeOut" : 0,
446 "id" : "7B34B91A-3E53-4B21-A7C7-22172D7442EA",
447 "kind" : "video",
448 "linkId" : "3688DE28-A2CB-4ED5-9666-9772A7FAF5FD",
449 "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1",
450 "muted" : false,
451 "newShot" : false,
452 "speed" : 1,
453 "srcIn" : 422.792511275297,
454 "start" : 25.76271186440681,
455 "track" : "v4"
456 },
457 {
458 "duration" : 10.644067796610166,
459 "fadeIn" : 0,
460 "fadeOut" : 0,
461 "id" : "3EDF6E35-1C81-4706-878F-E685962438E7",
462 "kind" : "video",
463 "linkId" : "E6380C0A-8BCB-4E01-A3FF-4B6CE609EBA7",
464 "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040",
465 "muted" : false,
466 "newShot" : false,
467 "speed" : 1,
468 "srcIn" : 447.0399334794017,
469 "start" : 40.33898305084746,
470 "track" : "v0"
471 },
472 {
473 "duration" : 10.644067796610166,
474 "fadeIn" : 0,
475 "fadeOut" : 0,
476 "id" : "5E3BFA57-DA8E-47B2-969D-026F31AD2A3C",
477 "kind" : "audio",
478 "linkId" : "E6380C0A-8BCB-4E01-A3FF-4B6CE609EBA7",
479 "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB",
480 "muted" : true,
481 "newShot" : false,
482 "speed" : 1,
483 "srcIn" : 447.1881883550484,
484 "start" : 40.33898305084746,
485 "track" : "v1"
486 },
487 {
488 "duration" : 10.644067796610166,
489 "fadeIn" : 0,
490 "fadeOut" : 0,
491 "id" : "8E2AD73C-7F9E-4F46-8D04-EF9087EE690A",
492 "kind" : "audio",
493 "linkId" : "E6380C0A-8BCB-4E01-A3FF-4B6CE609EBA7",
494 "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD",
495 "muted" : false,
496 "newShot" : false,
497 "speed" : 1,
498 "srcIn" : 447.5254237288134,
499 "start" : 40.33898305084746,
500 "track" : "v2"
501 },
502 {
503 "duration" : 10.644067796610166,
504 "fadeIn" : 0,
505 "fadeOut" : 0,
506 "id" : "F54DE68B-6E96-4BA9-BEB6-81E4B984892C",
507 "kind" : "video",
508 "linkId" : "E6380C0A-8BCB-4E01-A3FF-4B6CE609EBA7",
509 "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49",
510 "muted" : false,
511 "newShot" : false,
512 "speed" : 1,
513 "srcIn" : 447.09760781305863,
514 "start" : 40.33898305084746,
515 "track" : "v3"
516 },
517 {
518 "duration" : 10.644067796610166,
519 "fadeIn" : 0,
520 "fadeOut" : 0,
521 "id" : "C77A294D-6278-4BAD-8BC5-F7BC3C372A00",
522 "kind" : "video",
523 "linkId" : "E6380C0A-8BCB-4E01-A3FF-4B6CE609EBA7",
524 "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1",
525 "muted" : false,
526 "newShot" : false,
527 "speed" : 1,
528 "srcIn" : 447.09759602105964,
529 "start" : 40.33898305084746,
530 "track" : "v4"
531 },
532 {
533 "duration" : 9.694915254237287,
534 "fadeIn" : 0,
535 "fadeOut" : 0,
536 "id" : "BA4DCFDD-E47E-4485-86C7-5D2F91DAD82A",
537 "kind" : "video",
538 "linkId" : "6CC9888B-E11F-41E5-BAC2-A15C7D015557",
539 "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040",
540 "muted" : false,
541 "newShot" : false,
542 "speed" : 1,
543 "srcIn" : 495.4128148353339,
544 "start" : 50.983050847457626,
545 "track" : "v0"
546 },
547 {
548 "duration" : 9.694915254237287,
549 "fadeIn" : 0,
550 "fadeOut" : 0,
551 "id" : "F7D2760F-4578-4319-9C36-40A5E0C0554A",
552 "kind" : "audio",
553 "linkId" : "6CC9888B-E11F-41E5-BAC2-A15C7D015557",
554 "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB",
555 "muted" : true,
556 "newShot" : false,
557 "speed" : 1,
558 "srcIn" : 495.5610697109806,
559 "start" : 50.983050847457626,
560 "track" : "v1"
561 },
562 {
563 "duration" : 9.694915254237287,
564 "fadeIn" : 0,
565 "fadeOut" : 0,
566 "id" : "344797CD-1399-4A2F-B5D4-ED0DAFF921B2",
567 "kind" : "audio",
568 "linkId" : "6CC9888B-E11F-41E5-BAC2-A15C7D015557",
569 "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD",
570 "muted" : false,
571 "newShot" : false,
572 "speed" : 1,
573 "srcIn" : 495.8983050847456,
574 "start" : 50.983050847457626,
575 "track" : "v2"
576 },
577 {
578 "duration" : 9.694915254237287,
579 "fadeIn" : 0,
580 "fadeOut" : 0,
581 "id" : "436EE031-3821-460A-B1E6-362C6AC7DD15",
582 "kind" : "video",
583 "linkId" : "6CC9888B-E11F-41E5-BAC2-A15C7D015557",
584 "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49",
585 "muted" : false,
586 "newShot" : false,
587 "speed" : 1,
588 "srcIn" : 495.4704891689908,
589 "start" : 50.983050847457626,
590 "track" : "v3"
591 },
592 {
593 "duration" : 9.694915254237287,
594 "fadeIn" : 0,
595 "fadeOut" : 0,
596 "id" : "DA7C94D5-26B6-465A-958E-DA029F1CEC3F",
597 "kind" : "video",
598 "linkId" : "6CC9888B-E11F-41E5-BAC2-A15C7D015557",
599 "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1",
600 "muted" : false,
601 "newShot" : false,
602 "speed" : 1,
603 "srcIn" : 495.4704773769918,
604 "start" : 50.983050847457626,
605 "track" : "v4"
606 },
607 {
608 "duration" : 12.440677966101696,
609 "fadeIn" : 0,
610 "fadeOut" : 0,
611 "id" : "EF8B18B2-7BEB-4520-9C17-903C1A223C06",
612 "kind" : "video",
613 "linkId" : "8D7F9D29-68E0-4E62-B7D7-B149575B3269",
614 "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040",
615 "muted" : false,
616 "newShot" : false,
617 "speed" : 1,
618 "srcIn" : 511.0399334794017,
619 "start" : 60.67796610169491,
620 "track" : "v0"
621 },
622 {
623 "duration" : 12.440677966101696,
624 "fadeIn" : 0,
625 "fadeOut" : 0,
626 "id" : "65C8245D-4AA2-4534-BDAF-3D2841DC3F70",
627 "kind" : "audio",
628 "linkId" : "8D7F9D29-68E0-4E62-B7D7-B149575B3269",
629 "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB",
630 "muted" : true,
631 "newShot" : false,
632 "speed" : 1,
633 "srcIn" : 511.1881883550484,
634 "start" : 60.67796610169491,
635 "track" : "v1"
636 },
637 {
638 "duration" : 12.440677966101696,
639 "fadeIn" : 0,
640 "fadeOut" : 0,
641 "id" : "8ECF7044-2C74-43E6-9219-036DB453AA8E",
642 "kind" : "audio",
643 "linkId" : "8D7F9D29-68E0-4E62-B7D7-B149575B3269",
644 "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD",
645 "muted" : false,
646 "newShot" : false,
647 "speed" : 1,
648 "srcIn" : 511.5254237288134,
649 "start" : 60.67796610169491,
650 "track" : "v2"
651 },
652 {
653 "duration" : 12.440677966101696,
654 "fadeIn" : 0,
655 "fadeOut" : 0,
656 "id" : "E0691830-C2E3-4692-A443-C96E79034416",
657 "kind" : "video",
658 "linkId" : "8D7F9D29-68E0-4E62-B7D7-B149575B3269",
659 "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49",
660 "muted" : false,
661 "newShot" : false,
662 "speed" : 1,
663 "srcIn" : 511.09760781305863,
664 "start" : 60.67796610169491,
665 "track" : "v3"
666 },
667 {
668 "duration" : 12.440677966101696,
669 "fadeIn" : 0,
670 "fadeOut" : 0,
671 "id" : "1DF28AA1-85FC-4AA6-8BCC-86CF38B06C1B",
672 "kind" : "video",
673 "linkId" : "8D7F9D29-68E0-4E62-B7D7-B149575B3269",
674 "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1",
675 "muted" : false,
676 "newShot" : false,
677 "speed" : 1,
678 "srcIn" : 511.09759602105964,
679 "start" : 60.67796610169491,
680 "track" : "v4"
681 },
682 {
683 "duration" : 2.0677966101694807,
684 "fadeIn" : 0,
685 "fadeOut" : 0,
686 "id" : "4937BF52-96D7-4AF0-B222-22E27B797A5C",
687 "kind" : "video",
688 "linkId" : "E5D93442-8B82-4040-8E5F-BD717FD75859",
689 "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040",
690 "muted" : false,
691 "newShot" : false,
692 "speed" : 1,
693 "srcIn" : 539.9212894116051,
694 "start" : 73.11864406779661,
695 "track" : "v0"
696 },
697 {
698 "duration" : 2.0677966101694807,
699 "fadeIn" : 0,
700 "fadeOut" : 0,
701 "id" : "16FFB49E-AD0E-430F-BC1E-0E649D37B469",
702 "kind" : "audio",
703 "linkId" : "E5D93442-8B82-4040-8E5F-BD717FD75859",
704 "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB",
705 "muted" : true,
706 "newShot" : false,
707 "speed" : 1,
708 "srcIn" : 540.0695442872518,
709 "start" : 73.11864406779661,
710 "track" : "v1"
711 },
712 {
713 "duration" : 2.0677966101694807,
714 "fadeIn" : 0,
715 "fadeOut" : 0,
716 "id" : "3772DE62-3FC2-491B-9DBC-922F6DDC5BB1",
717 "kind" : "audio",
718 "linkId" : "E5D93442-8B82-4040-8E5F-BD717FD75859",
719 "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD",
720 "muted" : false,
721 "newShot" : false,
722 "speed" : 1,
723 "srcIn" : 540.4067796610168,
724 "start" : 73.11864406779661,
725 "track" : "v2"
726 },
727 {
728 "duration" : 2.0677966101694807,
729 "fadeIn" : 0,
730 "fadeOut" : 0,
731 "id" : "4A62F7EC-EFF6-4CE8-82FA-024FC1AE5421",
732 "kind" : "video",
733 "linkId" : "E5D93442-8B82-4040-8E5F-BD717FD75859",
734 "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49",
735 "muted" : false,
736 "newShot" : false,
737 "speed" : 1,
738 "srcIn" : 539.978963745262,
739 "start" : 73.11864406779661,
740 "track" : "v3"
741 },
742 {
743 "duration" : 2.0677966101694807,
744 "fadeIn" : 0,
745 "fadeOut" : 0,
746 "id" : "7E5889B3-52E4-48AD-8E76-DCAC57E2F26C",
747 "kind" : "video",
748 "linkId" : "E5D93442-8B82-4040-8E5F-BD717FD75859",
749 "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1",
750 "muted" : false,
751 "newShot" : false,
752 "speed" : 1,
753 "srcIn" : 539.978951953263,
754 "start" : 73.11864406779661,
755 "track" : "v4"
756 },
757 {
758 "duration" : 4.542372881355931,
759 "fadeIn" : 0,
760 "fadeOut" : 0,
761 "id" : "779C851A-2F3C-4B84-8EDB-99C3D657B311",
762 "kind" : "video",
763 "linkId" : "DCA0DA82-D677-4F94-8ECD-EC2EE408A444",
764 "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040",
765 "muted" : false,
766 "newShot" : false,
767 "speed" : 1,
768 "srcIn" : 552.0229843268594,
769 "start" : 75.18644067796609,
770 "track" : "v0"
771 },
772 {
773 "duration" : 4.542372881355931,
774 "fadeIn" : 0,
775 "fadeOut" : 0,
776 "id" : "6897606B-DA0A-4825-B6DB-325E170FC9B9",
777 "kind" : "audio",
778 "linkId" : "DCA0DA82-D677-4F94-8ECD-EC2EE408A444",
779 "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB",
780 "muted" : true,
781 "newShot" : false,
782 "speed" : 1,
783 "srcIn" : 552.1712392025061,
784 "start" : 75.18644067796609,
785 "track" : "v1"
786 },
787 {
788 "duration" : 4.542372881355931,
789 "fadeIn" : 0,
790 "fadeOut" : 0,
791 "id" : "07934764-AD42-4089-80B6-B244015447CF",
792 "kind" : "audio",
793 "linkId" : "DCA0DA82-D677-4F94-8ECD-EC2EE408A444",
794 "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD",
795 "muted" : false,
796 "newShot" : false,
797 "speed" : 1,
798 "srcIn" : 552.508474576271,
799 "start" : 75.18644067796609,
800 "track" : "v2"
801 },
802 {
803 "duration" : 4.542372881355931,
804 "fadeIn" : 0,
805 "fadeOut" : 0,
806 "id" : "EE2670C2-1AAA-40FC-A411-8CE33185992E",
807 "kind" : "video",
808 "linkId" : "DCA0DA82-D677-4F94-8ECD-EC2EE408A444",
809 "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49",
810 "muted" : false,
811 "newShot" : false,
812 "speed" : 1,
813 "srcIn" : 552.0806586605163,
814 "start" : 75.18644067796609,
815 "track" : "v3"
816 },
817 {
818 "duration" : 4.542372881355931,
819 "fadeIn" : 0,
820 "fadeOut" : 0,
821 "id" : "83DC8C97-0117-495D-AB6F-488575028DF6",
822 "kind" : "video",
823 "linkId" : "DCA0DA82-D677-4F94-8ECD-EC2EE408A444",
824 "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1",
825 "muted" : false,
826 "newShot" : false,
827 "speed" : 1,
828 "srcIn" : 552.0806468685173,
829 "start" : 75.18644067796609,
830 "track" : "v4"
831 },
832 {
833 "duration" : 2.542372881355945,
834 "fadeIn" : 0,
835 "fadeOut" : 0,
836 "id" : "75DD8465-3E74-4576-A287-6F0B7A25885F",
837 "kind" : "video",
838 "linkId" : "B3514666-D79A-43F4-96AF-77F14857D9AE",
839 "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040",
840 "muted" : false,
841 "newShot" : false,
842 "speed" : 1,
843 "srcIn" : 565.1077300895713,
844 "start" : 79.76271186440677,
845 "track" : "v0"
846 },
847 {
848 "duration" : 2.542372881355945,
849 "fadeIn" : 0,
850 "fadeOut" : 0,
851 "id" : "B5BC5C0D-5C8F-4616-954E-F0544734C4CC",
852 "kind" : "audio",
853 "linkId" : "B3514666-D79A-43F4-96AF-77F14857D9AE",
854 "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB",
855 "muted" : true,
856 "newShot" : false,
857 "speed" : 1,
858 "srcIn" : 565.255984965218,
859 "start" : 79.76271186440677,
860 "track" : "v1"
861 },
862 {
863 "duration" : 2.542372881355945,
864 "fadeIn" : 0,
865 "fadeOut" : 0,
866 "id" : "743266F1-8759-428D-8500-328A33B14EE7",
867 "kind" : "audio",
868 "linkId" : "B3514666-D79A-43F4-96AF-77F14857D9AE",
869 "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD",
870 "muted" : false,
871 "newShot" : false,
872 "speed" : 1,
873 "srcIn" : 565.593220338983,
874 "start" : 79.76271186440677,
875 "track" : "v2"
876 },
877 {
878 "duration" : 2.542372881355945,
879 "fadeIn" : 0,
880 "fadeOut" : 0,
881 "id" : "99C8EB5C-633C-407A-B29D-0F563C4A2A11",
882 "kind" : "video",
883 "linkId" : "B3514666-D79A-43F4-96AF-77F14857D9AE",
884 "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49",
885 "muted" : false,
886 "newShot" : false,
887 "speed" : 1,
888 "srcIn" : 565.1654044232282,
889 "start" : 79.76271186440677,
890 "track" : "v3"
891 },
892 {
893 "duration" : 2.542372881355945,
894 "fadeIn" : 0,
895 "fadeOut" : 0,
896 "id" : "F5A5A67A-1C13-4920-94A3-D63379305184",
897 "kind" : "video",
898 "linkId" : "B3514666-D79A-43F4-96AF-77F14857D9AE",
899 "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1",
900 "muted" : false,
901 "newShot" : false,
902 "speed" : 1,
903 "srcIn" : 565.1653926312292,
904 "start" : 79.76271186440677,
905 "track" : "v4"
906 },
907 {
908 "duration" : 7.457627118644069,
909 "fadeIn" : 0,
910 "fadeOut" : 0,
911 "id" : "C4F9A637-30B0-4050-B75E-6C87C85699A8",
912 "kind" : "video",
913 "linkId" : "E6C60A60-B509-4E76-A1F1-797378C6C7EF",
914 "mediaId" : "26CBEF77-6850-4D82-A5BD-A6A37587C040",
915 "muted" : false,
916 "newShot" : false,
917 "speed" : 1,
918 "srcIn" : 792.0229843268595,
919 "start" : 82.30508474576271,
920 "track" : "v0"
921 },
922 {
923 "duration" : 7.457627118644069,
924 "fadeIn" : 0,
925 "fadeOut" : 0,
926 "id" : "D0EF66D9-DF73-4099-BAA1-CD7E3DC03441",
927 "kind" : "audio",
928 "linkId" : "E6C60A60-B509-4E76-A1F1-797378C6C7EF",
929 "mediaId" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB",
930 "muted" : true,
931 "newShot" : false,
932 "speed" : 1,
933 "srcIn" : 792.1712392025062,
934 "start" : 82.30508474576271,
935 "track" : "v1"
936 },
937 {
938 "duration" : 7.457627118644069,
939 "fadeIn" : 0,
940 "fadeOut" : 0,
941 "id" : "FACBA48B-CB80-4E8C-AAF6-55705F37FDB8",
942 "kind" : "audio",
943 "linkId" : "E6C60A60-B509-4E76-A1F1-797378C6C7EF",
944 "mediaId" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD",
945 "muted" : false,
946 "newShot" : false,
947 "speed" : 1,
948 "srcIn" : 792.5084745762712,
949 "start" : 82.30508474576271,
950 "track" : "v2"
951 },
952 {
953 "duration" : 7.457627118644069,
954 "fadeIn" : 0,
955 "fadeOut" : 0,
956 "id" : "24F56A9C-9D34-412D-AAFA-816D98E1BF4C",
957 "kind" : "video",
958 "linkId" : "E6C60A60-B509-4E76-A1F1-797378C6C7EF",
959 "mediaId" : "609E978E-5994-4757-A6C7-EAAE72818A49",
960 "muted" : false,
961 "newShot" : false,
962 "speed" : 1,
963 "srcIn" : 792.0806586605164,
964 "start" : 82.30508474576271,
965 "track" : "v3"
966 },
967 {
968 "duration" : 7.457627118644069,
969 "fadeIn" : 0,
970 "fadeOut" : 0,
971 "id" : "E8BDB4FD-1478-46C9-B2B9-4A995785C804",
972 "kind" : "video",
973 "linkId" : "E6C60A60-B509-4E76-A1F1-797378C6C7EF",
974 "mediaId" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1",
975 "muted" : false,
976 "newShot" : false,
977 "speed" : 1,
978 "srcIn" : 792.0806468685174,
979 "start" : 82.30508474576271,
980 "track" : "v4"
981 },
982 {
983 "duration" : 5.7966101694915295,
984 "fadeIn" : 0,
985 "fadeOut" : 0,
986 "id" : "9F719822-EE9F-4D98-AE6F-95832502501D",
987 "kind" : "video",
988 "linkId" : "33DE57F6-49E9-47DA-971D-A7E3824D9B47",
989 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
990 "muted" : false,
991 "newShot" : false,
992 "speed" : 1,
993 "srcIn" : 6.203389830508485,
994 "start" : 89.76271186440678,
995 "track" : "v0"
996 },
997 {
998 "duration" : 5.7966101694915295,
999 "fadeIn" : 0,
1000 "fadeOut" : 0,
1001 "id" : "4D903E13-8AAE-4EED-9E78-EB2D6201EE84",
1002 "kind" : "audio",
1003 "linkId" : "33DE57F6-49E9-47DA-971D-A7E3824D9B47",
1004 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
1005 "muted" : true,
1006 "newShot" : false,
1007 "speed" : 1,
1008 "srcIn" : 6.1195182891674165,
1009 "start" : 89.76271186440678,
1010 "track" : "v1"
1011 },
1012 {
1013 "duration" : 5.7966101694915295,
1014 "fadeIn" : 0,
1015 "fadeOut" : 0,
1016 "id" : "0A6A0000-76C4-4F4F-A5EC-B9DB5AA10EE5",
1017 "kind" : "audio",
1018 "linkId" : "33DE57F6-49E9-47DA-971D-A7E3824D9B47",
1019 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
1020 "muted" : false,
1021 "newShot" : false,
1022 "speed" : 1,
1023 "srcIn" : 6.166194592398341,
1024 "start" : 89.76271186440678,
1025 "track" : "v2"
1026 },
1027 {
1028 "duration" : 5.7966101694915295,
1029 "fadeIn" : 0,
1030 "fadeOut" : 0,
1031 "id" : "91FC4279-5D23-44A0-BA93-5BC8CFB0775C",
1032 "kind" : "video",
1033 "linkId" : "33DE57F6-49E9-47DA-971D-A7E3824D9B47",
1034 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
1035 "muted" : false,
1036 "newShot" : false,
1037 "speed" : 1,
1038 "srcIn" : 6.029587456170219,
1039 "start" : 89.76271186440678,
1040 "track" : "v3"
1041 },
1042 {
1043 "duration" : 5.7966101694915295,
1044 "fadeIn" : 0,
1045 "fadeOut" : 0,
1046 "id" : "51B251E0-3404-4CAC-B18A-37A29B9BF96B",
1047 "kind" : "video",
1048 "linkId" : "33DE57F6-49E9-47DA-971D-A7E3824D9B47",
1049 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
1050 "muted" : false,
1051 "newShot" : false,
1052 "speed" : 1,
1053 "srcIn" : 6.029582872171403,
1054 "start" : 89.76271186440678,
1055 "track" : "v4"
1056 },
1057 {
1058 "duration" : 18.847457627118715,
1059 "fadeIn" : 0,
1060 "fadeOut" : 0,
1061 "id" : "E8DC76EC-C4D2-46AE-853A-A60F4F0D2E07",
1062 "kind" : "video",
1063 "linkId" : "C5B94050-9128-423F-8A3C-F610A15ECBE8",
1064 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
1065 "muted" : false,
1066 "newShot" : false,
1067 "speed" : 1,
1068 "srcIn" : 153.08474576271186,
1069 "start" : 116.81355932203384,
1070 "track" : "v0"
1071 },
1072 {
1073 "duration" : 18.847457627118715,
1074 "fadeIn" : 0,
1075 "fadeOut" : 0,
1076 "id" : "4440B25F-48FC-49CC-9A78-207D16DDCCB7",
1077 "kind" : "audio",
1078 "linkId" : "C5B94050-9128-423F-8A3C-F610A15ECBE8",
1079 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
1080 "muted" : true,
1081 "newShot" : false,
1082 "speed" : 1,
1083 "srcIn" : 153.0008742213708,
1084 "start" : 116.81355932203384,
1085 "track" : "v1"
1086 },
1087 {
1088 "duration" : 18.847457627118715,
1089 "fadeIn" : 0,
1090 "fadeOut" : 0,
1091 "id" : "6FEFD05B-C857-4EBE-9EE7-F0EAAA08569F",
1092 "kind" : "audio",
1093 "linkId" : "C5B94050-9128-423F-8A3C-F610A15ECBE8",
1094 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
1095 "muted" : false,
1096 "newShot" : false,
1097 "speed" : 1,
1098 "srcIn" : 153.04755052460172,
1099 "start" : 116.81355932203384,
1100 "track" : "v2"
1101 },
1102 {
1103 "duration" : 18.847457627118715,
1104 "fadeIn" : 0,
1105 "fadeOut" : 0,
1106 "id" : "E1C8762B-5477-4769-BD47-2DE3F3E7F08A",
1107 "kind" : "video",
1108 "linkId" : "C5B94050-9128-423F-8A3C-F610A15ECBE8",
1109 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
1110 "muted" : false,
1111 "newShot" : false,
1112 "speed" : 1,
1113 "srcIn" : 152.9109433883736,
1114 "start" : 116.81355932203384,
1115 "track" : "v3"
1116 },
1117 {
1118 "duration" : 18.847457627118715,
1119 "fadeIn" : 0,
1120 "fadeOut" : 0,
1121 "id" : "6C90A7B0-6891-4C26-9ACE-E78062706047",
1122 "kind" : "video",
1123 "linkId" : "C5B94050-9128-423F-8A3C-F610A15ECBE8",
1124 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
1125 "muted" : false,
1126 "newShot" : false,
1127 "speed" : 1,
1128 "srcIn" : 152.91093880437478,
1129 "start" : 116.81355932203384,
1130 "track" : "v4"
1131 },
1132 {
1133 "duration" : 13.220338983050851,
1134 "fadeIn" : 0,
1135 "fadeOut" : 0,
1136 "id" : "BE66E066-F241-46FE-8D9A-606DBA9554B7",
1137 "kind" : "video",
1138 "linkId" : "17FC9151-B113-420C-8754-353E10218637",
1139 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
1140 "muted" : false,
1141 "newShot" : false,
1142 "speed" : 1,
1143 "srcIn" : 81.01694915254235,
1144 "start" : 103.59322033898304,
1145 "track" : "v0"
1146 },
1147 {
1148 "duration" : 13.220338983050851,
1149 "fadeIn" : 0,
1150 "fadeOut" : 0,
1151 "id" : "D10CE445-F3A3-46E4-B38D-B77AAB60F5FB",
1152 "kind" : "audio",
1153 "linkId" : "17FC9151-B113-420C-8754-353E10218637",
1154 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
1155 "muted" : true,
1156 "newShot" : false,
1157 "speed" : 1,
1158 "srcIn" : 80.93307761120128,
1159 "start" : 103.59322033898304,
1160 "track" : "v1"
1161 },
1162 {
1163 "duration" : 13.220338983050851,
1164 "fadeIn" : 0,
1165 "fadeOut" : 0,
1166 "id" : "4F4DB6E2-2CC1-4696-8CE6-9B7119E8F034",
1167 "kind" : "audio",
1168 "linkId" : "17FC9151-B113-420C-8754-353E10218637",
1169 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
1170 "muted" : false,
1171 "newShot" : false,
1172 "speed" : 1,
1173 "srcIn" : 80.97975391443221,
1174 "start" : 103.59322033898304,
1175 "track" : "v2"
1176 },
1177 {
1178 "duration" : 13.220338983050851,
1179 "fadeIn" : 0,
1180 "fadeOut" : 0,
1181 "id" : "27A9D3E7-68B7-40B3-B332-244F8C0C1BF6",
1182 "kind" : "video",
1183 "linkId" : "17FC9151-B113-420C-8754-353E10218637",
1184 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
1185 "muted" : false,
1186 "newShot" : false,
1187 "speed" : 1,
1188 "srcIn" : 80.84314677820409,
1189 "start" : 103.59322033898304,
1190 "track" : "v3"
1191 },
1192 {
1193 "duration" : 13.220338983050851,
1194 "fadeIn" : 0,
1195 "fadeOut" : 0,
1196 "id" : "5DB078BA-6259-4E90-9C12-E9642D920CED",
1197 "kind" : "video",
1198 "linkId" : "17FC9151-B113-420C-8754-353E10218637",
1199 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
1200 "muted" : false,
1201 "newShot" : false,
1202 "speed" : 1,
1203 "srcIn" : 80.84314219420527,
1204 "start" : 103.59322033898304,
1205 "track" : "v4"
1206 },
1207 {
1208 "duration" : 2.6779661016949063,
1209 "fadeIn" : 0,
1210 "fadeOut" : 0,
1211 "id" : "5FE10E54-0DF5-4AE4-BD39-77E27BADC0B3",
1212 "kind" : "video",
1213 "linkId" : "557C472E-4AC3-4428-8BD4-7817D0701CAB",
1214 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
1215 "muted" : false,
1216 "newShot" : false,
1217 "speed" : 1,
1218 "srcIn" : 22.40677966101694,
1219 "start" : 95.55932203389831,
1220 "track" : "v0"
1221 },
1222 {
1223 "duration" : 2.6779661016949063,
1224 "fadeIn" : 0,
1225 "fadeOut" : 0,
1226 "id" : "981061A0-98AF-4B09-9FEF-6DE942F56A18",
1227 "kind" : "audio",
1228 "linkId" : "557C472E-4AC3-4428-8BD4-7817D0701CAB",
1229 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
1230 "muted" : true,
1231 "newShot" : false,
1232 "speed" : 1,
1233 "srcIn" : 22.322908119675873,
1234 "start" : 95.55932203389831,
1235 "track" : "v1"
1236 },
1237 {
1238 "duration" : 2.6779661016949063,
1239 "fadeIn" : 0,
1240 "fadeOut" : 0,
1241 "id" : "AD29D0F7-2863-42EA-BF7F-AB91267B34D1",
1242 "kind" : "audio",
1243 "linkId" : "557C472E-4AC3-4428-8BD4-7817D0701CAB",
1244 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
1245 "muted" : false,
1246 "newShot" : false,
1247 "speed" : 1,
1248 "srcIn" : 22.369584422906797,
1249 "start" : 95.55932203389831,
1250 "track" : "v2"
1251 },
1252 {
1253 "duration" : 2.6779661016949063,
1254 "fadeIn" : 0,
1255 "fadeOut" : 0,
1256 "id" : "665388F4-40AF-450A-9C85-EE082E1EE31F",
1257 "kind" : "video",
1258 "linkId" : "557C472E-4AC3-4428-8BD4-7817D0701CAB",
1259 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
1260 "muted" : false,
1261 "newShot" : false,
1262 "speed" : 1,
1263 "srcIn" : 22.232977286678675,
1264 "start" : 95.55932203389831,
1265 "track" : "v3"
1266 },
1267 {
1268 "duration" : 2.6779661016949063,
1269 "fadeIn" : 0,
1270 "fadeOut" : 0,
1271 "id" : "3A784722-971F-44D4-B835-69C262B69DC7",
1272 "kind" : "video",
1273 "linkId" : "557C472E-4AC3-4428-8BD4-7817D0701CAB",
1274 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
1275 "muted" : false,
1276 "newShot" : false,
1277 "speed" : 1,
1278 "srcIn" : 22.23297270267986,
1279 "start" : 95.55932203389831,
1280 "track" : "v4"
1281 },
1282 {
1283 "duration" : 5.355932203389827,
1284 "fadeIn" : 0,
1285 "fadeOut" : 0,
1286 "id" : "3101D12D-3956-4472-A689-AC25FAF6B2B3",
1287 "kind" : "video",
1288 "linkId" : "DD4C0E3A-C905-48F3-BA08-017781831163",
1289 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
1290 "muted" : false,
1291 "newShot" : false,
1292 "speed" : 1,
1293 "srcIn" : 29.423728813559308,
1294 "start" : 98.23728813559322,
1295 "track" : "v0"
1296 },
1297 {
1298 "duration" : 5.355932203389827,
1299 "fadeIn" : 0,
1300 "fadeOut" : 0,
1301 "id" : "8AE89603-E830-41CB-98AA-FCE4DDB65200",
1302 "kind" : "audio",
1303 "linkId" : "DD4C0E3A-C905-48F3-BA08-017781831163",
1304 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
1305 "muted" : true,
1306 "newShot" : false,
1307 "speed" : 1,
1308 "srcIn" : 29.33985727221824,
1309 "start" : 98.23728813559322,
1310 "track" : "v1"
1311 },
1312 {
1313 "duration" : 5.355932203389827,
1314 "fadeIn" : 0,
1315 "fadeOut" : 0,
1316 "id" : "30FEEB7F-94C6-4D85-98F3-9D56EFDEA6DB",
1317 "kind" : "audio",
1318 "linkId" : "DD4C0E3A-C905-48F3-BA08-017781831163",
1319 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
1320 "muted" : false,
1321 "newShot" : false,
1322 "speed" : 1,
1323 "srcIn" : 29.386533575449164,
1324 "start" : 98.23728813559322,
1325 "track" : "v2"
1326 },
1327 {
1328 "duration" : 5.355932203389827,
1329 "fadeIn" : 0,
1330 "fadeOut" : 0,
1331 "id" : "986FD34B-99B0-48E4-8AA8-EA9747E0F300",
1332 "kind" : "video",
1333 "linkId" : "DD4C0E3A-C905-48F3-BA08-017781831163",
1334 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
1335 "muted" : false,
1336 "newShot" : false,
1337 "speed" : 1,
1338 "srcIn" : 29.249926439221042,
1339 "start" : 98.23728813559322,
1340 "track" : "v3"
1341 },
1342 {
1343 "duration" : 5.355932203389827,
1344 "fadeIn" : 0,
1345 "fadeOut" : 0,
1346 "id" : "847F33A9-CA10-4FA4-95E4-3B250E43146A",
1347 "kind" : "video",
1348 "linkId" : "DD4C0E3A-C905-48F3-BA08-017781831163",
1349 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
1350 "muted" : false,
1351 "newShot" : false,
1352 "speed" : 1,
1353 "srcIn" : 29.249921855222226,
1354 "start" : 98.23728813559322,
1355 "track" : "v4"
1356 },
1357 {
1358 "duration" : 7.423728813559308,
1359 "fadeIn" : 0,
1360 "fadeOut" : 0,
1361 "id" : "11BBE731-6722-4267-9D42-9A793553DE06",
1362 "kind" : "video",
1363 "linkId" : "DA913126-892F-46B7-9A7A-51A2A1951521",
1364 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
1365 "muted" : false,
1366 "newShot" : false,
1367 "speed" : 1,
1368 "srcIn" : 193.55932203389838,
1369 "start" : 135.66101694915255,
1370 "track" : "v0"
1371 },
1372 {
1373 "duration" : 7.423728813559308,
1374 "fadeIn" : 0,
1375 "fadeOut" : 0,
1376 "id" : "22421728-439D-4470-95A1-EC2EF0D21C81",
1377 "kind" : "audio",
1378 "linkId" : "DA913126-892F-46B7-9A7A-51A2A1951521",
1379 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
1380 "muted" : true,
1381 "newShot" : false,
1382 "speed" : 1,
1383 "srcIn" : 193.47545049255731,
1384 "start" : 135.66101694915255,
1385 "track" : "v1"
1386 },
1387 {
1388 "duration" : 7.423728813559308,
1389 "fadeIn" : 0,
1390 "fadeOut" : 0,
1391 "id" : "AE02280F-55B2-4037-9D75-5CC083936FD1",
1392 "kind" : "audio",
1393 "linkId" : "DA913126-892F-46B7-9A7A-51A2A1951521",
1394 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
1395 "muted" : false,
1396 "newShot" : false,
1397 "speed" : 1,
1398 "srcIn" : 193.52212679578824,
1399 "start" : 135.66101694915255,
1400 "track" : "v2"
1401 },
1402 {
1403 "duration" : 7.423728813559308,
1404 "fadeIn" : 0,
1405 "fadeOut" : 0,
1406 "id" : "E2A5436F-D353-42DF-9A48-073C9FE6246F",
1407 "kind" : "video",
1408 "linkId" : "DA913126-892F-46B7-9A7A-51A2A1951521",
1409 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
1410 "muted" : false,
1411 "newShot" : false,
1412 "speed" : 1,
1413 "srcIn" : 193.38551965956012,
1414 "start" : 135.66101694915255,
1415 "track" : "v3"
1416 },
1417 {
1418 "duration" : 7.423728813559308,
1419 "fadeIn" : 0,
1420 "fadeOut" : 0,
1421 "id" : "A1DA8F0A-B592-4DBD-9926-213A33E42749",
1422 "kind" : "video",
1423 "linkId" : "DA913126-892F-46B7-9A7A-51A2A1951521",
1424 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
1425 "muted" : false,
1426 "newShot" : false,
1427 "speed" : 1,
1428 "srcIn" : 193.3855150755613,
1429 "start" : 135.66101694915255,
1430 "track" : "v4"
1431 },
1432 {
1433 "duration" : 2.3389830508474745,
1434 "fadeIn" : 0,
1435 "fadeOut" : 0,
1436 "id" : "0F625D10-4206-4D6D-8623-4F14529073E3",
1437 "kind" : "video",
1438 "linkId" : "E458F570-26D5-47DD-87C2-E11FF928A2CA",
1439 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
1440 "muted" : false,
1441 "newShot" : false,
1442 "speed" : 1,
1443 "srcIn" : 214.9152542372882,
1444 "start" : 143.08474576271186,
1445 "track" : "v0"
1446 },
1447 {
1448 "duration" : 2.3389830508474745,
1449 "fadeIn" : 0,
1450 "fadeOut" : 0,
1451 "id" : "F69F71DF-6EEA-48C1-A1F0-F8EE77EE669C",
1452 "kind" : "audio",
1453 "linkId" : "E458F570-26D5-47DD-87C2-E11FF928A2CA",
1454 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
1455 "muted" : true,
1456 "newShot" : false,
1457 "speed" : 1,
1458 "srcIn" : 214.83138269594713,
1459 "start" : 143.08474576271186,
1460 "track" : "v1"
1461 },
1462 {
1463 "duration" : 2.3389830508474745,
1464 "fadeIn" : 0,
1465 "fadeOut" : 0,
1466 "id" : "63619F12-6728-45D4-9664-77B370607EEB",
1467 "kind" : "audio",
1468 "linkId" : "E458F570-26D5-47DD-87C2-E11FF928A2CA",
1469 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
1470 "muted" : false,
1471 "newShot" : false,
1472 "speed" : 1,
1473 "srcIn" : 214.87805899917805,
1474 "start" : 143.08474576271186,
1475 "track" : "v2"
1476 },
1477 {
1478 "duration" : 2.3389830508474745,
1479 "fadeIn" : 0,
1480 "fadeOut" : 0,
1481 "id" : "4F7963B1-6583-494B-A319-8F2BF093C517",
1482 "kind" : "video",
1483 "linkId" : "E458F570-26D5-47DD-87C2-E11FF928A2CA",
1484 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
1485 "muted" : false,
1486 "newShot" : false,
1487 "speed" : 1,
1488 "srcIn" : 214.74145186294993,
1489 "start" : 143.08474576271186,
1490 "track" : "v3"
1491 },
1492 {
1493 "duration" : 2.3389830508474745,
1494 "fadeIn" : 0,
1495 "fadeOut" : 0,
1496 "id" : "95B72825-D2B7-4D97-8C1F-63E1EB598DF9",
1497 "kind" : "video",
1498 "linkId" : "E458F570-26D5-47DD-87C2-E11FF928A2CA",
1499 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
1500 "muted" : false,
1501 "newShot" : false,
1502 "speed" : 1,
1503 "srcIn" : 214.7414472789511,
1504 "start" : 143.08474576271186,
1505 "track" : "v4"
1506 },
1507 {
1508 "duration" : 6.135593220338961,
1509 "fadeIn" : 0,
1510 "fadeOut" : 0,
1511 "id" : "112CE6B8-BFDF-497C-9C69-CCE7802DB06C",
1512 "kind" : "video",
1513 "linkId" : "A28DCAA3-B061-4E38-8372-878AD7EF2060",
1514 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
1515 "muted" : false,
1516 "newShot" : false,
1517 "speed" : 1,
1518 "srcIn" : 226.33898305084753,
1519 "start" : 145.42372881355934,
1520 "track" : "v0"
1521 },
1522 {
1523 "duration" : 6.135593220338961,
1524 "fadeIn" : 0,
1525 "fadeOut" : 0,
1526 "id" : "4040BC36-A9F4-4ED3-A653-1978DDDAF7C5",
1527 "kind" : "audio",
1528 "linkId" : "A28DCAA3-B061-4E38-8372-878AD7EF2060",
1529 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
1530 "muted" : true,
1531 "newShot" : false,
1532 "speed" : 1,
1533 "srcIn" : 226.25511150950646,
1534 "start" : 145.42372881355934,
1535 "track" : "v1"
1536 },
1537 {
1538 "duration" : 6.135593220338961,
1539 "fadeIn" : 0,
1540 "fadeOut" : 0,
1541 "id" : "EFD116A1-EB5D-4CE3-B95D-14B73ECDE548",
1542 "kind" : "audio",
1543 "linkId" : "A28DCAA3-B061-4E38-8372-878AD7EF2060",
1544 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
1545 "muted" : false,
1546 "newShot" : false,
1547 "speed" : 1,
1548 "srcIn" : 226.3017878127374,
1549 "start" : 145.42372881355934,
1550 "track" : "v2"
1551 },
1552 {
1553 "duration" : 6.135593220338961,
1554 "fadeIn" : 0,
1555 "fadeOut" : 0,
1556 "id" : "0AE31757-C7B4-49BB-AB63-0641A6D83BC3",
1557 "kind" : "video",
1558 "linkId" : "A28DCAA3-B061-4E38-8372-878AD7EF2060",
1559 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
1560 "muted" : false,
1561 "newShot" : false,
1562 "speed" : 1,
1563 "srcIn" : 226.16518067650927,
1564 "start" : 145.42372881355934,
1565 "track" : "v3"
1566 },
1567 {
1568 "duration" : 6.135593220338961,
1569 "fadeIn" : 0,
1570 "fadeOut" : 0,
1571 "id" : "9E1DFB7E-F165-4D38-AC11-812E55244554",
1572 "kind" : "video",
1573 "linkId" : "A28DCAA3-B061-4E38-8372-878AD7EF2060",
1574 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
1575 "muted" : false,
1576 "newShot" : false,
1577 "speed" : 1,
1578 "srcIn" : 226.16517609251045,
1579 "start" : 145.42372881355934,
1580 "track" : "v4"
1581 },
1582 {
1583 "duration" : 8.508474576271198,
1584 "fadeIn" : 0,
1585 "fadeOut" : 0,
1586 "id" : "6C78F572-76AD-4191-822E-E127F9D580E4",
1587 "kind" : "video",
1588 "linkId" : "CBF9981A-DB5A-459B-96C0-CFDCEB644C78",
1589 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
1590 "muted" : false,
1591 "newShot" : false,
1592 "speed" : 1,
1593 "srcIn" : 233.8305084745763,
1594 "start" : 151.5593220338983,
1595 "track" : "v0"
1596 },
1597 {
1598 "duration" : 8.508474576271198,
1599 "fadeIn" : 0,
1600 "fadeOut" : 0,
1601 "id" : "443C9738-2CD2-45FF-89ED-93D209FFBFCA",
1602 "kind" : "audio",
1603 "linkId" : "CBF9981A-DB5A-459B-96C0-CFDCEB644C78",
1604 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
1605 "muted" : true,
1606 "newShot" : false,
1607 "speed" : 1,
1608 "srcIn" : 233.74663693323524,
1609 "start" : 151.5593220338983,
1610 "track" : "v1"
1611 },
1612 {
1613 "duration" : 8.508474576271198,
1614 "fadeIn" : 0,
1615 "fadeOut" : 0,
1616 "id" : "B28457F9-9054-461C-99E1-73E68152E679",
1617 "kind" : "audio",
1618 "linkId" : "CBF9981A-DB5A-459B-96C0-CFDCEB644C78",
1619 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
1620 "muted" : false,
1621 "newShot" : false,
1622 "speed" : 1,
1623 "srcIn" : 233.79331323646616,
1624 "start" : 151.5593220338983,
1625 "track" : "v2"
1626 },
1627 {
1628 "duration" : 8.508474576271198,
1629 "fadeIn" : 0,
1630 "fadeOut" : 0,
1631 "id" : "3D057A10-3381-4125-B50C-BC1998039A9D",
1632 "kind" : "video",
1633 "linkId" : "CBF9981A-DB5A-459B-96C0-CFDCEB644C78",
1634 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
1635 "muted" : false,
1636 "newShot" : false,
1637 "speed" : 1,
1638 "srcIn" : 233.65670610023804,
1639 "start" : 151.5593220338983,
1640 "track" : "v3"
1641 },
1642 {
1643 "duration" : 8.508474576271198,
1644 "fadeIn" : 0,
1645 "fadeOut" : 0,
1646 "id" : "BDECC22C-1053-4191-B2F2-2EDBC4F3B5B5",
1647 "kind" : "video",
1648 "linkId" : "CBF9981A-DB5A-459B-96C0-CFDCEB644C78",
1649 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
1650 "muted" : false,
1651 "newShot" : false,
1652 "speed" : 1,
1653 "srcIn" : 233.65670151623922,
1654 "start" : 151.5593220338983,
1655 "track" : "v4"
1656 },
1657 {
1658 "duration" : 9.050847457627128,
1659 "fadeIn" : 0,
1660 "fadeOut" : 0,
1661 "id" : "62014848-FAC6-4BBC-8E7A-14F0B28CC0E6",
1662 "kind" : "video",
1663 "linkId" : "4288CB77-E89F-42B1-927E-DEADACE56BA4",
1664 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
1665 "muted" : false,
1666 "newShot" : false,
1667 "speed" : 1,
1668 "srcIn" : 279.6271186440679,
1669 "start" : 160.0677966101695,
1670 "track" : "v0"
1671 },
1672 {
1673 "duration" : 9.050847457627128,
1674 "fadeIn" : 0,
1675 "fadeOut" : 0,
1676 "id" : "59D15847-5750-41D0-84C8-63D08E771477",
1677 "kind" : "audio",
1678 "linkId" : "4288CB77-E89F-42B1-927E-DEADACE56BA4",
1679 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
1680 "muted" : true,
1681 "newShot" : false,
1682 "speed" : 1,
1683 "srcIn" : 279.5432471027268,
1684 "start" : 160.0677966101695,
1685 "track" : "v1"
1686 },
1687 {
1688 "duration" : 9.050847457627128,
1689 "fadeIn" : 0,
1690 "fadeOut" : 0,
1691 "id" : "3404389F-7945-4513-A69B-1C428F1D90BE",
1692 "kind" : "audio",
1693 "linkId" : "4288CB77-E89F-42B1-927E-DEADACE56BA4",
1694 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
1695 "muted" : false,
1696 "newShot" : false,
1697 "speed" : 1,
1698 "srcIn" : 279.58992340595773,
1699 "start" : 160.0677966101695,
1700 "track" : "v2"
1701 },
1702 {
1703 "duration" : 9.050847457627128,
1704 "fadeIn" : 0,
1705 "fadeOut" : 0,
1706 "id" : "91B4872C-9861-4274-ADDC-E13CB1843662",
1707 "kind" : "video",
1708 "linkId" : "4288CB77-E89F-42B1-927E-DEADACE56BA4",
1709 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
1710 "muted" : false,
1711 "newShot" : false,
1712 "speed" : 1,
1713 "srcIn" : 279.4533162697296,
1714 "start" : 160.0677966101695,
1715 "track" : "v3"
1716 },
1717 {
1718 "duration" : 9.050847457627128,
1719 "fadeIn" : 0,
1720 "fadeOut" : 0,
1721 "id" : "BA59E5A7-F140-4986-8E5B-E8994D035D5D",
1722 "kind" : "video",
1723 "linkId" : "4288CB77-E89F-42B1-927E-DEADACE56BA4",
1724 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
1725 "muted" : false,
1726 "newShot" : false,
1727 "speed" : 1,
1728 "srcIn" : 279.4533116857308,
1729 "start" : 160.0677966101695,
1730 "track" : "v4"
1731 },
1732 {
1733 "duration" : 20.983050847457605,
1734 "fadeIn" : 0,
1735 "fadeOut" : 0,
1736 "id" : "D9CE7BC0-0A5C-4C90-A15A-7694E9E749B7",
1737 "kind" : "video",
1738 "linkId" : "1B8AA32F-8AAC-4699-91CD-75C5E92F7BA0",
1739 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
1740 "muted" : false,
1741 "newShot" : false,
1742 "speed" : 1,
1743 "srcIn" : 341.42372881355936,
1744 "start" : 169.11864406779662,
1745 "track" : "v0"
1746 },
1747 {
1748 "duration" : 20.983050847457605,
1749 "fadeIn" : 0,
1750 "fadeOut" : 0,
1751 "id" : "6F77C3F0-E5FF-426F-99E1-AA28B988BB0A",
1752 "kind" : "audio",
1753 "linkId" : "1B8AA32F-8AAC-4699-91CD-75C5E92F7BA0",
1754 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
1755 "muted" : true,
1756 "newShot" : false,
1757 "speed" : 1,
1758 "srcIn" : 341.3398572722183,
1759 "start" : 169.11864406779662,
1760 "track" : "v1"
1761 },
1762 {
1763 "duration" : 20.983050847457605,
1764 "fadeIn" : 0,
1765 "fadeOut" : 0,
1766 "id" : "DAF8FD62-8F36-4F73-94EE-5380D1E30401",
1767 "kind" : "audio",
1768 "linkId" : "1B8AA32F-8AAC-4699-91CD-75C5E92F7BA0",
1769 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
1770 "muted" : false,
1771 "newShot" : false,
1772 "speed" : 1,
1773 "srcIn" : 341.3865335754492,
1774 "start" : 169.11864406779662,
1775 "track" : "v2"
1776 },
1777 {
1778 "duration" : 20.983050847457605,
1779 "fadeIn" : 0,
1780 "fadeOut" : 0,
1781 "id" : "74FC2FC5-A8A4-469B-9482-73979D2C373D",
1782 "kind" : "video",
1783 "linkId" : "1B8AA32F-8AAC-4699-91CD-75C5E92F7BA0",
1784 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
1785 "muted" : false,
1786 "newShot" : false,
1787 "speed" : 1,
1788 "srcIn" : 341.2499264392211,
1789 "start" : 169.11864406779662,
1790 "track" : "v3"
1791 },
1792 {
1793 "duration" : 20.983050847457605,
1794 "fadeIn" : 0,
1795 "fadeOut" : 0,
1796 "id" : "FFA5E7DE-E5C3-4ED5-9004-3201CD4A30DA",
1797 "kind" : "video",
1798 "linkId" : "1B8AA32F-8AAC-4699-91CD-75C5E92F7BA0",
1799 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
1800 "muted" : false,
1801 "newShot" : false,
1802 "speed" : 1,
1803 "srcIn" : 341.2499218552223,
1804 "start" : 169.11864406779662,
1805 "track" : "v4"
1806 },
1807 {
1808 "duration" : 3.1525423728813564,
1809 "fadeIn" : 0,
1810 "fadeOut" : 0,
1811 "id" : "F7F6AF02-0DE4-4485-8590-753751DA656D",
1812 "kind" : "video",
1813 "linkId" : "1B2E5F19-8476-4AC4-B136-E5222F541298",
1814 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
1815 "muted" : false,
1816 "newShot" : false,
1817 "speed" : 1,
1818 "srcIn" : 516.9830508474577,
1819 "start" : 190.10169491525423,
1820 "track" : "v0"
1821 },
1822 {
1823 "duration" : 3.1525423728813564,
1824 "fadeIn" : 0,
1825 "fadeOut" : 0,
1826 "id" : "0F29059A-7B58-4133-AA5B-F26BDD11FCEA",
1827 "kind" : "audio",
1828 "linkId" : "1B2E5F19-8476-4AC4-B136-E5222F541298",
1829 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
1830 "muted" : true,
1831 "newShot" : false,
1832 "speed" : 1,
1833 "srcIn" : 516.8991793061166,
1834 "start" : 190.10169491525423,
1835 "track" : "v1"
1836 },
1837 {
1838 "duration" : 3.1525423728813564,
1839 "fadeIn" : 0,
1840 "fadeOut" : 0,
1841 "id" : "67E73C14-2908-4A77-83E5-6A8F38F25BF4",
1842 "kind" : "audio",
1843 "linkId" : "1B2E5F19-8476-4AC4-B136-E5222F541298",
1844 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
1845 "muted" : false,
1846 "newShot" : false,
1847 "speed" : 1,
1848 "srcIn" : 516.9458556093475,
1849 "start" : 190.10169491525423,
1850 "track" : "v2"
1851 },
1852 {
1853 "duration" : 3.1525423728813564,
1854 "fadeIn" : 0,
1855 "fadeOut" : 0,
1856 "id" : "DACD3C89-6F61-4E5A-BFF4-ABCB5013028B",
1857 "kind" : "video",
1858 "linkId" : "1B2E5F19-8476-4AC4-B136-E5222F541298",
1859 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
1860 "muted" : false,
1861 "newShot" : false,
1862 "speed" : 1,
1863 "srcIn" : 516.8092484731194,
1864 "start" : 190.10169491525423,
1865 "track" : "v3"
1866 },
1867 {
1868 "duration" : 3.1525423728813564,
1869 "fadeIn" : 0,
1870 "fadeOut" : 0,
1871 "id" : "D85A8D17-5419-4FE8-A767-7886ACAD06D2",
1872 "kind" : "video",
1873 "linkId" : "1B2E5F19-8476-4AC4-B136-E5222F541298",
1874 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
1875 "muted" : false,
1876 "newShot" : false,
1877 "speed" : 1,
1878 "srcIn" : 516.8092438891206,
1879 "start" : 190.10169491525423,
1880 "track" : "v4"
1881 },
1882 {
1883 "duration" : 3.0508474576271283,
1884 "fadeIn" : 0,
1885 "fadeOut" : 0,
1886 "id" : "22E40845-33D9-4113-9E6A-72ABDBAF556B",
1887 "kind" : "video",
1888 "linkId" : "3CC5F7F4-7A10-4825-BA22-58F0E5FA39EC",
1889 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
1890 "muted" : false,
1891 "newShot" : false,
1892 "speed" : 1,
1893 "srcIn" : 526.0677966101696,
1894 "start" : 193.25423728813558,
1895 "track" : "v0"
1896 },
1897 {
1898 "duration" : 3.0508474576271283,
1899 "fadeIn" : 0,
1900 "fadeOut" : 0,
1901 "id" : "80104D35-A942-4197-BE85-9E999B164BD5",
1902 "kind" : "audio",
1903 "linkId" : "3CC5F7F4-7A10-4825-BA22-58F0E5FA39EC",
1904 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
1905 "muted" : true,
1906 "newShot" : false,
1907 "speed" : 1,
1908 "srcIn" : 525.9839250688285,
1909 "start" : 193.25423728813558,
1910 "track" : "v1"
1911 },
1912 {
1913 "duration" : 3.0508474576271283,
1914 "fadeIn" : 0,
1915 "fadeOut" : 0,
1916 "id" : "98A4488E-8AFB-4DAF-AA4A-B6171B5BE4B0",
1917 "kind" : "audio",
1918 "linkId" : "3CC5F7F4-7A10-4825-BA22-58F0E5FA39EC",
1919 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
1920 "muted" : false,
1921 "newShot" : false,
1922 "speed" : 1,
1923 "srcIn" : 526.0306013720594,
1924 "start" : 193.25423728813558,
1925 "track" : "v2"
1926 },
1927 {
1928 "duration" : 3.0508474576271283,
1929 "fadeIn" : 0,
1930 "fadeOut" : 0,
1931 "id" : "31F9AFDE-F19F-4D52-8468-6E4FD6E73843",
1932 "kind" : "video",
1933 "linkId" : "3CC5F7F4-7A10-4825-BA22-58F0E5FA39EC",
1934 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
1935 "muted" : false,
1936 "newShot" : false,
1937 "speed" : 1,
1938 "srcIn" : 525.8939942358313,
1939 "start" : 193.25423728813558,
1940 "track" : "v3"
1941 },
1942 {
1943 "duration" : 3.0508474576271283,
1944 "fadeIn" : 0,
1945 "fadeOut" : 0,
1946 "id" : "58C86E52-F741-4EAF-93E1-9AD9B238BEBC",
1947 "kind" : "video",
1948 "linkId" : "3CC5F7F4-7A10-4825-BA22-58F0E5FA39EC",
1949 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
1950 "muted" : false,
1951 "newShot" : false,
1952 "speed" : 1,
1953 "srcIn" : 525.8939896518325,
1954 "start" : 193.25423728813558,
1955 "track" : "v4"
1956 },
1957 {
1958 "duration" : 12.101694915254257,
1959 "fadeIn" : 0,
1960 "fadeOut" : 0,
1961 "id" : "729D86A6-9A2B-4F18-97A2-9E4B8842705A",
1962 "kind" : "video",
1963 "linkId" : "96ED86B5-C0C4-427A-AB87-51061D1B6C02",
1964 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
1965 "muted" : false,
1966 "newShot" : false,
1967 "speed" : 1,
1968 "srcIn" : 547.3898305084747,
1969 "start" : 196.3050847457627,
1970 "track" : "v0"
1971 },
1972 {
1973 "duration" : 12.101694915254257,
1974 "fadeIn" : 0,
1975 "fadeOut" : 0,
1976 "id" : "537BA6C3-FE42-41F7-AFD1-CCD1498FD164",
1977 "kind" : "audio",
1978 "linkId" : "96ED86B5-C0C4-427A-AB87-51061D1B6C02",
1979 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
1980 "muted" : true,
1981 "newShot" : false,
1982 "speed" : 1,
1983 "srcIn" : 547.3059589671336,
1984 "start" : 196.3050847457627,
1985 "track" : "v1"
1986 },
1987 {
1988 "duration" : 12.101694915254257,
1989 "fadeIn" : 0,
1990 "fadeOut" : 0,
1991 "id" : "DF50C8A8-7061-47D8-9E3E-F76059F64F17",
1992 "kind" : "audio",
1993 "linkId" : "96ED86B5-C0C4-427A-AB87-51061D1B6C02",
1994 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
1995 "muted" : false,
1996 "newShot" : false,
1997 "speed" : 1,
1998 "srcIn" : 547.3526352703645,
1999 "start" : 196.3050847457627,
2000 "track" : "v2"
2001 },
2002 {
2003 "duration" : 12.101694915254257,
2004 "fadeIn" : 0,
2005 "fadeOut" : 0,
2006 "id" : "396149DF-84B8-447F-8A4F-6BC523F5F5C2",
2007 "kind" : "video",
2008 "linkId" : "96ED86B5-C0C4-427A-AB87-51061D1B6C02",
2009 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
2010 "muted" : false,
2011 "newShot" : false,
2012 "speed" : 1,
2013 "srcIn" : 547.2160281341364,
2014 "start" : 196.3050847457627,
2015 "track" : "v3"
2016 },
2017 {
2018 "duration" : 12.101694915254257,
2019 "fadeIn" : 0,
2020 "fadeOut" : 0,
2021 "id" : "8BAC1614-CB12-4DC2-9F56-99B2DD91B028",
2022 "kind" : "video",
2023 "linkId" : "96ED86B5-C0C4-427A-AB87-51061D1B6C02",
2024 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
2025 "muted" : false,
2026 "newShot" : false,
2027 "speed" : 1,
2028 "srcIn" : 547.2160235501376,
2029 "start" : 196.3050847457627,
2030 "track" : "v4"
2031 },
2032 {
2033 "duration" : 4.203389830508456,
2034 "fadeIn" : 0,
2035 "fadeOut" : 0,
2036 "id" : "C677D610-A145-44E9-A5DF-4F24EF105083",
2037 "kind" : "video",
2038 "linkId" : "61201D64-66C9-4C57-8602-97A96701E0BF",
2039 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
2040 "muted" : false,
2041 "newShot" : false,
2042 "speed" : 1,
2043 "srcIn" : 566.3389830508476,
2044 "start" : 208.40677966101697,
2045 "track" : "v0"
2046 },
2047 {
2048 "duration" : 4.203389830508456,
2049 "fadeIn" : 0,
2050 "fadeOut" : 0,
2051 "id" : "2D9D8A03-3523-4FF8-A897-637F158C3C79",
2052 "kind" : "audio",
2053 "linkId" : "61201D64-66C9-4C57-8602-97A96701E0BF",
2054 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
2055 "muted" : true,
2056 "newShot" : false,
2057 "speed" : 1,
2058 "srcIn" : 566.2551115095065,
2059 "start" : 208.40677966101697,
2060 "track" : "v1"
2061 },
2062 {
2063 "duration" : 4.203389830508456,
2064 "fadeIn" : 0,
2065 "fadeOut" : 0,
2066 "id" : "61480E1E-954B-45A2-9C3A-FF0F0BB690FC",
2067 "kind" : "audio",
2068 "linkId" : "61201D64-66C9-4C57-8602-97A96701E0BF",
2069 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
2070 "muted" : false,
2071 "newShot" : false,
2072 "speed" : 1,
2073 "srcIn" : 566.3017878127374,
2074 "start" : 208.40677966101697,
2075 "track" : "v2"
2076 },
2077 {
2078 "duration" : 4.203389830508456,
2079 "fadeIn" : 0,
2080 "fadeOut" : 0,
2081 "id" : "0501FE38-6C08-467C-9E1C-E41CA0498523",
2082 "kind" : "video",
2083 "linkId" : "61201D64-66C9-4C57-8602-97A96701E0BF",
2084 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
2085 "muted" : false,
2086 "newShot" : false,
2087 "speed" : 1,
2088 "srcIn" : 566.1651806765093,
2089 "start" : 208.40677966101697,
2090 "track" : "v3"
2091 },
2092 {
2093 "duration" : 4.203389830508456,
2094 "fadeIn" : 0,
2095 "fadeOut" : 0,
2096 "id" : "74D8A914-BC66-4C84-BFFA-A0CA6639D704",
2097 "kind" : "video",
2098 "linkId" : "61201D64-66C9-4C57-8602-97A96701E0BF",
2099 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
2100 "muted" : false,
2101 "newShot" : false,
2102 "speed" : 1,
2103 "srcIn" : 566.1651760925105,
2104 "start" : 208.40677966101697,
2105 "track" : "v4"
2106 },
2107 {
2108 "duration" : 6.406779661016941,
2109 "fadeIn" : 0,
2110 "fadeOut" : 0,
2111 "id" : "9B498BC6-78AB-44F5-B23B-25E8F7DDC89B",
2112 "kind" : "video",
2113 "linkId" : "02AB076D-1732-49F9-AA10-410FA9A3E78F",
2114 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
2115 "muted" : false,
2116 "newShot" : false,
2117 "speed" : 1,
2118 "srcIn" : 571.35593220339,
2119 "start" : 212.61016949152543,
2120 "track" : "v0"
2121 },
2122 {
2123 "duration" : 6.406779661016941,
2124 "fadeIn" : 0,
2125 "fadeOut" : 0,
2126 "id" : "8C7D77E2-3FA1-41F1-884C-63C516C4AE33",
2127 "kind" : "audio",
2128 "linkId" : "02AB076D-1732-49F9-AA10-410FA9A3E78F",
2129 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
2130 "muted" : true,
2131 "newShot" : false,
2132 "speed" : 1,
2133 "srcIn" : 571.272060662049,
2134 "start" : 212.61016949152543,
2135 "track" : "v1"
2136 },
2137 {
2138 "duration" : 6.406779661016941,
2139 "fadeIn" : 0,
2140 "fadeOut" : 0,
2141 "id" : "21A1243A-B2AF-4637-B9E4-3BA86980E892",
2142 "kind" : "audio",
2143 "linkId" : "02AB076D-1732-49F9-AA10-410FA9A3E78F",
2144 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
2145 "muted" : false,
2146 "newShot" : false,
2147 "speed" : 1,
2148 "srcIn" : 571.3187369652799,
2149 "start" : 212.61016949152543,
2150 "track" : "v2"
2151 },
2152 {
2153 "duration" : 6.406779661016941,
2154 "fadeIn" : 0,
2155 "fadeOut" : 0,
2156 "id" : "F361620E-6686-4423-BFBD-736FF93CDEC5",
2157 "kind" : "video",
2158 "linkId" : "02AB076D-1732-49F9-AA10-410FA9A3E78F",
2159 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
2160 "muted" : false,
2161 "newShot" : false,
2162 "speed" : 1,
2163 "srcIn" : 571.1821298290517,
2164 "start" : 212.61016949152543,
2165 "track" : "v3"
2166 },
2167 {
2168 "duration" : 6.406779661016941,
2169 "fadeIn" : 0,
2170 "fadeOut" : 0,
2171 "id" : "2A763F6C-BEF9-435E-8F42-C72F8F08381A",
2172 "kind" : "video",
2173 "linkId" : "02AB076D-1732-49F9-AA10-410FA9A3E78F",
2174 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
2175 "muted" : false,
2176 "newShot" : false,
2177 "speed" : 1,
2178 "srcIn" : 571.1821252450529,
2179 "start" : 212.61016949152543,
2180 "track" : "v4"
2181 },
2182 {
2183 "duration" : 4.610169491525454,
2184 "fadeIn" : 0,
2185 "fadeOut" : 0,
2186 "id" : "ADAD35C0-6E8C-4D37-B4E4-D2859B73BE27",
2187 "kind" : "video",
2188 "linkId" : "3473CFA9-BD18-42DA-AD05-6C5914B914B6",
2189 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
2190 "muted" : false,
2191 "newShot" : false,
2192 "speed" : 1,
2193 "srcIn" : 584.813559322034,
2194 "start" : 222.06779661016947,
2195 "track" : "v0"
2196 },
2197 {
2198 "duration" : 4.610169491525454,
2199 "fadeIn" : 0,
2200 "fadeOut" : 0,
2201 "id" : "86415894-FDC6-4F60-A9B8-0954DBA0D01D",
2202 "kind" : "audio",
2203 "linkId" : "3473CFA9-BD18-42DA-AD05-6C5914B914B6",
2204 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
2205 "muted" : true,
2206 "newShot" : false,
2207 "speed" : 1,
2208 "srcIn" : 584.729687780693,
2209 "start" : 222.06779661016947,
2210 "track" : "v1"
2211 },
2212 {
2213 "duration" : 4.610169491525454,
2214 "fadeIn" : 0,
2215 "fadeOut" : 0,
2216 "id" : "48CD7335-7777-4C44-A35A-17CFAA490549",
2217 "kind" : "audio",
2218 "linkId" : "3473CFA9-BD18-42DA-AD05-6C5914B914B6",
2219 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
2220 "muted" : false,
2221 "newShot" : false,
2222 "speed" : 1,
2223 "srcIn" : 584.7763640839239,
2224 "start" : 222.06779661016947,
2225 "track" : "v2"
2226 },
2227 {
2228 "duration" : 4.610169491525454,
2229 "fadeIn" : 0,
2230 "fadeOut" : 0,
2231 "id" : "54BD8C49-005E-4485-9DC1-7F916CF8BDD3",
2232 "kind" : "video",
2233 "linkId" : "3473CFA9-BD18-42DA-AD05-6C5914B914B6",
2234 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
2235 "muted" : false,
2236 "newShot" : false,
2237 "speed" : 1,
2238 "srcIn" : 584.6397569476958,
2239 "start" : 222.06779661016947,
2240 "track" : "v3"
2241 },
2242 {
2243 "duration" : 4.610169491525454,
2244 "fadeIn" : 0,
2245 "fadeOut" : 0,
2246 "id" : "23AEC066-77D9-4BA3-8117-EE7A8161CBBD",
2247 "kind" : "video",
2248 "linkId" : "3473CFA9-BD18-42DA-AD05-6C5914B914B6",
2249 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
2250 "muted" : false,
2251 "newShot" : false,
2252 "speed" : 1,
2253 "srcIn" : 584.639752363697,
2254 "start" : 222.06779661016947,
2255 "track" : "v4"
2256 },
2257 {
2258 "duration" : 2.237288135593218,
2259 "fadeIn" : 0,
2260 "fadeOut" : 0,
2261 "id" : "0991B547-CD7C-4CC3-87EC-AD16FF79CEEC",
2262 "kind" : "video",
2263 "linkId" : "02A86E0B-9E9F-4742-A7B2-44024A6F3E6B",
2264 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
2265 "muted" : false,
2266 "newShot" : false,
2267 "speed" : 1,
2268 "srcIn" : 577.7627118644069,
2269 "start" : 219.01694915254237,
2270 "track" : "v0"
2271 },
2272 {
2273 "duration" : 2.237288135593218,
2274 "fadeIn" : 0,
2275 "fadeOut" : 0,
2276 "id" : "A7485496-22C3-4CB6-8034-93F5ACE90052",
2277 "kind" : "audio",
2278 "linkId" : "02A86E0B-9E9F-4742-A7B2-44024A6F3E6B",
2279 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
2280 "muted" : true,
2281 "newShot" : false,
2282 "speed" : 1,
2283 "srcIn" : 577.6788403230659,
2284 "start" : 219.01694915254237,
2285 "track" : "v1"
2286 },
2287 {
2288 "duration" : 2.237288135593218,
2289 "fadeIn" : 0,
2290 "fadeOut" : 0,
2291 "id" : "A8426C1F-0305-45DF-A0C4-57CB7AD56360",
2292 "kind" : "audio",
2293 "linkId" : "02A86E0B-9E9F-4742-A7B2-44024A6F3E6B",
2294 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
2295 "muted" : false,
2296 "newShot" : false,
2297 "speed" : 1,
2298 "srcIn" : 577.7255166262968,
2299 "start" : 219.01694915254237,
2300 "track" : "v2"
2301 },
2302 {
2303 "duration" : 2.237288135593218,
2304 "fadeIn" : 0,
2305 "fadeOut" : 0,
2306 "id" : "F0053E8C-8FC8-47E2-8431-73026DBACEC8",
2307 "kind" : "video",
2308 "linkId" : "02A86E0B-9E9F-4742-A7B2-44024A6F3E6B",
2309 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
2310 "muted" : false,
2311 "newShot" : false,
2312 "speed" : 1,
2313 "srcIn" : 577.5889094900687,
2314 "start" : 219.01694915254237,
2315 "track" : "v3"
2316 },
2317 {
2318 "duration" : 2.237288135593218,
2319 "fadeIn" : 0,
2320 "fadeOut" : 0,
2321 "id" : "EF04E82D-AA3C-41CA-B856-4A6914526EC1",
2322 "kind" : "video",
2323 "linkId" : "02A86E0B-9E9F-4742-A7B2-44024A6F3E6B",
2324 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
2325 "muted" : false,
2326 "newShot" : false,
2327 "speed" : 1,
2328 "srcIn" : 577.5889049060698,
2329 "start" : 219.01694915254237,
2330 "track" : "v4"
2331 },
2332 {
2333 "duration" : 0.33898305084744607,
2334 "fadeIn" : 0,
2335 "fadeOut" : 0,
2336 "id" : "CABADC3E-FA59-4C65-87F2-13BFBE96E557",
2337 "kind" : "video",
2338 "linkId" : "00C998B7-CF37-4D26-B09F-6C36B9B32D82",
2339 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
2340 "muted" : false,
2341 "newShot" : false,
2342 "speed" : 1,
2343 "srcIn" : 582.4067796610171,
2344 "start" : 221.25423728813558,
2345 "track" : "v0"
2346 },
2347 {
2348 "duration" : 0.33898305084744607,
2349 "fadeIn" : 0,
2350 "fadeOut" : 0,
2351 "id" : "FD19E6CE-504A-42B5-96A0-1CD953096F29",
2352 "kind" : "audio",
2353 "linkId" : "00C998B7-CF37-4D26-B09F-6C36B9B32D82",
2354 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
2355 "muted" : true,
2356 "newShot" : false,
2357 "speed" : 1,
2358 "srcIn" : 582.3229081196761,
2359 "start" : 221.25423728813558,
2360 "track" : "v1"
2361 },
2362 {
2363 "duration" : 0.33898305084744607,
2364 "fadeIn" : 0,
2365 "fadeOut" : 0,
2366 "id" : "A112502F-0EDC-4D49-9AB8-9ACA09DFCDE5",
2367 "kind" : "audio",
2368 "linkId" : "00C998B7-CF37-4D26-B09F-6C36B9B32D82",
2369 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
2370 "muted" : false,
2371 "newShot" : false,
2372 "speed" : 1,
2373 "srcIn" : 582.369584422907,
2374 "start" : 221.25423728813558,
2375 "track" : "v2"
2376 },
2377 {
2378 "duration" : 0.33898305084744607,
2379 "fadeIn" : 0,
2380 "fadeOut" : 0,
2381 "id" : "F01A73DE-9729-484B-89E7-719D0391276E",
2382 "kind" : "video",
2383 "linkId" : "00C998B7-CF37-4D26-B09F-6C36B9B32D82",
2384 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
2385 "muted" : false,
2386 "newShot" : false,
2387 "speed" : 1,
2388 "srcIn" : 582.2329772866789,
2389 "start" : 221.25423728813558,
2390 "track" : "v3"
2391 },
2392 {
2393 "duration" : 0.33898305084744607,
2394 "fadeIn" : 0,
2395 "fadeOut" : 0,
2396 "id" : "627B2BC3-48D0-4F6C-A129-90620B0EB3FD",
2397 "kind" : "video",
2398 "linkId" : "00C998B7-CF37-4D26-B09F-6C36B9B32D82",
2399 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
2400 "muted" : false,
2401 "newShot" : false,
2402 "speed" : 1,
2403 "srcIn" : 582.2329727026801,
2404 "start" : 221.25423728813558,
2405 "track" : "v4"
2406 },
2407 {
2408 "duration" : 0.47457627118643586,
2409 "fadeIn" : 0,
2410 "fadeOut" : 0,
2411 "id" : "DC2DEF3C-5F2B-476E-AF27-8A22D6D9AED7",
2412 "kind" : "video",
2413 "linkId" : "699AF368-3D93-47C1-8338-C3D28F300CB5",
2414 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
2415 "muted" : false,
2416 "newShot" : false,
2417 "speed" : 1,
2418 "srcIn" : 583.1864406779663,
2419 "start" : 221.59322033898303,
2420 "track" : "v0"
2421 },
2422 {
2423 "duration" : 0.47457627118643586,
2424 "fadeIn" : 0,
2425 "fadeOut" : 0,
2426 "id" : "F714792D-CAB4-45F1-9BD8-36A1DF081EB8",
2427 "kind" : "audio",
2428 "linkId" : "699AF368-3D93-47C1-8338-C3D28F300CB5",
2429 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
2430 "muted" : true,
2431 "newShot" : false,
2432 "speed" : 1,
2433 "srcIn" : 583.1025691366252,
2434 "start" : 221.59322033898303,
2435 "track" : "v1"
2436 },
2437 {
2438 "duration" : 0.47457627118643586,
2439 "fadeIn" : 0,
2440 "fadeOut" : 0,
2441 "id" : "BA43BF71-4C54-4370-863D-D0B445C4A4DF",
2442 "kind" : "audio",
2443 "linkId" : "699AF368-3D93-47C1-8338-C3D28F300CB5",
2444 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
2445 "muted" : false,
2446 "newShot" : false,
2447 "speed" : 1,
2448 "srcIn" : 583.1492454398561,
2449 "start" : 221.59322033898303,
2450 "track" : "v2"
2451 },
2452 {
2453 "duration" : 0.47457627118643586,
2454 "fadeIn" : 0,
2455 "fadeOut" : 0,
2456 "id" : "4BAC786B-BDD8-4676-9B29-2A7AC95177F6",
2457 "kind" : "video",
2458 "linkId" : "699AF368-3D93-47C1-8338-C3D28F300CB5",
2459 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
2460 "muted" : false,
2461 "newShot" : false,
2462 "speed" : 1,
2463 "srcIn" : 583.012638303628,
2464 "start" : 221.59322033898303,
2465 "track" : "v3"
2466 },
2467 {
2468 "duration" : 0.47457627118643586,
2469 "fadeIn" : 0,
2470 "fadeOut" : 0,
2471 "id" : "987F92AC-8229-449F-BA8F-BB01CA7CADC3",
2472 "kind" : "video",
2473 "linkId" : "699AF368-3D93-47C1-8338-C3D28F300CB5",
2474 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
2475 "muted" : false,
2476 "newShot" : false,
2477 "speed" : 1,
2478 "srcIn" : 583.0126337196292,
2479 "start" : 221.59322033898303,
2480 "track" : "v4"
2481 },
2482 {
2483 "duration" : 4.813559322033882,
2484 "fadeIn" : 0,
2485 "fadeOut" : 0,
2486 "id" : "8B6B0536-EAC5-4272-9106-03B9E89E36AB",
2487 "kind" : "video",
2488 "linkId" : "0F2B0909-5C81-4A61-BBD8-6CD3ECC9563D",
2489 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
2490 "muted" : false,
2491 "newShot" : false,
2492 "speed" : 1,
2493 "srcIn" : 589.6949152542375,
2494 "start" : 226.67796610169492,
2495 "track" : "v0"
2496 },
2497 {
2498 "duration" : 4.813559322033882,
2499 "fadeIn" : 0,
2500 "fadeOut" : 0,
2501 "id" : "DD50D9F8-4C08-47DA-AB75-382FE215D3C0",
2502 "kind" : "audio",
2503 "linkId" : "0F2B0909-5C81-4A61-BBD8-6CD3ECC9563D",
2504 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
2505 "muted" : true,
2506 "newShot" : false,
2507 "speed" : 1,
2508 "srcIn" : 589.6110437128964,
2509 "start" : 226.67796610169492,
2510 "track" : "v1"
2511 },
2512 {
2513 "duration" : 4.813559322033882,
2514 "fadeIn" : 0,
2515 "fadeOut" : 0,
2516 "id" : "A2113A02-7099-45C4-990B-263F71229BAA",
2517 "kind" : "audio",
2518 "linkId" : "0F2B0909-5C81-4A61-BBD8-6CD3ECC9563D",
2519 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
2520 "muted" : false,
2521 "newShot" : false,
2522 "speed" : 1,
2523 "srcIn" : 589.6577200161273,
2524 "start" : 226.67796610169492,
2525 "track" : "v2"
2526 },
2527 {
2528 "duration" : 4.813559322033882,
2529 "fadeIn" : 0,
2530 "fadeOut" : 0,
2531 "id" : "72B5DADE-2448-445A-A7AB-6BA636E753FD",
2532 "kind" : "video",
2533 "linkId" : "0F2B0909-5C81-4A61-BBD8-6CD3ECC9563D",
2534 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
2535 "muted" : false,
2536 "newShot" : false,
2537 "speed" : 1,
2538 "srcIn" : 589.5211128798992,
2539 "start" : 226.67796610169492,
2540 "track" : "v3"
2541 },
2542 {
2543 "duration" : 4.813559322033882,
2544 "fadeIn" : 0,
2545 "fadeOut" : 0,
2546 "id" : "8CC675CB-9ABE-45FF-B384-C7AC4D78F091",
2547 "kind" : "video",
2548 "linkId" : "0F2B0909-5C81-4A61-BBD8-6CD3ECC9563D",
2549 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
2550 "muted" : false,
2551 "newShot" : false,
2552 "speed" : 1,
2553 "srcIn" : 589.5211082959004,
2554 "start" : 226.67796610169492,
2555 "track" : "v4"
2556 },
2557 {
2558 "duration" : 4.81355932203391,
2559 "fadeIn" : 0,
2560 "fadeOut" : 0,
2561 "id" : "EF0E9A54-F218-4203-8874-E267C94F96ED",
2562 "kind" : "video",
2563 "linkId" : "C9B743A1-0C1E-4522-9FAE-C8D05A31C193",
2564 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
2565 "muted" : false,
2566 "newShot" : false,
2567 "speed" : 1,
2568 "srcIn" : 594.5084745762713,
2569 "start" : 231.4915254237288,
2570 "track" : "v0"
2571 },
2572 {
2573 "duration" : 4.81355932203391,
2574 "fadeIn" : 0,
2575 "fadeOut" : 0,
2576 "id" : "3E1A7EAE-EA47-443C-A5DF-51295361CA28",
2577 "kind" : "audio",
2578 "linkId" : "C9B743A1-0C1E-4522-9FAE-C8D05A31C193",
2579 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
2580 "muted" : true,
2581 "newShot" : false,
2582 "speed" : 1,
2583 "srcIn" : 594.4246030349302,
2584 "start" : 231.4915254237288,
2585 "track" : "v1"
2586 },
2587 {
2588 "duration" : 4.81355932203391,
2589 "fadeIn" : 0,
2590 "fadeOut" : 0,
2591 "id" : "63928E72-332C-4FF0-AB20-7C4D9674F55F",
2592 "kind" : "audio",
2593 "linkId" : "C9B743A1-0C1E-4522-9FAE-C8D05A31C193",
2594 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
2595 "muted" : false,
2596 "newShot" : false,
2597 "speed" : 1,
2598 "srcIn" : 594.4712793381611,
2599 "start" : 231.4915254237288,
2600 "track" : "v2"
2601 },
2602 {
2603 "duration" : 4.81355932203391,
2604 "fadeIn" : 0,
2605 "fadeOut" : 0,
2606 "id" : "AF037655-401A-4032-866C-0E9B1BB6EAC8",
2607 "kind" : "video",
2608 "linkId" : "C9B743A1-0C1E-4522-9FAE-C8D05A31C193",
2609 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
2610 "muted" : false,
2611 "newShot" : false,
2612 "speed" : 1,
2613 "srcIn" : 594.334672201933,
2614 "start" : 231.4915254237288,
2615 "track" : "v3"
2616 },
2617 {
2618 "duration" : 4.81355932203391,
2619 "fadeIn" : 0,
2620 "fadeOut" : 0,
2621 "id" : "AF0F661D-9868-49AB-9276-E20CC57DBC28",
2622 "kind" : "video",
2623 "linkId" : "C9B743A1-0C1E-4522-9FAE-C8D05A31C193",
2624 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
2625 "muted" : false,
2626 "newShot" : false,
2627 "speed" : 1,
2628 "srcIn" : 594.3346676179342,
2629 "start" : 231.4915254237288,
2630 "track" : "v4"
2631 },
2632 {
2633 "duration" : 2.13559322033899,
2634 "fadeIn" : 0,
2635 "fadeOut" : 0,
2636 "id" : "EBA9BB70-0CEA-4170-9DBC-65B5D2FE9AD5",
2637 "kind" : "video",
2638 "linkId" : "4AAA7ABF-1577-4C2E-BF38-8705136C6450",
2639 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
2640 "muted" : false,
2641 "newShot" : false,
2642 "speed" : 1,
2643 "srcIn" : 616.8813559322035,
2644 "start" : 236.3050847457627,
2645 "track" : "v0"
2646 },
2647 {
2648 "duration" : 2.13559322033899,
2649 "fadeIn" : 0,
2650 "fadeOut" : 0,
2651 "id" : "A8D715D7-2371-400B-A53A-90BFA9CA14E1",
2652 "kind" : "audio",
2653 "linkId" : "4AAA7ABF-1577-4C2E-BF38-8705136C6450",
2654 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
2655 "muted" : true,
2656 "newShot" : false,
2657 "speed" : 1,
2658 "srcIn" : 616.7974843908625,
2659 "start" : 236.3050847457627,
2660 "track" : "v1"
2661 },
2662 {
2663 "duration" : 2.13559322033899,
2664 "fadeIn" : 0,
2665 "fadeOut" : 0,
2666 "id" : "1EC0AD88-9036-49C3-99E3-32F0047EB028",
2667 "kind" : "audio",
2668 "linkId" : "4AAA7ABF-1577-4C2E-BF38-8705136C6450",
2669 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
2670 "muted" : false,
2671 "newShot" : false,
2672 "speed" : 1,
2673 "srcIn" : 616.8441606940934,
2674 "start" : 236.3050847457627,
2675 "track" : "v2"
2676 },
2677 {
2678 "duration" : 2.13559322033899,
2679 "fadeIn" : 0,
2680 "fadeOut" : 0,
2681 "id" : "A1C1C0D1-CF81-42A4-8930-B00C4C02387B",
2682 "kind" : "video",
2683 "linkId" : "4AAA7ABF-1577-4C2E-BF38-8705136C6450",
2684 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
2685 "muted" : false,
2686 "newShot" : false,
2687 "speed" : 1,
2688 "srcIn" : 616.7075535578653,
2689 "start" : 236.3050847457627,
2690 "track" : "v3"
2691 },
2692 {
2693 "duration" : 2.13559322033899,
2694 "fadeIn" : 0,
2695 "fadeOut" : 0,
2696 "id" : "9694A094-9FA8-4451-BBC8-3A505816AB5C",
2697 "kind" : "video",
2698 "linkId" : "4AAA7ABF-1577-4C2E-BF38-8705136C6450",
2699 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
2700 "muted" : false,
2701 "newShot" : false,
2702 "speed" : 1,
2703 "srcIn" : 616.7075489738664,
2704 "start" : 236.3050847457627,
2705 "track" : "v4"
2706 },
2707 {
2708 "duration" : 2.2033898305084563,
2709 "fadeIn" : 0,
2710 "fadeOut" : 0,
2711 "id" : "2DFE5FB3-850C-408B-888E-1B117520E619",
2712 "kind" : "video",
2713 "linkId" : "20A4E34D-C55C-439E-9743-C91BA434CB07",
2714 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
2715 "muted" : false,
2716 "newShot" : false,
2717 "speed" : 1,
2718 "srcIn" : 623.1864406779663,
2719 "start" : 238.4406779661017,
2720 "track" : "v0"
2721 },
2722 {
2723 "duration" : 2.2033898305084563,
2724 "fadeIn" : 0,
2725 "fadeOut" : 0,
2726 "id" : "1C5AFA4B-AB07-45EB-9012-D6817BCA669F",
2727 "kind" : "audio",
2728 "linkId" : "20A4E34D-C55C-439E-9743-C91BA434CB07",
2729 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
2730 "muted" : true,
2731 "newShot" : false,
2732 "speed" : 1,
2733 "srcIn" : 623.1025691366252,
2734 "start" : 238.4406779661017,
2735 "track" : "v1"
2736 },
2737 {
2738 "duration" : 2.2033898305084563,
2739 "fadeIn" : 0,
2740 "fadeOut" : 0,
2741 "id" : "FA4004F4-5AA3-4F29-999B-41460017B48F",
2742 "kind" : "audio",
2743 "linkId" : "20A4E34D-C55C-439E-9743-C91BA434CB07",
2744 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
2745 "muted" : false,
2746 "newShot" : false,
2747 "speed" : 1,
2748 "srcIn" : 623.1492454398561,
2749 "start" : 238.4406779661017,
2750 "track" : "v2"
2751 },
2752 {
2753 "duration" : 2.2033898305084563,
2754 "fadeIn" : 0,
2755 "fadeOut" : 0,
2756 "id" : "F4539CE1-8350-4649-A8D5-75DEC9A85285",
2757 "kind" : "video",
2758 "linkId" : "20A4E34D-C55C-439E-9743-C91BA434CB07",
2759 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
2760 "muted" : false,
2761 "newShot" : false,
2762 "speed" : 1,
2763 "srcIn" : 623.012638303628,
2764 "start" : 238.4406779661017,
2765 "track" : "v3"
2766 },
2767 {
2768 "duration" : 2.2033898305084563,
2769 "fadeIn" : 0,
2770 "fadeOut" : 0,
2771 "id" : "B9D12715-E647-49E2-A68A-393FDA9865DF",
2772 "kind" : "video",
2773 "linkId" : "20A4E34D-C55C-439E-9743-C91BA434CB07",
2774 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
2775 "muted" : false,
2776 "newShot" : false,
2777 "speed" : 1,
2778 "srcIn" : 623.0126337196292,
2779 "start" : 238.4406779661017,
2780 "track" : "v4"
2781 },
2782 {
2783 "duration" : 6.27118644067798,
2784 "fadeIn" : 0,
2785 "fadeOut" : 0,
2786 "id" : "92455958-0ACB-484E-B1D9-19C02F73901A",
2787 "kind" : "video",
2788 "linkId" : "FB7680B4-9AF3-40EC-B7FB-010031D66837",
2789 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
2790 "muted" : false,
2791 "newShot" : false,
2792 "speed" : 1,
2793 "srcIn" : 627.491525423729,
2794 "start" : 240.64406779661016,
2795 "track" : "v0"
2796 },
2797 {
2798 "duration" : 6.27118644067798,
2799 "fadeIn" : 0,
2800 "fadeOut" : 0,
2801 "id" : "DB167161-A9FA-4F64-92F8-79A205627364",
2802 "kind" : "audio",
2803 "linkId" : "FB7680B4-9AF3-40EC-B7FB-010031D66837",
2804 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
2805 "muted" : true,
2806 "newShot" : false,
2807 "speed" : 1,
2808 "srcIn" : 627.4076538823879,
2809 "start" : 240.64406779661016,
2810 "track" : "v1"
2811 },
2812 {
2813 "duration" : 6.27118644067798,
2814 "fadeIn" : 0,
2815 "fadeOut" : 0,
2816 "id" : "D53DC51B-6ED3-43B5-90CF-F263E6A8EF0F",
2817 "kind" : "audio",
2818 "linkId" : "FB7680B4-9AF3-40EC-B7FB-010031D66837",
2819 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
2820 "muted" : false,
2821 "newShot" : false,
2822 "speed" : 1,
2823 "srcIn" : 627.4543301856188,
2824 "start" : 240.64406779661016,
2825 "track" : "v2"
2826 },
2827 {
2828 "duration" : 6.27118644067798,
2829 "fadeIn" : 0,
2830 "fadeOut" : 0,
2831 "id" : "0F96EE3E-4FE2-417F-9760-F24B131C949C",
2832 "kind" : "video",
2833 "linkId" : "FB7680B4-9AF3-40EC-B7FB-010031D66837",
2834 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
2835 "muted" : false,
2836 "newShot" : false,
2837 "speed" : 1,
2838 "srcIn" : 627.3177230493907,
2839 "start" : 240.64406779661016,
2840 "track" : "v3"
2841 },
2842 {
2843 "duration" : 6.27118644067798,
2844 "fadeIn" : 0,
2845 "fadeOut" : 0,
2846 "id" : "473DAC6E-6B6F-4395-AA04-1D2A9448459F",
2847 "kind" : "video",
2848 "linkId" : "FB7680B4-9AF3-40EC-B7FB-010031D66837",
2849 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
2850 "muted" : false,
2851 "newShot" : false,
2852 "speed" : 1,
2853 "srcIn" : 627.3177184653919,
2854 "start" : 240.64406779661016,
2855 "track" : "v4"
2856 },
2857 {
2858 "duration" : 5.288135593220346,
2859 "fadeIn" : 0,
2860 "fadeOut" : 0,
2861 "id" : "04295A05-E2E5-4A94-A70F-1C9DCC3ACF86",
2862 "kind" : "video",
2863 "linkId" : "EDE7F931-7884-4F1E-8EFB-E8B00DCB5A07",
2864 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
2865 "muted" : false,
2866 "newShot" : false,
2867 "speed" : 1,
2868 "srcIn" : 641.220338983051,
2869 "start" : 246.91525423728814,
2870 "track" : "v0"
2871 },
2872 {
2873 "duration" : 5.288135593220346,
2874 "fadeIn" : 0,
2875 "fadeOut" : 0,
2876 "id" : "A4446EF1-6B91-4D7B-9285-3C4E42A475ED",
2877 "kind" : "audio",
2878 "linkId" : "EDE7F931-7884-4F1E-8EFB-E8B00DCB5A07",
2879 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
2880 "muted" : true,
2881 "newShot" : false,
2882 "speed" : 1,
2883 "srcIn" : 641.1364674417099,
2884 "start" : 246.91525423728814,
2885 "track" : "v1"
2886 },
2887 {
2888 "duration" : 5.288135593220346,
2889 "fadeIn" : 0,
2890 "fadeOut" : 0,
2891 "id" : "37E22D5C-E179-4CD6-9C69-C6B281846229",
2892 "kind" : "audio",
2893 "linkId" : "EDE7F931-7884-4F1E-8EFB-E8B00DCB5A07",
2894 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
2895 "muted" : false,
2896 "newShot" : false,
2897 "speed" : 1,
2898 "srcIn" : 641.1831437449408,
2899 "start" : 246.91525423728814,
2900 "track" : "v2"
2901 },
2902 {
2903 "duration" : 5.288135593220346,
2904 "fadeIn" : 0,
2905 "fadeOut" : 0,
2906 "id" : "0D2B5334-E420-4746-B09A-110DA7EE6BA5",
2907 "kind" : "video",
2908 "linkId" : "EDE7F931-7884-4F1E-8EFB-E8B00DCB5A07",
2909 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
2910 "muted" : false,
2911 "newShot" : false,
2912 "speed" : 1,
2913 "srcIn" : 641.0465366087127,
2914 "start" : 246.91525423728814,
2915 "track" : "v3"
2916 },
2917 {
2918 "duration" : 5.288135593220346,
2919 "fadeIn" : 0,
2920 "fadeOut" : 0,
2921 "id" : "95AA48BC-4C4B-4F4B-B29F-FB2E01943DE6",
2922 "kind" : "video",
2923 "linkId" : "EDE7F931-7884-4F1E-8EFB-E8B00DCB5A07",
2924 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
2925 "muted" : false,
2926 "newShot" : false,
2927 "speed" : 1,
2928 "srcIn" : 641.0465320247139,
2929 "start" : 246.91525423728814,
2930 "track" : "v4"
2931 },
2932 {
2933 "duration" : 3.3898305084745743,
2934 "fadeIn" : 0,
2935 "fadeOut" : 0,
2936 "id" : "36A8E6B0-4985-4EBD-BD50-2621E6D840E5",
2937 "kind" : "video",
2938 "linkId" : "E1D1EEBE-B0FA-47B6-B02B-0D25E3E7C904",
2939 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
2940 "muted" : false,
2941 "newShot" : false,
2942 "speed" : 1,
2943 "srcIn" : 649.4237288135594,
2944 "start" : 252.20338983050848,
2945 "track" : "v0"
2946 },
2947 {
2948 "duration" : 3.3898305084745743,
2949 "fadeIn" : 0,
2950 "fadeOut" : 0,
2951 "id" : "CDDA95CC-536D-4225-AAC3-470904596C1D",
2952 "kind" : "audio",
2953 "linkId" : "E1D1EEBE-B0FA-47B6-B02B-0D25E3E7C904",
2954 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
2955 "muted" : true,
2956 "newShot" : false,
2957 "speed" : 1,
2958 "srcIn" : 649.3398572722183,
2959 "start" : 252.20338983050848,
2960 "track" : "v1"
2961 },
2962 {
2963 "duration" : 3.3898305084745743,
2964 "fadeIn" : 0,
2965 "fadeOut" : 0,
2966 "id" : "79AC6946-AD60-4D7C-9B74-BAF2C5043ED9",
2967 "kind" : "audio",
2968 "linkId" : "E1D1EEBE-B0FA-47B6-B02B-0D25E3E7C904",
2969 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
2970 "muted" : false,
2971 "newShot" : false,
2972 "speed" : 1,
2973 "srcIn" : 649.3865335754492,
2974 "start" : 252.20338983050848,
2975 "track" : "v2"
2976 },
2977 {
2978 "duration" : 3.3898305084745743,
2979 "fadeIn" : 0,
2980 "fadeOut" : 0,
2981 "id" : "266B1043-1A29-4485-85C8-9921E2D077FE",
2982 "kind" : "video",
2983 "linkId" : "E1D1EEBE-B0FA-47B6-B02B-0D25E3E7C904",
2984 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
2985 "muted" : false,
2986 "newShot" : false,
2987 "speed" : 1,
2988 "srcIn" : 649.2499264392211,
2989 "start" : 252.20338983050848,
2990 "track" : "v3"
2991 },
2992 {
2993 "duration" : 3.3898305084745743,
2994 "fadeIn" : 0,
2995 "fadeOut" : 0,
2996 "id" : "F9D93C0A-48FD-4149-B214-E2648C026BB8",
2997 "kind" : "video",
2998 "linkId" : "E1D1EEBE-B0FA-47B6-B02B-0D25E3E7C904",
2999 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
3000 "muted" : false,
3001 "newShot" : false,
3002 "speed" : 1,
3003 "srcIn" : 649.2499218552223,
3004 "start" : 252.20338983050848,
3005 "track" : "v4"
3006 },
3007 {
3008 "duration" : 8.9491525423729,
3009 "fadeIn" : 0,
3010 "fadeOut" : 0,
3011 "id" : "E13CDB86-D0E7-4219-891E-396AF72F7200",
3012 "kind" : "video",
3013 "linkId" : "E0DD6E6F-D7C7-4743-ADC2-D8787163B0C3",
3014 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
3015 "muted" : false,
3016 "newShot" : false,
3017 "speed" : 1,
3018 "srcIn" : 664.1016949152543,
3019 "start" : 255.59322033898306,
3020 "track" : "v0"
3021 },
3022 {
3023 "duration" : 8.9491525423729,
3024 "fadeIn" : 0,
3025 "fadeOut" : 0,
3026 "id" : "CF093561-83EC-4803-9CD7-E1D07429E06C",
3027 "kind" : "audio",
3028 "linkId" : "E0DD6E6F-D7C7-4743-ADC2-D8787163B0C3",
3029 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
3030 "muted" : true,
3031 "newShot" : false,
3032 "speed" : 1,
3033 "srcIn" : 664.0178233739132,
3034 "start" : 255.59322033898306,
3035 "track" : "v1"
3036 },
3037 {
3038 "duration" : 8.9491525423729,
3039 "fadeIn" : 0,
3040 "fadeOut" : 0,
3041 "id" : "D7A74DDC-3F2A-4E02-B350-45455C959B82",
3042 "kind" : "audio",
3043 "linkId" : "E0DD6E6F-D7C7-4743-ADC2-D8787163B0C3",
3044 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
3045 "muted" : false,
3046 "newShot" : false,
3047 "speed" : 1,
3048 "srcIn" : 664.0644996771441,
3049 "start" : 255.59322033898306,
3050 "track" : "v2"
3051 },
3052 {
3053 "duration" : 8.9491525423729,
3054 "fadeIn" : 0,
3055 "fadeOut" : 0,
3056 "id" : "95E67BFD-C59E-4F99-92B5-027C3BC13CC3",
3057 "kind" : "video",
3058 "linkId" : "E0DD6E6F-D7C7-4743-ADC2-D8787163B0C3",
3059 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
3060 "muted" : false,
3061 "newShot" : false,
3062 "speed" : 1,
3063 "srcIn" : 663.927892540916,
3064 "start" : 255.59322033898306,
3065 "track" : "v3"
3066 },
3067 {
3068 "duration" : 8.9491525423729,
3069 "fadeIn" : 0,
3070 "fadeOut" : 0,
3071 "id" : "AE9DDFED-5BE9-44A5-8349-623A8C1F1587",
3072 "kind" : "video",
3073 "linkId" : "E0DD6E6F-D7C7-4743-ADC2-D8787163B0C3",
3074 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
3075 "muted" : false,
3076 "newShot" : false,
3077 "speed" : 1,
3078 "srcIn" : 663.9278879569172,
3079 "start" : 255.59322033898306,
3080 "track" : "v4"
3081 },
3082 {
3083 "duration" : 2.4067796610169125,
3084 "fadeIn" : 0,
3085 "fadeOut" : 0,
3086 "id" : "6929AB44-D977-4A42-9DD4-69D4E759E4B7",
3087 "kind" : "video",
3088 "linkId" : "B1E3CEAB-46C9-4EF7-8BC4-F413D2FDD063",
3089 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
3090 "muted" : false,
3091 "newShot" : false,
3092 "speed" : 1,
3093 "srcIn" : 676.7796610169491,
3094 "start" : 264.54237288135596,
3095 "track" : "v0"
3096 },
3097 {
3098 "duration" : 2.4067796610169125,
3099 "fadeIn" : 0,
3100 "fadeOut" : 0,
3101 "id" : "F6BA54DA-3AAD-48D5-A132-C946F6FF5BBE",
3102 "kind" : "audio",
3103 "linkId" : "B1E3CEAB-46C9-4EF7-8BC4-F413D2FDD063",
3104 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
3105 "muted" : true,
3106 "newShot" : false,
3107 "speed" : 1,
3108 "srcIn" : 676.6957894756081,
3109 "start" : 264.54237288135596,
3110 "track" : "v1"
3111 },
3112 {
3113 "duration" : 2.4067796610169125,
3114 "fadeIn" : 0,
3115 "fadeOut" : 0,
3116 "id" : "95E0BF0A-76D3-4341-87C0-4C3855C6AD49",
3117 "kind" : "audio",
3118 "linkId" : "B1E3CEAB-46C9-4EF7-8BC4-F413D2FDD063",
3119 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
3120 "muted" : false,
3121 "newShot" : false,
3122 "speed" : 1,
3123 "srcIn" : 676.742465778839,
3124 "start" : 264.54237288135596,
3125 "track" : "v2"
3126 },
3127 {
3128 "duration" : 2.4067796610169125,
3129 "fadeIn" : 0,
3130 "fadeOut" : 0,
3131 "id" : "56B8138F-5F33-4EC1-A202-59403546D6C5",
3132 "kind" : "video",
3133 "linkId" : "B1E3CEAB-46C9-4EF7-8BC4-F413D2FDD063",
3134 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
3135 "muted" : false,
3136 "newShot" : false,
3137 "speed" : 1,
3138 "srcIn" : 676.6058586426109,
3139 "start" : 264.54237288135596,
3140 "track" : "v3"
3141 },
3142 {
3143 "duration" : 2.4067796610169125,
3144 "fadeIn" : 0,
3145 "fadeOut" : 0,
3146 "id" : "7C722B8F-E78E-45E3-8A2E-F30FF2127283",
3147 "kind" : "video",
3148 "linkId" : "B1E3CEAB-46C9-4EF7-8BC4-F413D2FDD063",
3149 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
3150 "muted" : false,
3151 "newShot" : false,
3152 "speed" : 1,
3153 "srcIn" : 676.6058540586121,
3154 "start" : 264.54237288135596,
3155 "track" : "v4"
3156 },
3157 {
3158 "duration" : 4.27118644067798,
3159 "fadeIn" : 0,
3160 "fadeOut" : 0,
3161 "id" : "74A4A856-856E-45BF-BF4B-60A6E5627D38",
3162 "kind" : "video",
3163 "linkId" : "4DA91130-EF31-48CE-9353-1BCA2F4D362F",
3164 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
3165 "muted" : false,
3166 "newShot" : false,
3167 "speed" : 1,
3168 "srcIn" : 680.5084745762712,
3169 "start" : 266.9491525423729,
3170 "track" : "v0"
3171 },
3172 {
3173 "duration" : 4.27118644067798,
3174 "fadeIn" : 0,
3175 "fadeOut" : 0,
3176 "id" : "3DBBE8FE-AD87-457B-8670-E95DE832B26C",
3177 "kind" : "audio",
3178 "linkId" : "4DA91130-EF31-48CE-9353-1BCA2F4D362F",
3179 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
3180 "muted" : true,
3181 "newShot" : false,
3182 "speed" : 1,
3183 "srcIn" : 680.4246030349301,
3184 "start" : 266.9491525423729,
3185 "track" : "v1"
3186 },
3187 {
3188 "duration" : 4.27118644067798,
3189 "fadeIn" : 0,
3190 "fadeOut" : 0,
3191 "id" : "5E2EC015-CDDC-45F6-BEF4-76E842BBC389",
3192 "kind" : "audio",
3193 "linkId" : "4DA91130-EF31-48CE-9353-1BCA2F4D362F",
3194 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
3195 "muted" : false,
3196 "newShot" : false,
3197 "speed" : 1,
3198 "srcIn" : 680.471279338161,
3199 "start" : 266.9491525423729,
3200 "track" : "v2"
3201 },
3202 {
3203 "duration" : 4.27118644067798,
3204 "fadeIn" : 0,
3205 "fadeOut" : 0,
3206 "id" : "1275A44B-DBDA-421C-9443-3923A7274D8D",
3207 "kind" : "video",
3208 "linkId" : "4DA91130-EF31-48CE-9353-1BCA2F4D362F",
3209 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
3210 "muted" : false,
3211 "newShot" : false,
3212 "speed" : 1,
3213 "srcIn" : 680.3346722019329,
3214 "start" : 266.9491525423729,
3215 "track" : "v3"
3216 },
3217 {
3218 "duration" : 4.27118644067798,
3219 "fadeIn" : 0,
3220 "fadeOut" : 0,
3221 "id" : "15AE2513-D3F9-47C3-B4F7-61A351DAB004",
3222 "kind" : "video",
3223 "linkId" : "4DA91130-EF31-48CE-9353-1BCA2F4D362F",
3224 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
3225 "muted" : false,
3226 "newShot" : false,
3227 "speed" : 1,
3228 "srcIn" : 680.3346676179341,
3229 "start" : 266.9491525423729,
3230 "track" : "v4"
3231 },
3232 {
3233 "duration" : 1.1186440677965948,
3234 "fadeIn" : 0,
3235 "fadeOut" : 0,
3236 "id" : "2077C05E-DE10-4841-AB24-8F92DE0579D1",
3237 "kind" : "video",
3238 "linkId" : "870FE72E-0830-4984-8A17-C8265B573862",
3239 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
3240 "muted" : false,
3241 "newShot" : false,
3242 "speed" : 1,
3243 "srcIn" : 698.1016949152543,
3244 "start" : 271.22033898305085,
3245 "track" : "v0"
3246 },
3247 {
3248 "duration" : 1.1186440677965948,
3249 "fadeIn" : 0,
3250 "fadeOut" : 0,
3251 "id" : "1832EFCE-19F8-4EBA-B21A-88826F9AE7BF",
3252 "kind" : "audio",
3253 "linkId" : "870FE72E-0830-4984-8A17-C8265B573862",
3254 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
3255 "muted" : true,
3256 "newShot" : false,
3257 "speed" : 1,
3258 "srcIn" : 698.0178233739132,
3259 "start" : 271.22033898305085,
3260 "track" : "v1"
3261 },
3262 {
3263 "duration" : 1.1186440677965948,
3264 "fadeIn" : 0,
3265 "fadeOut" : 0,
3266 "id" : "4823B8FA-4845-43E7-A4AD-95170EAE1A77",
3267 "kind" : "audio",
3268 "linkId" : "870FE72E-0830-4984-8A17-C8265B573862",
3269 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
3270 "muted" : false,
3271 "newShot" : false,
3272 "speed" : 1,
3273 "srcIn" : 698.0644996771441,
3274 "start" : 271.22033898305085,
3275 "track" : "v2"
3276 },
3277 {
3278 "duration" : 1.1186440677965948,
3279 "fadeIn" : 0,
3280 "fadeOut" : 0,
3281 "id" : "AB47B68B-4217-46FF-A1EA-D21122BC749F",
3282 "kind" : "video",
3283 "linkId" : "870FE72E-0830-4984-8A17-C8265B573862",
3284 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
3285 "muted" : false,
3286 "newShot" : false,
3287 "speed" : 1,
3288 "srcIn" : 697.927892540916,
3289 "start" : 271.22033898305085,
3290 "track" : "v3"
3291 },
3292 {
3293 "duration" : 1.1186440677965948,
3294 "fadeIn" : 0,
3295 "fadeOut" : 0,
3296 "id" : "948310E4-F6E1-4C7B-8DFC-4B1EFE91D9F3",
3297 "kind" : "video",
3298 "linkId" : "870FE72E-0830-4984-8A17-C8265B573862",
3299 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
3300 "muted" : false,
3301 "newShot" : false,
3302 "speed" : 1,
3303 "srcIn" : 697.9278879569172,
3304 "start" : 271.22033898305085,
3305 "track" : "v4"
3306 },
3307 {
3308 "duration" : 3.9661016949152668,
3309 "fadeIn" : 0,
3310 "fadeOut" : 0,
3311 "id" : "1841DCD7-E1AC-46F4-8BA9-65F76CE7F997",
3312 "kind" : "video",
3313 "linkId" : "8E3E2F7E-8CEA-4F62-AC48-EAD5610BBF7A",
3314 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
3315 "muted" : false,
3316 "newShot" : false,
3317 "speed" : 1,
3318 "srcIn" : 706.1016949152543,
3319 "start" : 272.33898305084745,
3320 "track" : "v0"
3321 },
3322 {
3323 "duration" : 3.9661016949152668,
3324 "fadeIn" : 0,
3325 "fadeOut" : 0,
3326 "id" : "9F70CFD3-7AB7-4A6D-87AF-B53BEDECA24A",
3327 "kind" : "audio",
3328 "linkId" : "8E3E2F7E-8CEA-4F62-AC48-EAD5610BBF7A",
3329 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
3330 "muted" : true,
3331 "newShot" : false,
3332 "speed" : 1,
3333 "srcIn" : 706.0178233739132,
3334 "start" : 272.33898305084745,
3335 "track" : "v1"
3336 },
3337 {
3338 "duration" : 3.9661016949152668,
3339 "fadeIn" : 0,
3340 "fadeOut" : 0,
3341 "id" : "6296E4FA-F3A8-4E9F-B5A6-21C104C11316",
3342 "kind" : "audio",
3343 "linkId" : "8E3E2F7E-8CEA-4F62-AC48-EAD5610BBF7A",
3344 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
3345 "muted" : false,
3346 "newShot" : false,
3347 "speed" : 1,
3348 "srcIn" : 706.0644996771441,
3349 "start" : 272.33898305084745,
3350 "track" : "v2"
3351 },
3352 {
3353 "duration" : 3.9661016949152668,
3354 "fadeIn" : 0,
3355 "fadeOut" : 0,
3356 "id" : "7DBC5BCC-4F01-497F-97A9-FD8E5FED80ED",
3357 "kind" : "video",
3358 "linkId" : "8E3E2F7E-8CEA-4F62-AC48-EAD5610BBF7A",
3359 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
3360 "muted" : false,
3361 "newShot" : false,
3362 "speed" : 1,
3363 "srcIn" : 705.927892540916,
3364 "start" : 272.33898305084745,
3365 "track" : "v3"
3366 },
3367 {
3368 "duration" : 3.9661016949152668,
3369 "fadeIn" : 0,
3370 "fadeOut" : 0,
3371 "id" : "D22E7D29-E8F2-49FC-AE39-95140062ED4C",
3372 "kind" : "video",
3373 "linkId" : "8E3E2F7E-8CEA-4F62-AC48-EAD5610BBF7A",
3374 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
3375 "muted" : false,
3376 "newShot" : false,
3377 "speed" : 1,
3378 "srcIn" : 705.9278879569172,
3379 "start" : 272.33898305084745,
3380 "track" : "v4"
3381 },
3382 {
3383 "duration" : 7.2542372881355845,
3384 "fadeIn" : 0,
3385 "fadeOut" : 0,
3386 "id" : "ADE6D5E2-A342-4F16-9FAB-2F40C2D1BCDD",
3387 "kind" : "video",
3388 "linkId" : "7AAFBE23-17BF-4AC0-B4E6-3D003AF75DF9",
3389 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
3390 "muted" : false,
3391 "newShot" : false,
3392 "speed" : 1,
3393 "srcIn" : 710.0677966101696,
3394 "start" : 276.3050847457627,
3395 "track" : "v0"
3396 },
3397 {
3398 "duration" : 7.2542372881355845,
3399 "fadeIn" : 0,
3400 "fadeOut" : 0,
3401 "id" : "BFEEDB13-FCE4-473E-BA8B-BAA795862399",
3402 "kind" : "audio",
3403 "linkId" : "7AAFBE23-17BF-4AC0-B4E6-3D003AF75DF9",
3404 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
3405 "muted" : true,
3406 "newShot" : false,
3407 "speed" : 1,
3408 "srcIn" : 709.9839250688285,
3409 "start" : 276.3050847457627,
3410 "track" : "v1"
3411 },
3412 {
3413 "duration" : 7.2542372881355845,
3414 "fadeIn" : 0,
3415 "fadeOut" : 0,
3416 "id" : "BC83D6FA-84FA-483B-8699-D1219DF29357",
3417 "kind" : "audio",
3418 "linkId" : "7AAFBE23-17BF-4AC0-B4E6-3D003AF75DF9",
3419 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
3420 "muted" : false,
3421 "newShot" : false,
3422 "speed" : 1,
3423 "srcIn" : 710.0306013720594,
3424 "start" : 276.3050847457627,
3425 "track" : "v2"
3426 },
3427 {
3428 "duration" : 7.2542372881355845,
3429 "fadeIn" : 0,
3430 "fadeOut" : 0,
3431 "id" : "58221FDA-00C6-45A1-9267-2AC4500070FE",
3432 "kind" : "video",
3433 "linkId" : "7AAFBE23-17BF-4AC0-B4E6-3D003AF75DF9",
3434 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
3435 "muted" : false,
3436 "newShot" : false,
3437 "speed" : 1,
3438 "srcIn" : 709.8939942358313,
3439 "start" : 276.3050847457627,
3440 "track" : "v3"
3441 },
3442 {
3443 "duration" : 7.2542372881355845,
3444 "fadeIn" : 0,
3445 "fadeOut" : 0,
3446 "id" : "BC23C71D-81CD-40A8-8D4C-5728293351FB",
3447 "kind" : "video",
3448 "linkId" : "7AAFBE23-17BF-4AC0-B4E6-3D003AF75DF9",
3449 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
3450 "muted" : false,
3451 "newShot" : false,
3452 "speed" : 1,
3453 "srcIn" : 709.8939896518325,
3454 "start" : 276.3050847457627,
3455 "track" : "v4"
3456 },
3457 {
3458 "duration" : 7.830508474576277,
3459 "fadeIn" : 0,
3460 "fadeOut" : 0,
3461 "id" : "5B448404-52D6-4D2E-83B3-DDABD2847728",
3462 "kind" : "video",
3463 "linkId" : "B6345D38-3912-4C07-BC43-685A4C767720",
3464 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
3465 "muted" : false,
3466 "newShot" : false,
3467 "speed" : 1,
3468 "srcIn" : 721.6271186440679,
3469 "start" : 283.5593220338983,
3470 "track" : "v0"
3471 },
3472 {
3473 "duration" : 7.830508474576277,
3474 "fadeIn" : 0,
3475 "fadeOut" : 0,
3476 "id" : "EE73D2CB-10F9-47A3-B0A9-B077DC6C6B40",
3477 "kind" : "audio",
3478 "linkId" : "B6345D38-3912-4C07-BC43-685A4C767720",
3479 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
3480 "muted" : true,
3481 "newShot" : false,
3482 "speed" : 1,
3483 "srcIn" : 721.5432471027268,
3484 "start" : 283.5593220338983,
3485 "track" : "v1"
3486 },
3487 {
3488 "duration" : 7.830508474576277,
3489 "fadeIn" : 0,
3490 "fadeOut" : 0,
3491 "id" : "BD06C153-DAD3-449C-B108-1A8130000941",
3492 "kind" : "audio",
3493 "linkId" : "B6345D38-3912-4C07-BC43-685A4C767720",
3494 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
3495 "muted" : false,
3496 "newShot" : false,
3497 "speed" : 1,
3498 "srcIn" : 721.5899234059577,
3499 "start" : 283.5593220338983,
3500 "track" : "v2"
3501 },
3502 {
3503 "duration" : 7.830508474576277,
3504 "fadeIn" : 0,
3505 "fadeOut" : 0,
3506 "id" : "9EFE24ED-F389-4DB3-945B-BF2F18D9915B",
3507 "kind" : "video",
3508 "linkId" : "B6345D38-3912-4C07-BC43-685A4C767720",
3509 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
3510 "muted" : false,
3511 "newShot" : false,
3512 "speed" : 1,
3513 "srcIn" : 721.4533162697296,
3514 "start" : 283.5593220338983,
3515 "track" : "v3"
3516 },
3517 {
3518 "duration" : 7.830508474576277,
3519 "fadeIn" : 0,
3520 "fadeOut" : 0,
3521 "id" : "26D01EC2-D5AF-4693-809B-C57786F0F277",
3522 "kind" : "video",
3523 "linkId" : "B6345D38-3912-4C07-BC43-685A4C767720",
3524 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
3525 "muted" : false,
3526 "newShot" : false,
3527 "speed" : 1,
3528 "srcIn" : 721.4533116857308,
3529 "start" : 283.5593220338983,
3530 "track" : "v4"
3531 },
3532 {
3533 "duration" : 6.27118644067798,
3534 "fadeIn" : 0,
3535 "fadeOut" : 0,
3536 "id" : "6ACBC2E5-88A0-48BD-ABE1-16D4214541B8",
3537 "kind" : "video",
3538 "linkId" : "9DBD3CAA-D3B8-40E4-9DAB-5C8025F8248C",
3539 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
3540 "muted" : false,
3541 "newShot" : false,
3542 "speed" : 1,
3543 "srcIn" : 736.3389830508476,
3544 "start" : 291.3898305084746,
3545 "track" : "v0"
3546 },
3547 {
3548 "duration" : 6.27118644067798,
3549 "fadeIn" : 0,
3550 "fadeOut" : 0,
3551 "id" : "EBF24B46-FE81-45AB-97D4-6FD5E677B893",
3552 "kind" : "audio",
3553 "linkId" : "9DBD3CAA-D3B8-40E4-9DAB-5C8025F8248C",
3554 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
3555 "muted" : true,
3556 "newShot" : false,
3557 "speed" : 1,
3558 "srcIn" : 736.2551115095065,
3559 "start" : 291.3898305084746,
3560 "track" : "v1"
3561 },
3562 {
3563 "duration" : 6.27118644067798,
3564 "fadeIn" : 0,
3565 "fadeOut" : 0,
3566 "id" : "DF4C7EBF-43A7-4CDE-837E-4995C5DE3EE7",
3567 "kind" : "audio",
3568 "linkId" : "9DBD3CAA-D3B8-40E4-9DAB-5C8025F8248C",
3569 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
3570 "muted" : false,
3571 "newShot" : false,
3572 "speed" : 1,
3573 "srcIn" : 736.3017878127374,
3574 "start" : 291.3898305084746,
3575 "track" : "v2"
3576 },
3577 {
3578 "duration" : 6.27118644067798,
3579 "fadeIn" : 0,
3580 "fadeOut" : 0,
3581 "id" : "F39E9B23-7810-4DA0-B17A-6D487A801159",
3582 "kind" : "video",
3583 "linkId" : "9DBD3CAA-D3B8-40E4-9DAB-5C8025F8248C",
3584 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
3585 "muted" : false,
3586 "newShot" : false,
3587 "speed" : 1,
3588 "srcIn" : 736.1651806765093,
3589 "start" : 291.3898305084746,
3590 "track" : "v3"
3591 },
3592 {
3593 "duration" : 6.27118644067798,
3594 "fadeIn" : 0,
3595 "fadeOut" : 0,
3596 "id" : "D58A628D-522C-4267-BA46-9E4D55FE030E",
3597 "kind" : "video",
3598 "linkId" : "9DBD3CAA-D3B8-40E4-9DAB-5C8025F8248C",
3599 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
3600 "muted" : false,
3601 "newShot" : false,
3602 "speed" : 1,
3603 "srcIn" : 736.1651760925105,
3604 "start" : 291.3898305084746,
3605 "track" : "v4"
3606 },
3607 {
3608 "duration" : 9.288135593220318,
3609 "fadeIn" : 0,
3610 "fadeOut" : 0,
3611 "id" : "8A9513E7-2C9C-4EB6-84AE-EDFF80DCF552",
3612 "kind" : "video",
3613 "linkId" : "2D759F83-E902-4851-A3CB-8D0088A77D2D",
3614 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
3615 "muted" : false,
3616 "newShot" : false,
3617 "speed" : 1,
3618 "srcIn" : 747.2881355932204,
3619 "start" : 297.66101694915255,
3620 "track" : "v0"
3621 },
3622 {
3623 "duration" : 9.288135593220318,
3624 "fadeIn" : 0,
3625 "fadeOut" : 0,
3626 "id" : "D7CDE17B-5946-4F71-B09B-6983C216681E",
3627 "kind" : "audio",
3628 "linkId" : "2D759F83-E902-4851-A3CB-8D0088A77D2D",
3629 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
3630 "muted" : true,
3631 "newShot" : false,
3632 "speed" : 1,
3633 "srcIn" : 747.2042640518794,
3634 "start" : 297.66101694915255,
3635 "track" : "v1"
3636 },
3637 {
3638 "duration" : 9.288135593220318,
3639 "fadeIn" : 0,
3640 "fadeOut" : 0,
3641 "id" : "CC586FEC-2BAB-4053-AC79-1FA59BD9BF45",
3642 "kind" : "audio",
3643 "linkId" : "2D759F83-E902-4851-A3CB-8D0088A77D2D",
3644 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
3645 "muted" : false,
3646 "newShot" : false,
3647 "speed" : 1,
3648 "srcIn" : 747.2509403551103,
3649 "start" : 297.66101694915255,
3650 "track" : "v2"
3651 },
3652 {
3653 "duration" : 9.288135593220318,
3654 "fadeIn" : 0,
3655 "fadeOut" : 0,
3656 "id" : "926C01C2-ADAD-4B46-BE04-CD6B4618CCC3",
3657 "kind" : "video",
3658 "linkId" : "2D759F83-E902-4851-A3CB-8D0088A77D2D",
3659 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
3660 "muted" : false,
3661 "newShot" : false,
3662 "speed" : 1,
3663 "srcIn" : 747.1143332188822,
3664 "start" : 297.66101694915255,
3665 "track" : "v3"
3666 },
3667 {
3668 "duration" : 9.288135593220318,
3669 "fadeIn" : 0,
3670 "fadeOut" : 0,
3671 "id" : "0AB6F4F2-89CA-4D4B-A52E-765F89C40923",
3672 "kind" : "video",
3673 "linkId" : "2D759F83-E902-4851-A3CB-8D0088A77D2D",
3674 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
3675 "muted" : false,
3676 "newShot" : false,
3677 "speed" : 1,
3678 "srcIn" : 747.1143286348833,
3679 "start" : 297.66101694915255,
3680 "track" : "v4"
3681 },
3682 {
3683 "duration" : 4.372881355932236,
3684 "fadeIn" : 0,
3685 "fadeOut" : 0,
3686 "id" : "E84D7CB3-1292-4241-AAD4-AA71BA97E643",
3687 "kind" : "video",
3688 "linkId" : "1F9BC1C6-8DE8-4750-ADF1-DBC052261B98",
3689 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
3690 "muted" : false,
3691 "newShot" : false,
3692 "speed" : 1,
3693 "srcIn" : 763.2203389830509,
3694 "start" : 306.9491525423729,
3695 "track" : "v0"
3696 },
3697 {
3698 "duration" : 4.372881355932236,
3699 "fadeIn" : 0,
3700 "fadeOut" : 0,
3701 "id" : "48214E82-E8A7-45D1-A218-C6F71276D005",
3702 "kind" : "audio",
3703 "linkId" : "1F9BC1C6-8DE8-4750-ADF1-DBC052261B98",
3704 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
3705 "muted" : true,
3706 "newShot" : false,
3707 "speed" : 1,
3708 "srcIn" : 763.1364674417098,
3709 "start" : 306.9491525423729,
3710 "track" : "v1"
3711 },
3712 {
3713 "duration" : 4.372881355932236,
3714 "fadeIn" : 0,
3715 "fadeOut" : 0,
3716 "id" : "5D3BAE7E-FE37-43FE-9E8C-945DB4037BD3",
3717 "kind" : "audio",
3718 "linkId" : "1F9BC1C6-8DE8-4750-ADF1-DBC052261B98",
3719 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
3720 "muted" : false,
3721 "newShot" : false,
3722 "speed" : 1,
3723 "srcIn" : 763.1831437449407,
3724 "start" : 306.9491525423729,
3725 "track" : "v2"
3726 },
3727 {
3728 "duration" : 4.372881355932236,
3729 "fadeIn" : 0,
3730 "fadeOut" : 0,
3731 "id" : "6B9196EE-5BAF-4BC6-A372-D785A26AB9B6",
3732 "kind" : "video",
3733 "linkId" : "1F9BC1C6-8DE8-4750-ADF1-DBC052261B98",
3734 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
3735 "muted" : false,
3736 "newShot" : false,
3737 "speed" : 1,
3738 "srcIn" : 763.0465366087126,
3739 "start" : 306.9491525423729,
3740 "track" : "v3"
3741 },
3742 {
3743 "duration" : 4.372881355932236,
3744 "fadeIn" : 0,
3745 "fadeOut" : 0,
3746 "id" : "92FFAC2A-5A18-4CD5-A8E8-38A3237085FB",
3747 "kind" : "video",
3748 "linkId" : "1F9BC1C6-8DE8-4750-ADF1-DBC052261B98",
3749 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
3750 "muted" : false,
3751 "newShot" : false,
3752 "speed" : 1,
3753 "srcIn" : 763.0465320247138,
3754 "start" : 306.9491525423729,
3755 "track" : "v4"
3756 },
3757 {
3758 "duration" : 6.203389830508456,
3759 "fadeIn" : 0,
3760 "fadeOut" : 0,
3761 "id" : "B4E35468-D60F-41C5-9DEB-00DF0D9AB8F0",
3762 "kind" : "video",
3763 "linkId" : "5C054F95-5ECF-452C-8B05-51B9272EC65B",
3764 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
3765 "muted" : false,
3766 "newShot" : false,
3767 "speed" : 1,
3768 "srcIn" : 771.2203389830509,
3769 "start" : 311.3220338983051,
3770 "track" : "v0"
3771 },
3772 {
3773 "duration" : 6.203389830508456,
3774 "fadeIn" : 0,
3775 "fadeOut" : 0,
3776 "id" : "EE8E5E05-B61B-4960-ABF2-9C3490F03B65",
3777 "kind" : "audio",
3778 "linkId" : "5C054F95-5ECF-452C-8B05-51B9272EC65B",
3779 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
3780 "muted" : true,
3781 "newShot" : false,
3782 "speed" : 1,
3783 "srcIn" : 771.1364674417098,
3784 "start" : 311.3220338983051,
3785 "track" : "v1"
3786 },
3787 {
3788 "duration" : 6.203389830508456,
3789 "fadeIn" : 0,
3790 "fadeOut" : 0,
3791 "id" : "41C6BBFA-FF1F-4336-8A02-CC2A7AD8D711",
3792 "kind" : "audio",
3793 "linkId" : "5C054F95-5ECF-452C-8B05-51B9272EC65B",
3794 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
3795 "muted" : false,
3796 "newShot" : false,
3797 "speed" : 1,
3798 "srcIn" : 771.1831437449407,
3799 "start" : 311.3220338983051,
3800 "track" : "v2"
3801 },
3802 {
3803 "duration" : 6.203389830508456,
3804 "fadeIn" : 0,
3805 "fadeOut" : 0,
3806 "id" : "01FAD5BD-4079-4B78-964E-021AB45FD327",
3807 "kind" : "video",
3808 "linkId" : "5C054F95-5ECF-452C-8B05-51B9272EC65B",
3809 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
3810 "muted" : false,
3811 "newShot" : false,
3812 "speed" : 1,
3813 "srcIn" : 771.0465366087126,
3814 "start" : 311.3220338983051,
3815 "track" : "v3"
3816 },
3817 {
3818 "duration" : 6.203389830508456,
3819 "fadeIn" : 0,
3820 "fadeOut" : 0,
3821 "id" : "D05C5F7B-1439-48C3-A549-332D2965E644",
3822 "kind" : "video",
3823 "linkId" : "5C054F95-5ECF-452C-8B05-51B9272EC65B",
3824 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
3825 "muted" : false,
3826 "newShot" : false,
3827 "speed" : 1,
3828 "srcIn" : 771.0465320247138,
3829 "start" : 311.3220338983051,
3830 "track" : "v4"
3831 },
3832 {
3833 "duration" : 4.033898305084733,
3834 "fadeIn" : 0,
3835 "fadeOut" : 0,
3836 "id" : "9ADC6EF4-01E6-44A2-A4E7-CE979C4216A6",
3837 "kind" : "video",
3838 "linkId" : "E7791304-08F7-4757-B49E-97BD758DF635",
3839 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
3840 "muted" : false,
3841 "newShot" : false,
3842 "speed" : 1,
3843 "srcIn" : 780.271186440678,
3844 "start" : 317.52542372881356,
3845 "track" : "v0"
3846 },
3847 {
3848 "duration" : 4.033898305084733,
3849 "fadeIn" : 0,
3850 "fadeOut" : 0,
3851 "id" : "C83F690F-75F2-4F0E-9F44-2B3B64FC4D7D",
3852 "kind" : "audio",
3853 "linkId" : "E7791304-08F7-4757-B49E-97BD758DF635",
3854 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
3855 "muted" : true,
3856 "newShot" : false,
3857 "speed" : 1,
3858 "srcIn" : 780.1873148993369,
3859 "start" : 317.52542372881356,
3860 "track" : "v1"
3861 },
3862 {
3863 "duration" : 4.033898305084733,
3864 "fadeIn" : 0,
3865 "fadeOut" : 0,
3866 "id" : "7F4D1569-EB2B-470A-A39D-4E27BB9631D3",
3867 "kind" : "audio",
3868 "linkId" : "E7791304-08F7-4757-B49E-97BD758DF635",
3869 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
3870 "muted" : false,
3871 "newShot" : false,
3872 "speed" : 1,
3873 "srcIn" : 780.2339912025678,
3874 "start" : 317.52542372881356,
3875 "track" : "v2"
3876 },
3877 {
3878 "duration" : 4.033898305084733,
3879 "fadeIn" : 0,
3880 "fadeOut" : 0,
3881 "id" : "F566D86E-6BCB-450B-B165-E742669968FE",
3882 "kind" : "video",
3883 "linkId" : "E7791304-08F7-4757-B49E-97BD758DF635",
3884 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
3885 "muted" : false,
3886 "newShot" : false,
3887 "speed" : 1,
3888 "srcIn" : 780.0973840663397,
3889 "start" : 317.52542372881356,
3890 "track" : "v3"
3891 },
3892 {
3893 "duration" : 4.033898305084733,
3894 "fadeIn" : 0,
3895 "fadeOut" : 0,
3896 "id" : "397C4077-BE8E-46D8-9A26-4A85FB164ADD",
3897 "kind" : "video",
3898 "linkId" : "E7791304-08F7-4757-B49E-97BD758DF635",
3899 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
3900 "muted" : false,
3901 "newShot" : false,
3902 "speed" : 1,
3903 "srcIn" : 780.0973794823409,
3904 "start" : 317.52542372881356,
3905 "track" : "v4"
3906 },
3907 {
3908 "duration" : 2.474576271186436,
3909 "fadeIn" : 0,
3910 "fadeOut" : 0,
3911 "id" : "3EA1AE5B-B71F-4DD3-BAB2-258343BC38E6",
3912 "kind" : "video",
3913 "linkId" : "9683EB9F-EC01-480F-8BE7-2201B247DE7E",
3914 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
3915 "muted" : false,
3916 "newShot" : false,
3917 "speed" : 1,
3918 "srcIn" : 790.7118644067798,
3919 "start" : 321.5593220338983,
3920 "track" : "v0"
3921 },
3922 {
3923 "duration" : 2.474576271186436,
3924 "fadeIn" : 0,
3925 "fadeOut" : 0,
3926 "id" : "84190D3F-8FAE-46C1-8402-B9BE98C961DD",
3927 "kind" : "audio",
3928 "linkId" : "9683EB9F-EC01-480F-8BE7-2201B247DE7E",
3929 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
3930 "muted" : true,
3931 "newShot" : false,
3932 "speed" : 1,
3933 "srcIn" : 790.6279928654387,
3934 "start" : 321.5593220338983,
3935 "track" : "v1"
3936 },
3937 {
3938 "duration" : 2.474576271186436,
3939 "fadeIn" : 0,
3940 "fadeOut" : 0,
3941 "id" : "BA726F72-FB82-4612-8EBA-361CFF5A177B",
3942 "kind" : "audio",
3943 "linkId" : "9683EB9F-EC01-480F-8BE7-2201B247DE7E",
3944 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
3945 "muted" : false,
3946 "newShot" : false,
3947 "speed" : 1,
3948 "srcIn" : 790.6746691686697,
3949 "start" : 321.5593220338983,
3950 "track" : "v2"
3951 },
3952 {
3953 "duration" : 2.474576271186436,
3954 "fadeIn" : 0,
3955 "fadeOut" : 0,
3956 "id" : "A2BD0CC2-F087-4B62-B210-2422E7CDA62E",
3957 "kind" : "video",
3958 "linkId" : "9683EB9F-EC01-480F-8BE7-2201B247DE7E",
3959 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
3960 "muted" : false,
3961 "newShot" : false,
3962 "speed" : 1,
3963 "srcIn" : 790.5380620324415,
3964 "start" : 321.5593220338983,
3965 "track" : "v3"
3966 },
3967 {
3968 "duration" : 2.474576271186436,
3969 "fadeIn" : 0,
3970 "fadeOut" : 0,
3971 "id" : "CB9AED26-D860-4411-9D20-1B63F56F0BEC",
3972 "kind" : "video",
3973 "linkId" : "9683EB9F-EC01-480F-8BE7-2201B247DE7E",
3974 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
3975 "muted" : false,
3976 "newShot" : false,
3977 "speed" : 1,
3978 "srcIn" : 790.5380574484427,
3979 "start" : 321.5593220338983,
3980 "track" : "v4"
3981 },
3982 {
3983 "duration" : 3.525423728813564,
3984 "fadeIn" : 0,
3985 "fadeOut" : 0,
3986 "id" : "5650BD3B-4BF5-4533-B873-6A89B355DBBD",
3987 "kind" : "video",
3988 "linkId" : "CABF546D-96A6-4629-8BD2-A6CED6EBD7E4",
3989 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
3990 "muted" : false,
3991 "newShot" : false,
3992 "speed" : 1,
3993 "srcIn" : 794.7118644067798,
3994 "start" : 324.03389830508473,
3995 "track" : "v0"
3996 },
3997 {
3998 "duration" : 3.525423728813564,
3999 "fadeIn" : 0,
4000 "fadeOut" : 0,
4001 "id" : "3BA60039-3255-435B-90E4-738D94B13821",
4002 "kind" : "audio",
4003 "linkId" : "CABF546D-96A6-4629-8BD2-A6CED6EBD7E4",
4004 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
4005 "muted" : true,
4006 "newShot" : false,
4007 "speed" : 1,
4008 "srcIn" : 794.6279928654387,
4009 "start" : 324.03389830508473,
4010 "track" : "v1"
4011 },
4012 {
4013 "duration" : 3.525423728813564,
4014 "fadeIn" : 0,
4015 "fadeOut" : 0,
4016 "id" : "1A07AF09-C783-4547-BB45-5671AD0A75E7",
4017 "kind" : "audio",
4018 "linkId" : "CABF546D-96A6-4629-8BD2-A6CED6EBD7E4",
4019 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
4020 "muted" : false,
4021 "newShot" : false,
4022 "speed" : 1,
4023 "srcIn" : 794.6746691686697,
4024 "start" : 324.03389830508473,
4025 "track" : "v2"
4026 },
4027 {
4028 "duration" : 3.525423728813564,
4029 "fadeIn" : 0,
4030 "fadeOut" : 0,
4031 "id" : "A656775F-4507-44FD-ADCB-E23AE9572BD6",
4032 "kind" : "video",
4033 "linkId" : "CABF546D-96A6-4629-8BD2-A6CED6EBD7E4",
4034 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
4035 "muted" : false,
4036 "newShot" : false,
4037 "speed" : 1,
4038 "srcIn" : 794.5380620324415,
4039 "start" : 324.03389830508473,
4040 "track" : "v3"
4041 },
4042 {
4043 "duration" : 3.525423728813564,
4044 "fadeIn" : 0,
4045 "fadeOut" : 0,
4046 "id" : "F287E2E4-9B24-4928-9DF4-BE2A88C6F6E1",
4047 "kind" : "video",
4048 "linkId" : "CABF546D-96A6-4629-8BD2-A6CED6EBD7E4",
4049 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
4050 "muted" : false,
4051 "newShot" : false,
4052 "speed" : 1,
4053 "srcIn" : 794.5380574484427,
4054 "start" : 324.03389830508473,
4055 "track" : "v4"
4056 },
4057 {
4058 "duration" : 11.694915254237287,
4059 "fadeIn" : 0,
4060 "fadeOut" : 0,
4061 "id" : "10BDB7F1-A97E-46B4-AA17-D5AA082F4FE5",
4062 "kind" : "video",
4063 "linkId" : "FE61ADB3-0C95-4A1A-B7F6-D3B4874BD0EA",
4064 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
4065 "muted" : false,
4066 "newShot" : false,
4067 "speed" : 1,
4068 "srcIn" : 812.6440677966104,
4069 "start" : 327.5593220338983,
4070 "track" : "v0"
4071 },
4072 {
4073 "duration" : 11.694915254237287,
4074 "fadeIn" : 0,
4075 "fadeOut" : 0,
4076 "id" : "1A175E00-C9B8-4071-9395-B96E6BBC0766",
4077 "kind" : "audio",
4078 "linkId" : "FE61ADB3-0C95-4A1A-B7F6-D3B4874BD0EA",
4079 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
4080 "muted" : true,
4081 "newShot" : false,
4082 "speed" : 1,
4083 "srcIn" : 812.5601962552694,
4084 "start" : 327.5593220338983,
4085 "track" : "v1"
4086 },
4087 {
4088 "duration" : 11.694915254237287,
4089 "fadeIn" : 0,
4090 "fadeOut" : 0,
4091 "id" : "9CDBFDAC-83C2-4F7B-AC48-618437D267F1",
4092 "kind" : "audio",
4093 "linkId" : "FE61ADB3-0C95-4A1A-B7F6-D3B4874BD0EA",
4094 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
4095 "muted" : false,
4096 "newShot" : false,
4097 "speed" : 1,
4098 "srcIn" : 812.6068725585003,
4099 "start" : 327.5593220338983,
4100 "track" : "v2"
4101 },
4102 {
4103 "duration" : 11.694915254237287,
4104 "fadeIn" : 0,
4105 "fadeOut" : 0,
4106 "id" : "F8DB026C-68E2-4110-8310-A4E77894383C",
4107 "kind" : "video",
4108 "linkId" : "FE61ADB3-0C95-4A1A-B7F6-D3B4874BD0EA",
4109 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
4110 "muted" : false,
4111 "newShot" : false,
4112 "speed" : 1,
4113 "srcIn" : 812.4702654222722,
4114 "start" : 327.5593220338983,
4115 "track" : "v3"
4116 },
4117 {
4118 "duration" : 11.694915254237287,
4119 "fadeIn" : 0,
4120 "fadeOut" : 0,
4121 "id" : "2C6F6969-72A9-49A9-8A8D-A15D2D15A434",
4122 "kind" : "video",
4123 "linkId" : "FE61ADB3-0C95-4A1A-B7F6-D3B4874BD0EA",
4124 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
4125 "muted" : false,
4126 "newShot" : false,
4127 "speed" : 1,
4128 "srcIn" : 812.4702608382734,
4129 "start" : 327.5593220338983,
4130 "track" : "v4"
4131 },
4132 {
4133 "duration" : 1.3898305084745743,
4134 "fadeIn" : 0,
4135 "fadeOut" : 0,
4136 "id" : "5D76FCCF-E20B-4E2B-B2BF-06D04D2EC873",
4137 "kind" : "video",
4138 "linkId" : "5A0236C5-2C98-4C5E-A570-96D211E7B3C6",
4139 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
4140 "muted" : false,
4141 "newShot" : false,
4142 "speed" : 1,
4143 "srcIn" : 980.813559322034,
4144 "start" : 339.2542372881356,
4145 "track" : "v0"
4146 },
4147 {
4148 "duration" : 1.3898305084745743,
4149 "fadeIn" : 0,
4150 "fadeOut" : 0,
4151 "id" : "B834F191-B8D9-4288-A76C-5BC67BBEAA7E",
4152 "kind" : "audio",
4153 "linkId" : "5A0236C5-2C98-4C5E-A570-96D211E7B3C6",
4154 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
4155 "muted" : true,
4156 "newShot" : false,
4157 "speed" : 1,
4158 "srcIn" : 980.729687780693,
4159 "start" : 339.2542372881356,
4160 "track" : "v1"
4161 },
4162 {
4163 "duration" : 1.3898305084745743,
4164 "fadeIn" : 0,
4165 "fadeOut" : 0,
4166 "id" : "EC711F21-C8A3-40C5-B230-A451B6878977",
4167 "kind" : "audio",
4168 "linkId" : "5A0236C5-2C98-4C5E-A570-96D211E7B3C6",
4169 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
4170 "muted" : false,
4171 "newShot" : false,
4172 "speed" : 1,
4173 "srcIn" : 980.7763640839239,
4174 "start" : 339.2542372881356,
4175 "track" : "v2"
4176 },
4177 {
4178 "duration" : 1.3898305084745743,
4179 "fadeIn" : 0,
4180 "fadeOut" : 0,
4181 "id" : "3BFC58C2-D801-45E4-8F01-5F94BDB9ED96",
4182 "kind" : "video",
4183 "linkId" : "5A0236C5-2C98-4C5E-A570-96D211E7B3C6",
4184 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
4185 "muted" : false,
4186 "newShot" : false,
4187 "speed" : 1,
4188 "srcIn" : 980.6397569476958,
4189 "start" : 339.2542372881356,
4190 "track" : "v3"
4191 },
4192 {
4193 "duration" : 1.3898305084745743,
4194 "fadeIn" : 0,
4195 "fadeOut" : 0,
4196 "id" : "28885AEF-445D-47A2-88D3-550BBF692ADE",
4197 "kind" : "video",
4198 "linkId" : "5A0236C5-2C98-4C5E-A570-96D211E7B3C6",
4199 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
4200 "muted" : false,
4201 "newShot" : false,
4202 "speed" : 1,
4203 "srcIn" : 980.639752363697,
4204 "start" : 339.2542372881356,
4205 "track" : "v4"
4206 },
4207 {
4208 "duration" : 0.6440677966101589,
4209 "fadeIn" : 0,
4210 "fadeOut" : 0,
4211 "id" : "EA534368-94B4-475D-8041-45696947070E",
4212 "kind" : "video",
4213 "linkId" : "B31B5062-B2A8-445B-A891-F2D2104E86E3",
4214 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
4215 "muted" : false,
4216 "newShot" : false,
4217 "speed" : 1,
4218 "srcIn" : 983.1186440677968,
4219 "start" : 340.64406779661016,
4220 "track" : "v0"
4221 },
4222 {
4223 "duration" : 0.6440677966101589,
4224 "fadeIn" : 0,
4225 "fadeOut" : 0,
4226 "id" : "F6F6F9A9-8B8B-443A-B401-32205A74C093",
4227 "kind" : "audio",
4228 "linkId" : "B31B5062-B2A8-445B-A891-F2D2104E86E3",
4229 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
4230 "muted" : true,
4231 "newShot" : false,
4232 "speed" : 1,
4233 "srcIn" : 983.0347725264558,
4234 "start" : 340.64406779661016,
4235 "track" : "v1"
4236 },
4237 {
4238 "duration" : 0.6440677966101589,
4239 "fadeIn" : 0,
4240 "fadeOut" : 0,
4241 "id" : "1176FB5D-D780-4273-9FCA-9695A04A100F",
4242 "kind" : "audio",
4243 "linkId" : "B31B5062-B2A8-445B-A891-F2D2104E86E3",
4244 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
4245 "muted" : false,
4246 "newShot" : false,
4247 "speed" : 1,
4248 "srcIn" : 983.0814488296867,
4249 "start" : 340.64406779661016,
4250 "track" : "v2"
4251 },
4252 {
4253 "duration" : 0.6440677966101589,
4254 "fadeIn" : 0,
4255 "fadeOut" : 0,
4256 "id" : "5DA60AD6-AC7E-4579-86CD-50F79A14150D",
4257 "kind" : "video",
4258 "linkId" : "B31B5062-B2A8-445B-A891-F2D2104E86E3",
4259 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
4260 "muted" : false,
4261 "newShot" : false,
4262 "speed" : 1,
4263 "srcIn" : 982.9448416934586,
4264 "start" : 340.64406779661016,
4265 "track" : "v3"
4266 },
4267 {
4268 "duration" : 0.6440677966101589,
4269 "fadeIn" : 0,
4270 "fadeOut" : 0,
4271 "id" : "3DBD1518-ACEA-4CC3-9B85-FB6C1BFDDDE4",
4272 "kind" : "video",
4273 "linkId" : "B31B5062-B2A8-445B-A891-F2D2104E86E3",
4274 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
4275 "muted" : false,
4276 "newShot" : false,
4277 "speed" : 1,
4278 "srcIn" : 982.9448371094597,
4279 "start" : 340.64406779661016,
4280 "track" : "v4"
4281 },
4282 {
4283 "duration" : 3.6271186440678207,
4284 "fadeIn" : 0,
4285 "fadeOut" : 0,
4286 "id" : "832B3ED1-5BC4-43EE-8FE6-717D93008C5A",
4287 "kind" : "video",
4288 "linkId" : "596C8323-127D-44C1-B319-7DF1FB8B1171",
4289 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
4290 "muted" : false,
4291 "newShot" : false,
4292 "speed" : 1,
4293 "srcIn" : 1037.8305084745766,
4294 "start" : 341.2881355932203,
4295 "track" : "v0"
4296 },
4297 {
4298 "duration" : 3.6271186440678207,
4299 "fadeIn" : 0,
4300 "fadeOut" : 0,
4301 "id" : "00209D2F-08A1-4B49-96F2-91A8C5EF454D",
4302 "kind" : "audio",
4303 "linkId" : "596C8323-127D-44C1-B319-7DF1FB8B1171",
4304 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
4305 "muted" : true,
4306 "newShot" : false,
4307 "speed" : 1,
4308 "srcIn" : 1037.7466369332355,
4309 "start" : 341.2881355932203,
4310 "track" : "v1"
4311 },
4312 {
4313 "duration" : 3.6271186440678207,
4314 "fadeIn" : 0,
4315 "fadeOut" : 0,
4316 "id" : "43AEF5F0-29EF-45AD-A7B6-893C574685CB",
4317 "kind" : "audio",
4318 "linkId" : "596C8323-127D-44C1-B319-7DF1FB8B1171",
4319 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
4320 "muted" : false,
4321 "newShot" : false,
4322 "speed" : 1,
4323 "srcIn" : 1037.7933132364665,
4324 "start" : 341.2881355932203,
4325 "track" : "v2"
4326 },
4327 {
4328 "duration" : 3.6271186440678207,
4329 "fadeIn" : 0,
4330 "fadeOut" : 0,
4331 "id" : "32BC4FE2-4816-49E7-80A0-2DA08DCC56A0",
4332 "kind" : "video",
4333 "linkId" : "596C8323-127D-44C1-B319-7DF1FB8B1171",
4334 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
4335 "muted" : false,
4336 "newShot" : false,
4337 "speed" : 1,
4338 "srcIn" : 1037.6567061002384,
4339 "start" : 341.2881355932203,
4340 "track" : "v3"
4341 },
4342 {
4343 "duration" : 3.6271186440678207,
4344 "fadeIn" : 0,
4345 "fadeOut" : 0,
4346 "id" : "7C5CF291-4A7F-4BBD-809E-A9094B0B00E0",
4347 "kind" : "video",
4348 "linkId" : "596C8323-127D-44C1-B319-7DF1FB8B1171",
4349 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
4350 "muted" : false,
4351 "newShot" : false,
4352 "speed" : 1,
4353 "srcIn" : 1037.6567015162395,
4354 "start" : 341.2881355932203,
4355 "track" : "v4"
4356 },
4357 {
4358 "duration" : 1.355932203389841,
4359 "fadeIn" : 0,
4360 "fadeOut" : 0,
4361 "id" : "0905362C-DE86-4A3A-B256-9FD740D3A19D",
4362 "kind" : "video",
4363 "linkId" : "E5D49AA9-6CD5-438D-8B96-6ADA7002033B",
4364 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
4365 "muted" : false,
4366 "newShot" : false,
4367 "speed" : 1,
4368 "srcIn" : 1122.203389830509,
4369 "start" : 344.91525423728814,
4370 "track" : "v0"
4371 },
4372 {
4373 "duration" : 1.355932203389841,
4374 "fadeIn" : 0,
4375 "fadeOut" : 0,
4376 "id" : "48C76EE1-E83E-45E8-A767-47E35F66576F",
4377 "kind" : "audio",
4378 "linkId" : "E5D49AA9-6CD5-438D-8B96-6ADA7002033B",
4379 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
4380 "muted" : true,
4381 "newShot" : false,
4382 "speed" : 1,
4383 "srcIn" : 1122.119518289168,
4384 "start" : 344.91525423728814,
4385 "track" : "v1"
4386 },
4387 {
4388 "duration" : 1.355932203389841,
4389 "fadeIn" : 0,
4390 "fadeOut" : 0,
4391 "id" : "2F82F324-00A9-48EC-BF8C-DAA85B8C9D73",
4392 "kind" : "audio",
4393 "linkId" : "E5D49AA9-6CD5-438D-8B96-6ADA7002033B",
4394 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
4395 "muted" : false,
4396 "newShot" : false,
4397 "speed" : 1,
4398 "srcIn" : 1122.1661945923988,
4399 "start" : 344.91525423728814,
4400 "track" : "v2"
4401 },
4402 {
4403 "duration" : 1.355932203389841,
4404 "fadeIn" : 0,
4405 "fadeOut" : 0,
4406 "id" : "5B27AC9E-88CF-4AB2-927D-A79412E95F09",
4407 "kind" : "video",
4408 "linkId" : "E5D49AA9-6CD5-438D-8B96-6ADA7002033B",
4409 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
4410 "muted" : false,
4411 "newShot" : false,
4412 "speed" : 1,
4413 "srcIn" : 1122.0295874561707,
4414 "start" : 344.91525423728814,
4415 "track" : "v3"
4416 },
4417 {
4418 "duration" : 1.355932203389841,
4419 "fadeIn" : 0,
4420 "fadeOut" : 0,
4421 "id" : "6CC1FC2E-3809-4A37-87B2-3255B1430A3C",
4422 "kind" : "video",
4423 "linkId" : "E5D49AA9-6CD5-438D-8B96-6ADA7002033B",
4424 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
4425 "muted" : false,
4426 "newShot" : false,
4427 "speed" : 1,
4428 "srcIn" : 1122.0295828721719,
4429 "start" : 344.91525423728814,
4430 "track" : "v4"
4431 },
4432 {
4433 "duration" : 5.0847457627118615,
4434 "fadeIn" : 0,
4435 "fadeOut" : 0,
4436 "id" : "F27B9C51-75DE-4E4F-8808-3BA889863CF9",
4437 "kind" : "video",
4438 "linkId" : "81F5C3C3-3A90-4A30-8056-87972464F37F",
4439 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
4440 "muted" : false,
4441 "newShot" : false,
4442 "speed" : 1,
4443 "srcIn" : 1129.254237288136,
4444 "start" : 346.271186440678,
4445 "track" : "v0"
4446 },
4447 {
4448 "duration" : 5.0847457627118615,
4449 "fadeIn" : 0,
4450 "fadeOut" : 0,
4451 "id" : "D6CB9F93-3A96-4AD3-8448-52E45B33A28D",
4452 "kind" : "audio",
4453 "linkId" : "81F5C3C3-3A90-4A30-8056-87972464F37F",
4454 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
4455 "muted" : true,
4456 "newShot" : false,
4457 "speed" : 1,
4458 "srcIn" : 1129.170365746795,
4459 "start" : 346.271186440678,
4460 "track" : "v1"
4461 },
4462 {
4463 "duration" : 5.0847457627118615,
4464 "fadeIn" : 0,
4465 "fadeOut" : 0,
4466 "id" : "34239B0B-3D34-4D29-9C48-D18A775FCDDB",
4467 "kind" : "audio",
4468 "linkId" : "81F5C3C3-3A90-4A30-8056-87972464F37F",
4469 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
4470 "muted" : false,
4471 "newShot" : false,
4472 "speed" : 1,
4473 "srcIn" : 1129.2170420500258,
4474 "start" : 346.271186440678,
4475 "track" : "v2"
4476 },
4477 {
4478 "duration" : 5.0847457627118615,
4479 "fadeIn" : 0,
4480 "fadeOut" : 0,
4481 "id" : "C0EC3471-CC80-448C-BA8B-6F565ED9EE06",
4482 "kind" : "video",
4483 "linkId" : "81F5C3C3-3A90-4A30-8056-87972464F37F",
4484 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
4485 "muted" : false,
4486 "newShot" : false,
4487 "speed" : 1,
4488 "srcIn" : 1129.0804349137977,
4489 "start" : 346.271186440678,
4490 "track" : "v3"
4491 },
4492 {
4493 "duration" : 5.0847457627118615,
4494 "fadeIn" : 0,
4495 "fadeOut" : 0,
4496 "id" : "89A2AE13-B7C0-4138-B475-2D7ED7D1BEEA",
4497 "kind" : "video",
4498 "linkId" : "81F5C3C3-3A90-4A30-8056-87972464F37F",
4499 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
4500 "muted" : false,
4501 "newShot" : false,
4502 "speed" : 1,
4503 "srcIn" : 1129.080430329799,
4504 "start" : 346.271186440678,
4505 "track" : "v4"
4506 },
4507 {
4508 "duration" : 4.711864406779625,
4509 "fadeIn" : 0,
4510 "fadeOut" : 0,
4511 "id" : "2E892202-DFC8-4722-848D-8ED956667E0D",
4512 "kind" : "video",
4513 "linkId" : "CD0169DB-72F4-4E7A-B27D-1D1C0A2437D9",
4514 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
4515 "muted" : false,
4516 "newShot" : false,
4517 "speed" : 1,
4518 "srcIn" : 1278.2372881355936,
4519 "start" : 351.35593220338984,
4520 "track" : "v0"
4521 },
4522 {
4523 "duration" : 4.711864406779625,
4524 "fadeIn" : 0,
4525 "fadeOut" : 0,
4526 "id" : "43D45A86-49A1-449A-9E77-900F5D0ACC61",
4527 "kind" : "audio",
4528 "linkId" : "CD0169DB-72F4-4E7A-B27D-1D1C0A2437D9",
4529 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
4530 "muted" : true,
4531 "newShot" : false,
4532 "speed" : 1,
4533 "srcIn" : 1278.1534165942526,
4534 "start" : 351.35593220338984,
4535 "track" : "v1"
4536 },
4537 {
4538 "duration" : 4.711864406779625,
4539 "fadeIn" : 0,
4540 "fadeOut" : 0,
4541 "id" : "9B4179AA-3FE3-4B93-B578-86F7BDFBAC59",
4542 "kind" : "audio",
4543 "linkId" : "CD0169DB-72F4-4E7A-B27D-1D1C0A2437D9",
4544 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
4545 "muted" : false,
4546 "newShot" : false,
4547 "speed" : 1,
4548 "srcIn" : 1278.2000928974835,
4549 "start" : 351.35593220338984,
4550 "track" : "v2"
4551 },
4552 {
4553 "duration" : 4.711864406779625,
4554 "fadeIn" : 0,
4555 "fadeOut" : 0,
4556 "id" : "84160650-3975-445E-9CCE-FE9A4B354DE4",
4557 "kind" : "video",
4558 "linkId" : "CD0169DB-72F4-4E7A-B27D-1D1C0A2437D9",
4559 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
4560 "muted" : false,
4561 "newShot" : false,
4562 "speed" : 1,
4563 "srcIn" : 1278.0634857612554,
4564 "start" : 351.35593220338984,
4565 "track" : "v3"
4566 },
4567 {
4568 "duration" : 4.711864406779625,
4569 "fadeIn" : 0,
4570 "fadeOut" : 0,
4571 "id" : "EEE0A850-F637-47A2-A351-78A5E35E2E71",
4572 "kind" : "video",
4573 "linkId" : "CD0169DB-72F4-4E7A-B27D-1D1C0A2437D9",
4574 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
4575 "muted" : false,
4576 "newShot" : false,
4577 "speed" : 1,
4578 "srcIn" : 1278.0634811772566,
4579 "start" : 351.35593220338984,
4580 "track" : "v4"
4581 },
4582 {
4583 "duration" : 12.27118644067798,
4584 "fadeIn" : 0,
4585 "fadeOut" : 0,
4586 "id" : "40E92BD1-0CD5-4846-B649-57DEE758C62A",
4587 "kind" : "video",
4588 "linkId" : "7D4C2DFB-52DA-48A5-B7A5-8C50DE99C254",
4589 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
4590 "muted" : false,
4591 "newShot" : false,
4592 "speed" : 1,
4593 "srcIn" : 1283.4237288135596,
4594 "start" : 356.06779661016947,
4595 "track" : "v0"
4596 },
4597 {
4598 "duration" : 12.27118644067798,
4599 "fadeIn" : 0,
4600 "fadeOut" : 0,
4601 "id" : "A9E11104-5BA1-47B5-A29B-0DB9D895655D",
4602 "kind" : "audio",
4603 "linkId" : "7D4C2DFB-52DA-48A5-B7A5-8C50DE99C254",
4604 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
4605 "muted" : true,
4606 "newShot" : false,
4607 "speed" : 1,
4608 "srcIn" : 1283.3398572722185,
4609 "start" : 356.06779661016947,
4610 "track" : "v1"
4611 },
4612 {
4613 "duration" : 12.27118644067798,
4614 "fadeIn" : 0,
4615 "fadeOut" : 0,
4616 "id" : "EB6EE9AD-7386-4D10-BFBF-E253B17F3431",
4617 "kind" : "audio",
4618 "linkId" : "7D4C2DFB-52DA-48A5-B7A5-8C50DE99C254",
4619 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
4620 "muted" : false,
4621 "newShot" : false,
4622 "speed" : 1,
4623 "srcIn" : 1283.3865335754494,
4624 "start" : 356.06779661016947,
4625 "track" : "v2"
4626 },
4627 {
4628 "duration" : 12.27118644067798,
4629 "fadeIn" : 0,
4630 "fadeOut" : 0,
4631 "id" : "438EA227-D2C5-4532-9A3A-252007721608",
4632 "kind" : "video",
4633 "linkId" : "7D4C2DFB-52DA-48A5-B7A5-8C50DE99C254",
4634 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
4635 "muted" : false,
4636 "newShot" : false,
4637 "speed" : 1,
4638 "srcIn" : 1283.2499264392213,
4639 "start" : 356.06779661016947,
4640 "track" : "v3"
4641 },
4642 {
4643 "duration" : 12.27118644067798,
4644 "fadeIn" : 0,
4645 "fadeOut" : 0,
4646 "id" : "E7E7A787-8710-4FC3-8D04-8FB4A9808A63",
4647 "kind" : "video",
4648 "linkId" : "7D4C2DFB-52DA-48A5-B7A5-8C50DE99C254",
4649 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
4650 "muted" : false,
4651 "newShot" : false,
4652 "speed" : 1,
4653 "srcIn" : 1283.2499218552225,
4654 "start" : 356.06779661016947,
4655 "track" : "v4"
4656 },
4657 {
4658 "duration" : 3.0508474576271283,
4659 "fadeIn" : 0,
4660 "fadeOut" : 0,
4661 "id" : "65671782-CE0B-49E2-AF2D-C5D2D41FFA24",
4662 "kind" : "video",
4663 "linkId" : "D50E12ED-A0DF-4078-95DE-6D5332CDFB62",
4664 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
4665 "muted" : false,
4666 "newShot" : false,
4667 "speed" : 1,
4668 "srcIn" : 1304.271186440678,
4669 "start" : 368.33898305084745,
4670 "track" : "v0"
4671 },
4672 {
4673 "duration" : 3.0508474576271283,
4674 "fadeIn" : 0,
4675 "fadeOut" : 0,
4676 "id" : "B250A8E7-5DB5-463B-BC7E-2A985C554A48",
4677 "kind" : "audio",
4678 "linkId" : "D50E12ED-A0DF-4078-95DE-6D5332CDFB62",
4679 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
4680 "muted" : true,
4681 "newShot" : false,
4682 "speed" : 1,
4683 "srcIn" : 1304.187314899337,
4684 "start" : 368.33898305084745,
4685 "track" : "v1"
4686 },
4687 {
4688 "duration" : 3.0508474576271283,
4689 "fadeIn" : 0,
4690 "fadeOut" : 0,
4691 "id" : "538F899C-5E20-49EC-8416-74178778FE07",
4692 "kind" : "audio",
4693 "linkId" : "D50E12ED-A0DF-4078-95DE-6D5332CDFB62",
4694 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
4695 "muted" : false,
4696 "newShot" : false,
4697 "speed" : 1,
4698 "srcIn" : 1304.233991202568,
4699 "start" : 368.33898305084745,
4700 "track" : "v2"
4701 },
4702 {
4703 "duration" : 3.0508474576271283,
4704 "fadeIn" : 0,
4705 "fadeOut" : 0,
4706 "id" : "AB06969E-2217-4DDA-B6A6-0537DB89EDD0",
4707 "kind" : "video",
4708 "linkId" : "D50E12ED-A0DF-4078-95DE-6D5332CDFB62",
4709 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
4710 "muted" : false,
4711 "newShot" : false,
4712 "speed" : 1,
4713 "srcIn" : 1304.0973840663398,
4714 "start" : 368.33898305084745,
4715 "track" : "v3"
4716 },
4717 {
4718 "duration" : 3.0508474576271283,
4719 "fadeIn" : 0,
4720 "fadeOut" : 0,
4721 "id" : "84921634-8C3A-4FDC-9737-68D94154D037",
4722 "kind" : "video",
4723 "linkId" : "D50E12ED-A0DF-4078-95DE-6D5332CDFB62",
4724 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
4725 "muted" : false,
4726 "newShot" : false,
4727 "speed" : 1,
4728 "srcIn" : 1304.097379482341,
4729 "start" : 368.33898305084745,
4730 "track" : "v4"
4731 },
4732 {
4733 "duration" : 6.610169491525426,
4734 "fadeIn" : 0,
4735 "fadeOut" : 0,
4736 "id" : "B2E2127E-A4B7-4682-BC09-FE4FBF53EC66",
4737 "kind" : "video",
4738 "linkId" : "4A3BE2AA-0118-46FB-9311-40602A8063DF",
4739 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
4740 "muted" : false,
4741 "newShot" : false,
4742 "speed" : 1,
4743 "srcIn" : 1309.6610169491526,
4744 "start" : 371.3898305084746,
4745 "track" : "v0"
4746 },
4747 {
4748 "duration" : 6.610169491525426,
4749 "fadeIn" : 0,
4750 "fadeOut" : 0,
4751 "id" : "A2F9926B-983E-4F53-99C7-92BB9B023122",
4752 "kind" : "audio",
4753 "linkId" : "4A3BE2AA-0118-46FB-9311-40602A8063DF",
4754 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
4755 "muted" : true,
4756 "newShot" : false,
4757 "speed" : 1,
4758 "srcIn" : 1309.5771454078115,
4759 "start" : 371.3898305084746,
4760 "track" : "v1"
4761 },
4762 {
4763 "duration" : 6.610169491525426,
4764 "fadeIn" : 0,
4765 "fadeOut" : 0,
4766 "id" : "D13D2198-79F9-4915-A5E0-AC690ADBC371",
4767 "kind" : "audio",
4768 "linkId" : "4A3BE2AA-0118-46FB-9311-40602A8063DF",
4769 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
4770 "muted" : false,
4771 "newShot" : false,
4772 "speed" : 1,
4773 "srcIn" : 1309.6238217110424,
4774 "start" : 371.3898305084746,
4775 "track" : "v2"
4776 },
4777 {
4778 "duration" : 6.610169491525426,
4779 "fadeIn" : 0,
4780 "fadeOut" : 0,
4781 "id" : "53651C3A-4919-49F0-ADED-34CDF520F07E",
4782 "kind" : "video",
4783 "linkId" : "4A3BE2AA-0118-46FB-9311-40602A8063DF",
4784 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
4785 "muted" : false,
4786 "newShot" : false,
4787 "speed" : 1,
4788 "srcIn" : 1309.4872145748143,
4789 "start" : 371.3898305084746,
4790 "track" : "v3"
4791 },
4792 {
4793 "duration" : 6.610169491525426,
4794 "fadeIn" : 0,
4795 "fadeOut" : 0,
4796 "id" : "00F70279-9C4E-40AB-8D35-B686234D9CD2",
4797 "kind" : "video",
4798 "linkId" : "4A3BE2AA-0118-46FB-9311-40602A8063DF",
4799 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
4800 "muted" : false,
4801 "newShot" : false,
4802 "speed" : 1,
4803 "srcIn" : 1309.4872099908155,
4804 "start" : 371.3898305084746,
4805 "track" : "v4"
4806 },
4807 {
4808 "duration" : 6.372881355932179,
4809 "fadeIn" : 0,
4810 "fadeOut" : 0,
4811 "id" : "3EB22ECC-5EAD-4107-9F24-6864C9464EAC",
4812 "kind" : "video",
4813 "linkId" : "346B7025-BD91-4339-98B4-DBE997BC1A27",
4814 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
4815 "muted" : false,
4816 "newShot" : false,
4817 "speed" : 1,
4818 "srcIn" : 1319.1186440677966,
4819 "start" : 378,
4820 "track" : "v0"
4821 },
4822 {
4823 "duration" : 6.372881355932179,
4824 "fadeIn" : 0,
4825 "fadeOut" : 0,
4826 "id" : "2B2021EE-7138-45C6-A0CF-0D8BBAC1116A",
4827 "kind" : "audio",
4828 "linkId" : "346B7025-BD91-4339-98B4-DBE997BC1A27",
4829 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
4830 "muted" : true,
4831 "newShot" : false,
4832 "speed" : 1,
4833 "srcIn" : 1319.0347725264555,
4834 "start" : 378,
4835 "track" : "v1"
4836 },
4837 {
4838 "duration" : 6.372881355932179,
4839 "fadeIn" : 0,
4840 "fadeOut" : 0,
4841 "id" : "4A8A3D03-F2FB-4651-A609-318676737F28",
4842 "kind" : "audio",
4843 "linkId" : "346B7025-BD91-4339-98B4-DBE997BC1A27",
4844 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
4845 "muted" : false,
4846 "newShot" : false,
4847 "speed" : 1,
4848 "srcIn" : 1319.0814488296865,
4849 "start" : 378,
4850 "track" : "v2"
4851 },
4852 {
4853 "duration" : 6.372881355932179,
4854 "fadeIn" : 0,
4855 "fadeOut" : 0,
4856 "id" : "196CB0CE-61ED-4852-A694-AB8B592CF510",
4857 "kind" : "video",
4858 "linkId" : "346B7025-BD91-4339-98B4-DBE997BC1A27",
4859 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
4860 "muted" : false,
4861 "newShot" : false,
4862 "speed" : 1,
4863 "srcIn" : 1318.9448416934583,
4864 "start" : 378,
4865 "track" : "v3"
4866 },
4867 {
4868 "duration" : 6.372881355932179,
4869 "fadeIn" : 0,
4870 "fadeOut" : 0,
4871 "id" : "AB9E730D-3DB3-430E-92B1-8528951D9146",
4872 "kind" : "video",
4873 "linkId" : "346B7025-BD91-4339-98B4-DBE997BC1A27",
4874 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
4875 "muted" : false,
4876 "newShot" : false,
4877 "speed" : 1,
4878 "srcIn" : 1318.9448371094595,
4879 "start" : 378,
4880 "track" : "v4"
4881 },
4882 {
4883 "duration" : 10.983050847457662,
4884 "fadeIn" : 0,
4885 "fadeOut" : 0,
4886 "id" : "59AB16D3-EBCD-438B-8B8C-88413E1E8D67",
4887 "kind" : "video",
4888 "linkId" : "8A7CFF6A-2FE9-4C3F-813E-9DDC903D1C26",
4889 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
4890 "muted" : false,
4891 "newShot" : false,
4892 "speed" : 1,
4893 "srcIn" : 1327.1186440677966,
4894 "start" : 384.3728813559322,
4895 "track" : "v0"
4896 },
4897 {
4898 "duration" : 10.983050847457662,
4899 "fadeIn" : 0,
4900 "fadeOut" : 0,
4901 "id" : "77501D06-4929-403D-8FCB-6E79E9A296A4",
4902 "kind" : "audio",
4903 "linkId" : "8A7CFF6A-2FE9-4C3F-813E-9DDC903D1C26",
4904 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
4905 "muted" : true,
4906 "newShot" : false,
4907 "speed" : 1,
4908 "srcIn" : 1327.0347725264555,
4909 "start" : 384.3728813559322,
4910 "track" : "v1"
4911 },
4912 {
4913 "duration" : 10.983050847457662,
4914 "fadeIn" : 0,
4915 "fadeOut" : 0,
4916 "id" : "A8B5D5C7-311F-4D71-BD19-7B1BAC7B047E",
4917 "kind" : "audio",
4918 "linkId" : "8A7CFF6A-2FE9-4C3F-813E-9DDC903D1C26",
4919 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
4920 "muted" : false,
4921 "newShot" : false,
4922 "speed" : 1,
4923 "srcIn" : 1327.0814488296865,
4924 "start" : 384.3728813559322,
4925 "track" : "v2"
4926 },
4927 {
4928 "duration" : 10.983050847457662,
4929 "fadeIn" : 0,
4930 "fadeOut" : 0,
4931 "id" : "F9DB0D5E-9113-4D5E-81AB-EAE9AF22C7FE",
4932 "kind" : "video",
4933 "linkId" : "8A7CFF6A-2FE9-4C3F-813E-9DDC903D1C26",
4934 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
4935 "muted" : false,
4936 "newShot" : false,
4937 "speed" : 1,
4938 "srcIn" : 1326.9448416934583,
4939 "start" : 384.3728813559322,
4940 "track" : "v3"
4941 },
4942 {
4943 "duration" : 10.983050847457662,
4944 "fadeIn" : 0,
4945 "fadeOut" : 0,
4946 "id" : "D4A49349-31F4-41F7-91B5-682C810269F4",
4947 "kind" : "video",
4948 "linkId" : "8A7CFF6A-2FE9-4C3F-813E-9DDC903D1C26",
4949 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
4950 "muted" : false,
4951 "newShot" : false,
4952 "speed" : 1,
4953 "srcIn" : 1326.9448371094595,
4954 "start" : 384.3728813559322,
4955 "track" : "v4"
4956 },
4957 {
4958 "duration" : 14.169491525423723,
4959 "fadeIn" : 0,
4960 "fadeOut" : 0,
4961 "id" : "3BEF9E6B-7901-407C-BEA3-7AFAB2A27DF5",
4962 "kind" : "video",
4963 "linkId" : "704CF8B0-3041-4053-9080-715DFE6B2D25",
4964 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
4965 "muted" : false,
4966 "newShot" : false,
4967 "speed" : 1,
4968 "srcIn" : 1389.0169491525423,
4969 "start" : 395.35593220338984,
4970 "track" : "v0"
4971 },
4972 {
4973 "duration" : 14.169491525423723,
4974 "fadeIn" : 0,
4975 "fadeOut" : 0,
4976 "id" : "BC1D4D97-B973-4693-9B24-5823A5192FAA",
4977 "kind" : "audio",
4978 "linkId" : "704CF8B0-3041-4053-9080-715DFE6B2D25",
4979 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
4980 "muted" : true,
4981 "newShot" : false,
4982 "speed" : 1,
4983 "srcIn" : 1388.9330776112013,
4984 "start" : 395.35593220338984,
4985 "track" : "v1"
4986 },
4987 {
4988 "duration" : 14.169491525423723,
4989 "fadeIn" : 0,
4990 "fadeOut" : 0,
4991 "id" : "DBBCAF21-55F8-44F0-9F8A-4EA556B8EB57",
4992 "kind" : "audio",
4993 "linkId" : "704CF8B0-3041-4053-9080-715DFE6B2D25",
4994 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
4995 "muted" : false,
4996 "newShot" : false,
4997 "speed" : 1,
4998 "srcIn" : 1388.9797539144322,
4999 "start" : 395.35593220338984,
5000 "track" : "v2"
5001 },
5002 {
5003 "duration" : 14.169491525423723,
5004 "fadeIn" : 0,
5005 "fadeOut" : 0,
5006 "id" : "6C7A0EB2-88FC-4404-B48D-AA7FE6269170",
5007 "kind" : "video",
5008 "linkId" : "704CF8B0-3041-4053-9080-715DFE6B2D25",
5009 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
5010 "muted" : false,
5011 "newShot" : false,
5012 "speed" : 1,
5013 "srcIn" : 1388.843146778204,
5014 "start" : 395.35593220338984,
5015 "track" : "v3"
5016 },
5017 {
5018 "duration" : 14.169491525423723,
5019 "fadeIn" : 0,
5020 "fadeOut" : 0,
5021 "id" : "50068A9B-E7CF-4DD9-9917-7155FEEE9EA2",
5022 "kind" : "video",
5023 "linkId" : "704CF8B0-3041-4053-9080-715DFE6B2D25",
5024 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
5025 "muted" : false,
5026 "newShot" : false,
5027 "speed" : 1,
5028 "srcIn" : 1388.8431421942053,
5029 "start" : 395.35593220338984,
5030 "track" : "v4"
5031 },
5032 {
5033 "duration" : 12.610169491525426,
5034 "fadeIn" : 0,
5035 "fadeOut" : 0,
5036 "id" : "EA7CF47C-E4D3-437C-B0E5-4275167856C5",
5037 "kind" : "video",
5038 "linkId" : "5E30C5A7-ECB1-41A5-9456-2F737C45E0C1",
5039 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
5040 "muted" : false,
5041 "newShot" : false,
5042 "speed" : 1,
5043 "srcIn" : 1406.5762711864404,
5044 "start" : 409.52542372881356,
5045 "track" : "v0"
5046 },
5047 {
5048 "duration" : 12.610169491525426,
5049 "fadeIn" : 0,
5050 "fadeOut" : 0,
5051 "id" : "1E9A4B81-6D6C-4CEF-BCD9-A56D14BC12E5",
5052 "kind" : "audio",
5053 "linkId" : "5E30C5A7-ECB1-41A5-9456-2F737C45E0C1",
5054 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
5055 "muted" : true,
5056 "newShot" : false,
5057 "speed" : 1,
5058 "srcIn" : 1406.4923996450993,
5059 "start" : 409.52542372881356,
5060 "track" : "v1"
5061 },
5062 {
5063 "duration" : 12.610169491525426,
5064 "fadeIn" : 0,
5065 "fadeOut" : 0,
5066 "id" : "1506A648-D35B-4619-A082-06FF4E3CBA5B",
5067 "kind" : "audio",
5068 "linkId" : "5E30C5A7-ECB1-41A5-9456-2F737C45E0C1",
5069 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
5070 "muted" : false,
5071 "newShot" : false,
5072 "speed" : 1,
5073 "srcIn" : 1406.5390759483303,
5074 "start" : 409.52542372881356,
5075 "track" : "v2"
5076 },
5077 {
5078 "duration" : 12.610169491525426,
5079 "fadeIn" : 0,
5080 "fadeOut" : 0,
5081 "id" : "9D08D70B-5627-48C7-873D-A8153FA5DA0E",
5082 "kind" : "video",
5083 "linkId" : "5E30C5A7-ECB1-41A5-9456-2F737C45E0C1",
5084 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
5085 "muted" : false,
5086 "newShot" : false,
5087 "speed" : 1,
5088 "srcIn" : 1406.4024688121021,
5089 "start" : 409.52542372881356,
5090 "track" : "v3"
5091 },
5092 {
5093 "duration" : 12.610169491525426,
5094 "fadeIn" : 0,
5095 "fadeOut" : 0,
5096 "id" : "A6181FAE-7AFB-4D19-B593-91540501DC66",
5097 "kind" : "video",
5098 "linkId" : "5E30C5A7-ECB1-41A5-9456-2F737C45E0C1",
5099 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
5100 "muted" : false,
5101 "newShot" : false,
5102 "speed" : 1,
5103 "srcIn" : 1406.4024642281033,
5104 "start" : 409.52542372881356,
5105 "track" : "v4"
5106 },
5107 {
5108 "duration" : 12.13559322033899,
5109 "fadeIn" : 0,
5110 "fadeOut" : 0,
5111 "id" : "4E3F3515-7F7B-447C-BC9F-E4220A2BA2A2",
5112 "kind" : "video",
5113 "linkId" : "226C31FD-B8DF-4F4A-A67B-DCB285B8B836",
5114 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
5115 "muted" : false,
5116 "newShot" : false,
5117 "speed" : 1,
5118 "srcIn" : 1434.1694915254236,
5119 "start" : 428.271186440678,
5120 "track" : "v0"
5121 },
5122 {
5123 "duration" : 12.13559322033899,
5124 "fadeIn" : 0,
5125 "fadeOut" : 0,
5126 "id" : "635A9B0E-7D5C-4F08-BEB1-0D43CEB95071",
5127 "kind" : "audio",
5128 "linkId" : "226C31FD-B8DF-4F4A-A67B-DCB285B8B836",
5129 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
5130 "muted" : true,
5131 "newShot" : false,
5132 "speed" : 1,
5133 "srcIn" : 1434.0856199840825,
5134 "start" : 428.271186440678,
5135 "track" : "v1"
5136 },
5137 {
5138 "duration" : 12.13559322033899,
5139 "fadeIn" : 0,
5140 "fadeOut" : 0,
5141 "id" : "B26C43CA-9142-4FF8-8DE6-5FB3CF02701F",
5142 "kind" : "audio",
5143 "linkId" : "226C31FD-B8DF-4F4A-A67B-DCB285B8B836",
5144 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
5145 "muted" : false,
5146 "newShot" : false,
5147 "speed" : 1,
5148 "srcIn" : 1434.1322962873135,
5149 "start" : 428.271186440678,
5150 "track" : "v2"
5151 },
5152 {
5153 "duration" : 12.13559322033899,
5154 "fadeIn" : 0,
5155 "fadeOut" : 0,
5156 "id" : "1D681666-828B-4516-905F-70C0B9927714",
5157 "kind" : "video",
5158 "linkId" : "226C31FD-B8DF-4F4A-A67B-DCB285B8B836",
5159 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
5160 "muted" : false,
5161 "newShot" : false,
5162 "speed" : 1,
5163 "srcIn" : 1433.9956891510853,
5164 "start" : 428.271186440678,
5165 "track" : "v3"
5166 },
5167 {
5168 "duration" : 12.13559322033899,
5169 "fadeIn" : 0,
5170 "fadeOut" : 0,
5171 "id" : "14FFF5E0-4F74-4347-AEC2-714B77823B90",
5172 "kind" : "video",
5173 "linkId" : "226C31FD-B8DF-4F4A-A67B-DCB285B8B836",
5174 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
5175 "muted" : false,
5176 "newShot" : false,
5177 "speed" : 1,
5178 "srcIn" : 1433.9956845670865,
5179 "start" : 428.271186440678,
5180 "track" : "v4"
5181 },
5182 {
5183 "duration" : 6.13559322033899,
5184 "fadeIn" : 0,
5185 "fadeOut" : 0,
5186 "id" : "651982BC-F62C-433E-9CA6-7F53FDD18AD2",
5187 "kind" : "video",
5188 "linkId" : "985611EC-FA7E-4535-BFEC-AECD319001B6",
5189 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
5190 "muted" : false,
5191 "newShot" : false,
5192 "speed" : 1,
5193 "srcIn" : 1428.0338983050847,
5194 "start" : 422.135593220339,
5195 "track" : "v0"
5196 },
5197 {
5198 "duration" : 6.13559322033899,
5199 "fadeIn" : 0,
5200 "fadeOut" : 0,
5201 "id" : "011DD52B-7DF9-4AB2-9143-9249F293A98C",
5202 "kind" : "audio",
5203 "linkId" : "985611EC-FA7E-4535-BFEC-AECD319001B6",
5204 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
5205 "muted" : true,
5206 "newShot" : false,
5207 "speed" : 1,
5208 "srcIn" : 1427.9500267637436,
5209 "start" : 422.135593220339,
5210 "track" : "v1"
5211 },
5212 {
5213 "duration" : 6.13559322033899,
5214 "fadeIn" : 0,
5215 "fadeOut" : 0,
5216 "id" : "9C6C77CE-A577-41A8-AF62-5B1AD22511CB",
5217 "kind" : "audio",
5218 "linkId" : "985611EC-FA7E-4535-BFEC-AECD319001B6",
5219 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
5220 "muted" : false,
5221 "newShot" : false,
5222 "speed" : 1,
5223 "srcIn" : 1427.9967030669745,
5224 "start" : 422.135593220339,
5225 "track" : "v2"
5226 },
5227 {
5228 "duration" : 6.13559322033899,
5229 "fadeIn" : 0,
5230 "fadeOut" : 0,
5231 "id" : "81445B0A-E2DC-4623-B61E-F78297F1AF9B",
5232 "kind" : "video",
5233 "linkId" : "985611EC-FA7E-4535-BFEC-AECD319001B6",
5234 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
5235 "muted" : false,
5236 "newShot" : false,
5237 "speed" : 1,
5238 "srcIn" : 1427.8600959307464,
5239 "start" : 422.135593220339,
5240 "track" : "v3"
5241 },
5242 {
5243 "duration" : 6.13559322033899,
5244 "fadeIn" : 0,
5245 "fadeOut" : 0,
5246 "id" : "2FCF79AA-A233-43B0-924A-F22316673337",
5247 "kind" : "video",
5248 "linkId" : "985611EC-FA7E-4535-BFEC-AECD319001B6",
5249 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
5250 "muted" : false,
5251 "newShot" : false,
5252 "speed" : 1,
5253 "srcIn" : 1427.8600913467476,
5254 "start" : 422.135593220339,
5255 "track" : "v4"
5256 },
5257 {
5258 "duration" : 9.45762711864404,
5259 "fadeIn" : 0,
5260 "fadeOut" : 0,
5261 "id" : "3226AEDC-71C8-4045-95AB-2938C4E153FF",
5262 "kind" : "video",
5263 "linkId" : "ABBC461D-6EED-494A-8B1A-7677A2E7D027",
5264 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
5265 "muted" : false,
5266 "newShot" : false,
5267 "speed" : 1,
5268 "srcIn" : 1449.559322033898,
5269 "start" : 440.40677966101697,
5270 "track" : "v0"
5271 },
5272 {
5273 "duration" : 9.45762711864404,
5274 "fadeIn" : 0,
5275 "fadeOut" : 0,
5276 "id" : "FAF956D0-C01F-4926-93F2-49FA34A04E13",
5277 "kind" : "audio",
5278 "linkId" : "ABBC461D-6EED-494A-8B1A-7677A2E7D027",
5279 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
5280 "muted" : true,
5281 "newShot" : false,
5282 "speed" : 1,
5283 "srcIn" : 1449.475450492557,
5284 "start" : 440.40677966101697,
5285 "track" : "v1"
5286 },
5287 {
5288 "duration" : 9.45762711864404,
5289 "fadeIn" : 0,
5290 "fadeOut" : 0,
5291 "id" : "3EB9B48B-CACE-4489-BAFE-59EB0F2E6305",
5292 "kind" : "audio",
5293 "linkId" : "ABBC461D-6EED-494A-8B1A-7677A2E7D027",
5294 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
5295 "muted" : false,
5296 "newShot" : false,
5297 "speed" : 1,
5298 "srcIn" : 1449.522126795788,
5299 "start" : 440.40677966101697,
5300 "track" : "v2"
5301 },
5302 {
5303 "duration" : 9.45762711864404,
5304 "fadeIn" : 0,
5305 "fadeOut" : 0,
5306 "id" : "4406B9FA-D633-49BC-BB25-FA2554A8FA6A",
5307 "kind" : "video",
5308 "linkId" : "ABBC461D-6EED-494A-8B1A-7677A2E7D027",
5309 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
5310 "muted" : false,
5311 "newShot" : false,
5312 "speed" : 1,
5313 "srcIn" : 1449.3855196595598,
5314 "start" : 440.40677966101697,
5315 "track" : "v3"
5316 },
5317 {
5318 "duration" : 9.45762711864404,
5319 "fadeIn" : 0,
5320 "fadeOut" : 0,
5321 "id" : "99E4D7B0-8E74-4871-B79C-5EE9AE03E424",
5322 "kind" : "video",
5323 "linkId" : "ABBC461D-6EED-494A-8B1A-7677A2E7D027",
5324 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
5325 "muted" : false,
5326 "newShot" : false,
5327 "speed" : 1,
5328 "srcIn" : 1449.385515075561,
5329 "start" : 440.40677966101697,
5330 "track" : "v4"
5331 },
5332 {
5333 "duration" : 6.677966101694949,
5334 "fadeIn" : 0,
5335 "fadeOut" : 0,
5336 "id" : "559361D5-9DB2-4B63-8E39-79DEB5DA658A",
5337 "kind" : "video",
5338 "linkId" : "F4E6AD2E-D33A-4B9D-9CC7-E85C4DF5F3C7",
5339 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
5340 "muted" : false,
5341 "newShot" : false,
5342 "speed" : 1,
5343 "srcIn" : 1462.64406779661,
5344 "start" : 449.864406779661,
5345 "track" : "v0"
5346 },
5347 {
5348 "duration" : 6.677966101694949,
5349 "fadeIn" : 0,
5350 "fadeOut" : 0,
5351 "id" : "37884883-822C-45CA-B6E1-133027A1718A",
5352 "kind" : "audio",
5353 "linkId" : "F4E6AD2E-D33A-4B9D-9CC7-E85C4DF5F3C7",
5354 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
5355 "muted" : true,
5356 "newShot" : false,
5357 "speed" : 1,
5358 "srcIn" : 1462.560196255269,
5359 "start" : 449.864406779661,
5360 "track" : "v1"
5361 },
5362 {
5363 "duration" : 6.677966101694949,
5364 "fadeIn" : 0,
5365 "fadeOut" : 0,
5366 "id" : "81675B52-F6A6-4C8A-997F-8AF4803A89E7",
5367 "kind" : "audio",
5368 "linkId" : "F4E6AD2E-D33A-4B9D-9CC7-E85C4DF5F3C7",
5369 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
5370 "muted" : false,
5371 "newShot" : false,
5372 "speed" : 1,
5373 "srcIn" : 1462.6068725584998,
5374 "start" : 449.864406779661,
5375 "track" : "v2"
5376 },
5377 {
5378 "duration" : 6.677966101694949,
5379 "fadeIn" : 0,
5380 "fadeOut" : 0,
5381 "id" : "4787230C-8C29-4CF7-854E-A3AB00759466",
5382 "kind" : "video",
5383 "linkId" : "F4E6AD2E-D33A-4B9D-9CC7-E85C4DF5F3C7",
5384 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
5385 "muted" : false,
5386 "newShot" : false,
5387 "speed" : 1,
5388 "srcIn" : 1462.4702654222717,
5389 "start" : 449.864406779661,
5390 "track" : "v3"
5391 },
5392 {
5393 "duration" : 6.677966101694949,
5394 "fadeIn" : 0,
5395 "fadeOut" : 0,
5396 "id" : "CDA54502-AAC3-496A-B251-B43F1F744E11",
5397 "kind" : "video",
5398 "linkId" : "F4E6AD2E-D33A-4B9D-9CC7-E85C4DF5F3C7",
5399 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
5400 "muted" : false,
5401 "newShot" : false,
5402 "speed" : 1,
5403 "srcIn" : 1462.470260838273,
5404 "start" : 449.864406779661,
5405 "track" : "v4"
5406 },
5407 {
5408 "duration" : 2.1016949152541997,
5409 "fadeIn" : 0,
5410 "fadeOut" : 0,
5411 "id" : "CEDDFF86-9747-4058-98DC-1EB7A4B1F572",
5412 "kind" : "video",
5413 "linkId" : "EDBE29AE-0AD9-452E-A93A-771CE2BE813D",
5414 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
5415 "muted" : false,
5416 "newShot" : false,
5417 "speed" : 1,
5418 "srcIn" : 1471.6610169491523,
5419 "start" : 456.54237288135596,
5420 "track" : "v0"
5421 },
5422 {
5423 "duration" : 2.1016949152541997,
5424 "fadeIn" : 0,
5425 "fadeOut" : 0,
5426 "id" : "65DBD942-E0B7-428A-B955-F7C74957C7E7",
5427 "kind" : "audio",
5428 "linkId" : "EDBE29AE-0AD9-452E-A93A-771CE2BE813D",
5429 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
5430 "muted" : true,
5431 "newShot" : false,
5432 "speed" : 1,
5433 "srcIn" : 1471.5771454078113,
5434 "start" : 456.54237288135596,
5435 "track" : "v1"
5436 },
5437 {
5438 "duration" : 2.1016949152541997,
5439 "fadeIn" : 0,
5440 "fadeOut" : 0,
5441 "id" : "F6898171-8F65-41C6-8518-DFF6DBBABC04",
5442 "kind" : "audio",
5443 "linkId" : "EDBE29AE-0AD9-452E-A93A-771CE2BE813D",
5444 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
5445 "muted" : false,
5446 "newShot" : false,
5447 "speed" : 1,
5448 "srcIn" : 1471.6238217110422,
5449 "start" : 456.54237288135596,
5450 "track" : "v2"
5451 },
5452 {
5453 "duration" : 2.1016949152541997,
5454 "fadeIn" : 0,
5455 "fadeOut" : 0,
5456 "id" : "D865CA10-5853-486F-82AD-02B05EE60EDB",
5457 "kind" : "video",
5458 "linkId" : "EDBE29AE-0AD9-452E-A93A-771CE2BE813D",
5459 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
5460 "muted" : false,
5461 "newShot" : false,
5462 "speed" : 1,
5463 "srcIn" : 1471.487214574814,
5464 "start" : 456.54237288135596,
5465 "track" : "v3"
5466 },
5467 {
5468 "duration" : 2.1016949152541997,
5469 "fadeIn" : 0,
5470 "fadeOut" : 0,
5471 "id" : "0616F857-62D2-44F6-A078-21F47E1CD066",
5472 "kind" : "video",
5473 "linkId" : "EDBE29AE-0AD9-452E-A93A-771CE2BE813D",
5474 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
5475 "muted" : false,
5476 "newShot" : false,
5477 "speed" : 1,
5478 "srcIn" : 1471.4872099908152,
5479 "start" : 456.54237288135596,
5480 "track" : "v4"
5481 },
5482 {
5483 "duration" : 8.101694915254257,
5484 "fadeIn" : 0,
5485 "fadeOut" : 0,
5486 "id" : "A734B468-A8AC-4E1B-BBFF-00BCD0D78390",
5487 "kind" : "video",
5488 "linkId" : "E9457961-D7DC-4D55-8C56-3E6D232AC821",
5489 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
5490 "muted" : false,
5491 "newShot" : false,
5492 "speed" : 1,
5493 "srcIn" : 1478.3389830508472,
5494 "start" : 458.64406779661016,
5495 "track" : "v0"
5496 },
5497 {
5498 "duration" : 8.101694915254257,
5499 "fadeIn" : 0,
5500 "fadeOut" : 0,
5501 "id" : "0B521685-7B77-48BB-B145-8FC0FC1B8D14",
5502 "kind" : "audio",
5503 "linkId" : "E9457961-D7DC-4D55-8C56-3E6D232AC821",
5504 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
5505 "muted" : true,
5506 "newShot" : false,
5507 "speed" : 1,
5508 "srcIn" : 1478.2551115095062,
5509 "start" : 458.64406779661016,
5510 "track" : "v1"
5511 },
5512 {
5513 "duration" : 8.101694915254257,
5514 "fadeIn" : 0,
5515 "fadeOut" : 0,
5516 "id" : "B79DCE82-D278-4E64-BF3E-DD1383DE9320",
5517 "kind" : "audio",
5518 "linkId" : "E9457961-D7DC-4D55-8C56-3E6D232AC821",
5519 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
5520 "muted" : false,
5521 "newShot" : false,
5522 "speed" : 1,
5523 "srcIn" : 1478.301787812737,
5524 "start" : 458.64406779661016,
5525 "track" : "v2"
5526 },
5527 {
5528 "duration" : 8.101694915254257,
5529 "fadeIn" : 0,
5530 "fadeOut" : 0,
5531 "id" : "C1373384-5CBC-48DA-AEEA-B1DC0A1277AB",
5532 "kind" : "video",
5533 "linkId" : "E9457961-D7DC-4D55-8C56-3E6D232AC821",
5534 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
5535 "muted" : false,
5536 "newShot" : false,
5537 "speed" : 1,
5538 "srcIn" : 1478.165180676509,
5539 "start" : 458.64406779661016,
5540 "track" : "v3"
5541 },
5542 {
5543 "duration" : 8.101694915254257,
5544 "fadeIn" : 0,
5545 "fadeOut" : 0,
5546 "id" : "8B3DE2A2-8DE0-408E-96C4-669DAE9AD4A9",
5547 "kind" : "video",
5548 "linkId" : "E9457961-D7DC-4D55-8C56-3E6D232AC821",
5549 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
5550 "muted" : false,
5551 "newShot" : false,
5552 "speed" : 1,
5553 "srcIn" : 1478.1651760925101,
5554 "start" : 458.64406779661016,
5555 "track" : "v4"
5556 },
5557 {
5558 "duration" : 2.13559322033899,
5559 "fadeIn" : 0,
5560 "fadeOut" : 0,
5561 "id" : "EFA97D54-119E-4829-9D03-E931F198D283",
5562 "kind" : "video",
5563 "linkId" : "8FAC32E3-47A3-49EC-A6FC-51E8FD39E2C8",
5564 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
5565 "muted" : false,
5566 "newShot" : false,
5567 "speed" : 1,
5568 "srcIn" : 1491.2203389830506,
5569 "start" : 466.7457627118644,
5570 "track" : "v0"
5571 },
5572 {
5573 "duration" : 2.13559322033899,
5574 "fadeIn" : 0,
5575 "fadeOut" : 0,
5576 "id" : "D10DB1AB-A596-41C4-99D6-2874E01C4666",
5577 "kind" : "audio",
5578 "linkId" : "8FAC32E3-47A3-49EC-A6FC-51E8FD39E2C8",
5579 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
5580 "muted" : true,
5581 "newShot" : false,
5582 "speed" : 1,
5583 "srcIn" : 1491.1364674417096,
5584 "start" : 466.7457627118644,
5585 "track" : "v1"
5586 },
5587 {
5588 "duration" : 2.13559322033899,
5589 "fadeIn" : 0,
5590 "fadeOut" : 0,
5591 "id" : "05FD9DBC-666B-4F86-98C5-B006D88468C8",
5592 "kind" : "audio",
5593 "linkId" : "8FAC32E3-47A3-49EC-A6FC-51E8FD39E2C8",
5594 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
5595 "muted" : false,
5596 "newShot" : false,
5597 "speed" : 1,
5598 "srcIn" : 1491.1831437449405,
5599 "start" : 466.7457627118644,
5600 "track" : "v2"
5601 },
5602 {
5603 "duration" : 2.13559322033899,
5604 "fadeIn" : 0,
5605 "fadeOut" : 0,
5606 "id" : "F8379F4E-716C-4AB7-A712-E90386ACAAF5",
5607 "kind" : "video",
5608 "linkId" : "8FAC32E3-47A3-49EC-A6FC-51E8FD39E2C8",
5609 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
5610 "muted" : false,
5611 "newShot" : false,
5612 "speed" : 1,
5613 "srcIn" : 1491.0465366087124,
5614 "start" : 466.7457627118644,
5615 "track" : "v3"
5616 },
5617 {
5618 "duration" : 2.13559322033899,
5619 "fadeIn" : 0,
5620 "fadeOut" : 0,
5621 "id" : "0AE01210-BDC5-4A27-A1D9-447FB98CBD8C",
5622 "kind" : "video",
5623 "linkId" : "8FAC32E3-47A3-49EC-A6FC-51E8FD39E2C8",
5624 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
5625 "muted" : false,
5626 "newShot" : false,
5627 "speed" : 1,
5628 "srcIn" : 1491.0465320247135,
5629 "start" : 466.7457627118644,
5630 "track" : "v4"
5631 },
5632 {
5633 "duration" : 12.13559322033899,
5634 "fadeIn" : 0,
5635 "fadeOut" : 0,
5636 "id" : "17C94894-CE9A-414A-99D9-983680DBF3C0",
5637 "kind" : "video",
5638 "linkId" : "BD0AA767-5E48-4364-B00E-41745729A942",
5639 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
5640 "muted" : false,
5641 "newShot" : false,
5642 "speed" : 1,
5643 "srcIn" : 1493.3559322033896,
5644 "start" : 468.8813559322034,
5645 "track" : "v0"
5646 },
5647 {
5648 "duration" : 12.13559322033899,
5649 "fadeIn" : 0,
5650 "fadeOut" : 0,
5651 "id" : "517FEEE5-722F-4AF0-9739-D6C4CBFF13F7",
5652 "kind" : "audio",
5653 "linkId" : "BD0AA767-5E48-4364-B00E-41745729A942",
5654 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
5655 "muted" : true,
5656 "newShot" : false,
5657 "speed" : 1,
5658 "srcIn" : 1493.2720606620485,
5659 "start" : 468.8813559322034,
5660 "track" : "v1"
5661 },
5662 {
5663 "duration" : 12.13559322033899,
5664 "fadeIn" : 0,
5665 "fadeOut" : 0,
5666 "id" : "66CDAA10-44F3-4B54-AE2E-76150ACC9CD0",
5667 "kind" : "audio",
5668 "linkId" : "BD0AA767-5E48-4364-B00E-41745729A942",
5669 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
5670 "muted" : false,
5671 "newShot" : false,
5672 "speed" : 1,
5673 "srcIn" : 1493.3187369652794,
5674 "start" : 468.8813559322034,
5675 "track" : "v2"
5676 },
5677 {
5678 "duration" : 12.13559322033899,
5679 "fadeIn" : 0,
5680 "fadeOut" : 0,
5681 "id" : "A6487CCD-4DF5-4DD5-BFE5-2536683FBC19",
5682 "kind" : "video",
5683 "linkId" : "BD0AA767-5E48-4364-B00E-41745729A942",
5684 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
5685 "muted" : false,
5686 "newShot" : false,
5687 "speed" : 1,
5688 "srcIn" : 1493.1821298290513,
5689 "start" : 468.8813559322034,
5690 "track" : "v3"
5691 },
5692 {
5693 "duration" : 12.13559322033899,
5694 "fadeIn" : 0,
5695 "fadeOut" : 0,
5696 "id" : "3E4AB4B8-236F-4408-8452-95DFA2ED6598",
5697 "kind" : "video",
5698 "linkId" : "BD0AA767-5E48-4364-B00E-41745729A942",
5699 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
5700 "muted" : false,
5701 "newShot" : false,
5702 "speed" : 1,
5703 "srcIn" : 1493.1821252450525,
5704 "start" : 468.8813559322034,
5705 "track" : "v4"
5706 },
5707 {
5708 "duration" : 2.7796610169491487,
5709 "fadeIn" : 0,
5710 "fadeOut" : 0,
5711 "id" : "D2B2B51E-C0F9-43FA-A96C-FF04E0956544",
5712 "kind" : "video",
5713 "linkId" : "F6AD378F-5CCF-4E64-988D-88B2876624B9",
5714 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
5715 "muted" : false,
5716 "newShot" : false,
5717 "speed" : 1,
5718 "srcIn" : 1537.8644067796606,
5719 "start" : 481.0169491525424,
5720 "track" : "v0"
5721 },
5722 {
5723 "duration" : 2.7796610169491487,
5724 "fadeIn" : 0,
5725 "fadeOut" : 0,
5726 "id" : "6F365D36-DA75-4DAC-875E-2799D7405B79",
5727 "kind" : "audio",
5728 "linkId" : "F6AD378F-5CCF-4E64-988D-88B2876624B9",
5729 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
5730 "muted" : true,
5731 "newShot" : false,
5732 "speed" : 1,
5733 "srcIn" : 1537.7805352383195,
5734 "start" : 481.0169491525424,
5735 "track" : "v1"
5736 },
5737 {
5738 "duration" : 2.7796610169491487,
5739 "fadeIn" : 0,
5740 "fadeOut" : 0,
5741 "id" : "6086A913-26DD-4E7A-8DA8-5B7FF6C55700",
5742 "kind" : "audio",
5743 "linkId" : "F6AD378F-5CCF-4E64-988D-88B2876624B9",
5744 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
5745 "muted" : false,
5746 "newShot" : false,
5747 "speed" : 1,
5748 "srcIn" : 1537.8272115415505,
5749 "start" : 481.0169491525424,
5750 "track" : "v2"
5751 },
5752 {
5753 "duration" : 2.7796610169491487,
5754 "fadeIn" : 0,
5755 "fadeOut" : 0,
5756 "id" : "2F29A3B0-A10F-4DB4-B9E2-B76DBFA73359",
5757 "kind" : "video",
5758 "linkId" : "F6AD378F-5CCF-4E64-988D-88B2876624B9",
5759 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
5760 "muted" : false,
5761 "newShot" : false,
5762 "speed" : 1,
5763 "srcIn" : 1537.6906044053223,
5764 "start" : 481.0169491525424,
5765 "track" : "v3"
5766 },
5767 {
5768 "duration" : 2.7796610169491487,
5769 "fadeIn" : 0,
5770 "fadeOut" : 0,
5771 "id" : "1684BBB7-47B4-4BFA-B8C1-058C3FAC9CED",
5772 "kind" : "video",
5773 "linkId" : "F6AD378F-5CCF-4E64-988D-88B2876624B9",
5774 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
5775 "muted" : false,
5776 "newShot" : false,
5777 "speed" : 1,
5778 "srcIn" : 1537.6905998213235,
5779 "start" : 481.0169491525424,
5780 "track" : "v4"
5781 },
5782 {
5783 "duration" : 1.0847457627118615,
5784 "fadeIn" : 0,
5785 "fadeOut" : 0,
5786 "id" : "A7680D68-6BCD-4F79-A28F-858D96D4A6E2",
5787 "kind" : "video",
5788 "linkId" : "A3087F67-EF84-47FF-B3BE-7F42F50669A0",
5789 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
5790 "muted" : false,
5791 "newShot" : false,
5792 "speed" : 1,
5793 "srcIn" : 1540.6440677966098,
5794 "start" : 483.79661016949154,
5795 "track" : "v0"
5796 },
5797 {
5798 "duration" : 1.0847457627118615,
5799 "fadeIn" : 0,
5800 "fadeOut" : 0,
5801 "id" : "3854E595-101A-4295-A1B3-5765E651A0AF",
5802 "kind" : "audio",
5803 "linkId" : "A3087F67-EF84-47FF-B3BE-7F42F50669A0",
5804 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
5805 "muted" : true,
5806 "newShot" : false,
5807 "speed" : 1,
5808 "srcIn" : 1540.5601962552687,
5809 "start" : 483.79661016949154,
5810 "track" : "v1"
5811 },
5812 {
5813 "duration" : 1.0847457627118615,
5814 "fadeIn" : 0,
5815 "fadeOut" : 0,
5816 "id" : "D256891E-1C5E-4D56-9CA9-3638EDDF53E4",
5817 "kind" : "audio",
5818 "linkId" : "A3087F67-EF84-47FF-B3BE-7F42F50669A0",
5819 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
5820 "muted" : false,
5821 "newShot" : false,
5822 "speed" : 1,
5823 "srcIn" : 1540.6068725584996,
5824 "start" : 483.79661016949154,
5825 "track" : "v2"
5826 },
5827 {
5828 "duration" : 1.0847457627118615,
5829 "fadeIn" : 0,
5830 "fadeOut" : 0,
5831 "id" : "B12D34EF-C8C7-49A2-8C6F-04B3A3068BC7",
5832 "kind" : "video",
5833 "linkId" : "A3087F67-EF84-47FF-B3BE-7F42F50669A0",
5834 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
5835 "muted" : false,
5836 "newShot" : false,
5837 "speed" : 1,
5838 "srcIn" : 1540.4702654222715,
5839 "start" : 483.79661016949154,
5840 "track" : "v3"
5841 },
5842 {
5843 "duration" : 1.0847457627118615,
5844 "fadeIn" : 0,
5845 "fadeOut" : 0,
5846 "id" : "D07A239B-1D03-40C0-82F5-BF4EE984E137",
5847 "kind" : "video",
5848 "linkId" : "A3087F67-EF84-47FF-B3BE-7F42F50669A0",
5849 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
5850 "muted" : false,
5851 "newShot" : false,
5852 "speed" : 1,
5853 "srcIn" : 1540.4702608382727,
5854 "start" : 483.79661016949154,
5855 "track" : "v4"
5856 },
5857 {
5858 "duration" : 3.5932203389830306,
5859 "fadeIn" : 0,
5860 "fadeOut" : 0,
5861 "id" : "B7BE7F5F-53C9-40B3-9F12-7DF7A2D9685A",
5862 "kind" : "video",
5863 "linkId" : "F9AD2997-81A6-4EF6-BD27-5B43EE87F75A",
5864 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
5865 "muted" : false,
5866 "newShot" : false,
5867 "speed" : 1,
5868 "srcIn" : 1630.745762711864,
5869 "start" : 484.8813559322034,
5870 "track" : "v0"
5871 },
5872 {
5873 "duration" : 3.5932203389830306,
5874 "fadeIn" : 0,
5875 "fadeOut" : 0,
5876 "id" : "B0F7D729-54ED-422A-B202-11DF2760B5F3",
5877 "kind" : "audio",
5878 "linkId" : "F9AD2997-81A6-4EF6-BD27-5B43EE87F75A",
5879 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
5880 "muted" : true,
5881 "newShot" : false,
5882 "speed" : 1,
5883 "srcIn" : 1630.661891170523,
5884 "start" : 484.8813559322034,
5885 "track" : "v1"
5886 },
5887 {
5888 "duration" : 3.5932203389830306,
5889 "fadeIn" : 0,
5890 "fadeOut" : 0,
5891 "id" : "905A3ACC-296C-4843-B9D9-B86F50F5BA5F",
5892 "kind" : "audio",
5893 "linkId" : "F9AD2997-81A6-4EF6-BD27-5B43EE87F75A",
5894 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
5895 "muted" : false,
5896 "newShot" : false,
5897 "speed" : 1,
5898 "srcIn" : 1630.7085674737539,
5899 "start" : 484.8813559322034,
5900 "track" : "v2"
5901 },
5902 {
5903 "duration" : 3.5932203389830306,
5904 "fadeIn" : 0,
5905 "fadeOut" : 0,
5906 "id" : "0B7D26CE-983C-4362-B1CD-AC3A40378E3B",
5907 "kind" : "video",
5908 "linkId" : "F9AD2997-81A6-4EF6-BD27-5B43EE87F75A",
5909 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
5910 "muted" : false,
5911 "newShot" : false,
5912 "speed" : 1,
5913 "srcIn" : 1630.5719603375258,
5914 "start" : 484.8813559322034,
5915 "track" : "v3"
5916 },
5917 {
5918 "duration" : 3.5932203389830306,
5919 "fadeIn" : 0,
5920 "fadeOut" : 0,
5921 "id" : "AFE6D697-39D5-420E-BD1B-A64E0A13DC41",
5922 "kind" : "video",
5923 "linkId" : "F9AD2997-81A6-4EF6-BD27-5B43EE87F75A",
5924 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
5925 "muted" : false,
5926 "newShot" : false,
5927 "speed" : 1,
5928 "srcIn" : 1630.571955753527,
5929 "start" : 484.8813559322034,
5930 "track" : "v4"
5931 },
5932 {
5933 "duration" : 5.525423728813564,
5934 "fadeIn" : 0,
5935 "fadeOut" : 0,
5936 "id" : "FFF0A0F3-132A-4462-A828-0FAEB88BBC4D",
5937 "kind" : "video",
5938 "linkId" : "0DB5CF0D-9192-4B84-94D8-6B5373F51616",
5939 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
5940 "muted" : false,
5941 "newShot" : false,
5942 "speed" : 1,
5943 "srcIn" : 1690.881355932203,
5944 "start" : 488.47457627118644,
5945 "track" : "v0"
5946 },
5947 {
5948 "duration" : 5.525423728813564,
5949 "fadeIn" : 0,
5950 "fadeOut" : 0,
5951 "id" : "8C1B2FDC-C8A5-44ED-9F1E-CA222B045C32",
5952 "kind" : "audio",
5953 "linkId" : "0DB5CF0D-9192-4B84-94D8-6B5373F51616",
5954 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
5955 "muted" : true,
5956 "newShot" : false,
5957 "speed" : 1,
5958 "srcIn" : 1690.7974843908619,
5959 "start" : 488.47457627118644,
5960 "track" : "v1"
5961 },
5962 {
5963 "duration" : 5.525423728813564,
5964 "fadeIn" : 0,
5965 "fadeOut" : 0,
5966 "id" : "630C64EC-19AC-4FC2-B36B-5B8934D80D33",
5967 "kind" : "audio",
5968 "linkId" : "0DB5CF0D-9192-4B84-94D8-6B5373F51616",
5969 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
5970 "muted" : false,
5971 "newShot" : false,
5972 "speed" : 1,
5973 "srcIn" : 1690.8441606940928,
5974 "start" : 488.47457627118644,
5975 "track" : "v2"
5976 },
5977 {
5978 "duration" : 5.525423728813564,
5979 "fadeIn" : 0,
5980 "fadeOut" : 0,
5981 "id" : "234A86B0-ECCC-4314-9B52-A58DB3C619BB",
5982 "kind" : "video",
5983 "linkId" : "0DB5CF0D-9192-4B84-94D8-6B5373F51616",
5984 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
5985 "muted" : false,
5986 "newShot" : false,
5987 "speed" : 1,
5988 "srcIn" : 1690.7075535578647,
5989 "start" : 488.47457627118644,
5990 "track" : "v3"
5991 },
5992 {
5993 "duration" : 5.525423728813564,
5994 "fadeIn" : 0,
5995 "fadeOut" : 0,
5996 "id" : "02321509-9B22-45BA-884A-BFB975074D08",
5997 "kind" : "video",
5998 "linkId" : "0DB5CF0D-9192-4B84-94D8-6B5373F51616",
5999 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
6000 "muted" : false,
6001 "newShot" : false,
6002 "speed" : 1,
6003 "srcIn" : 1690.7075489738659,
6004 "start" : 488.47457627118644,
6005 "track" : "v4"
6006 },
6007 {
6008 "duration" : 3.9661016949152668,
6009 "fadeIn" : 0,
6010 "fadeOut" : 0,
6011 "id" : "A8D392D7-E60E-4189-B7D6-B1C2B5E6927D",
6012 "kind" : "video",
6013 "linkId" : "A50B7D21-1369-498E-A893-10D9266DA682",
6014 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
6015 "muted" : false,
6016 "newShot" : false,
6017 "speed" : 1,
6018 "srcIn" : 1703.5254237288132,
6019 "start" : 494,
6020 "track" : "v0"
6021 },
6022 {
6023 "duration" : 3.9661016949152668,
6024 "fadeIn" : 0,
6025 "fadeOut" : 0,
6026 "id" : "79B92814-E4D4-422A-B414-FEC1D42BABF8",
6027 "kind" : "audio",
6028 "linkId" : "A50B7D21-1369-498E-A893-10D9266DA682",
6029 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
6030 "muted" : true,
6031 "newShot" : false,
6032 "speed" : 1,
6033 "srcIn" : 1703.441552187472,
6034 "start" : 494,
6035 "track" : "v1"
6036 },
6037 {
6038 "duration" : 3.9661016949152668,
6039 "fadeIn" : 0,
6040 "fadeOut" : 0,
6041 "id" : "154F32D0-7DAB-433B-833A-86D9F7B2B99E",
6042 "kind" : "audio",
6043 "linkId" : "A50B7D21-1369-498E-A893-10D9266DA682",
6044 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
6045 "muted" : false,
6046 "newShot" : false,
6047 "speed" : 1,
6048 "srcIn" : 1703.488228490703,
6049 "start" : 494,
6050 "track" : "v2"
6051 },
6052 {
6053 "duration" : 3.9661016949152668,
6054 "fadeIn" : 0,
6055 "fadeOut" : 0,
6056 "id" : "C57B1520-E9CD-43AB-ABB5-AA9F59B97CE5",
6057 "kind" : "video",
6058 "linkId" : "A50B7D21-1369-498E-A893-10D9266DA682",
6059 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
6060 "muted" : false,
6061 "newShot" : false,
6062 "speed" : 1,
6063 "srcIn" : 1703.351621354475,
6064 "start" : 494,
6065 "track" : "v3"
6066 },
6067 {
6068 "duration" : 3.9661016949152668,
6069 "fadeIn" : 0,
6070 "fadeOut" : 0,
6071 "id" : "72ECB107-E625-47A0-91F5-4B07C84F9D01",
6072 "kind" : "video",
6073 "linkId" : "A50B7D21-1369-498E-A893-10D9266DA682",
6074 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
6075 "muted" : false,
6076 "newShot" : false,
6077 "speed" : 1,
6078 "srcIn" : 1703.351616770476,
6079 "start" : 494,
6080 "track" : "v4"
6081 },
6082 {
6083 "duration" : 3.322033898305051,
6084 "fadeIn" : 0,
6085 "fadeOut" : 0,
6086 "id" : "592BF8E8-84BB-4D68-ACAF-C5250A378311",
6087 "kind" : "video",
6088 "linkId" : "2F16524C-A3E5-4EC3-B918-2C0107EB5F71",
6089 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
6090 "muted" : false,
6091 "newShot" : false,
6092 "speed" : 1,
6093 "srcIn" : 1714.4067796610166,
6094 "start" : 497.96610169491527,
6095 "track" : "v0"
6096 },
6097 {
6098 "duration" : 3.322033898305051,
6099 "fadeIn" : 0,
6100 "fadeOut" : 0,
6101 "id" : "6A420F63-349C-490A-AEF2-A9A23B8F5911",
6102 "kind" : "audio",
6103 "linkId" : "2F16524C-A3E5-4EC3-B918-2C0107EB5F71",
6104 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
6105 "muted" : true,
6106 "newShot" : false,
6107 "speed" : 1,
6108 "srcIn" : 1714.3229081196755,
6109 "start" : 497.96610169491527,
6110 "track" : "v1"
6111 },
6112 {
6113 "duration" : 3.322033898305051,
6114 "fadeIn" : 0,
6115 "fadeOut" : 0,
6116 "id" : "9BFFE86E-C803-440C-A23A-27946EB8F38E",
6117 "kind" : "audio",
6118 "linkId" : "2F16524C-A3E5-4EC3-B918-2C0107EB5F71",
6119 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
6120 "muted" : false,
6121 "newShot" : false,
6122 "speed" : 1,
6123 "srcIn" : 1714.3695844229064,
6124 "start" : 497.96610169491527,
6125 "track" : "v2"
6126 },
6127 {
6128 "duration" : 3.322033898305051,
6129 "fadeIn" : 0,
6130 "fadeOut" : 0,
6131 "id" : "507C5A42-E790-48A6-8DE6-F867042AD251",
6132 "kind" : "video",
6133 "linkId" : "2F16524C-A3E5-4EC3-B918-2C0107EB5F71",
6134 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
6135 "muted" : false,
6136 "newShot" : false,
6137 "speed" : 1,
6138 "srcIn" : 1714.2329772866783,
6139 "start" : 497.96610169491527,
6140 "track" : "v3"
6141 },
6142 {
6143 "duration" : 3.322033898305051,
6144 "fadeIn" : 0,
6145 "fadeOut" : 0,
6146 "id" : "1255A12C-C270-451F-B9E7-2C3827620218",
6147 "kind" : "video",
6148 "linkId" : "2F16524C-A3E5-4EC3-B918-2C0107EB5F71",
6149 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
6150 "muted" : false,
6151 "newShot" : false,
6152 "speed" : 1,
6153 "srcIn" : 1714.2329727026795,
6154 "start" : 497.96610169491527,
6155 "track" : "v4"
6156 },
6157 {
6158 "duration" : 5.491525423728831,
6159 "fadeIn" : 0,
6160 "fadeOut" : 0,
6161 "id" : "FAF25F24-F13A-4DE0-9CF8-7D2584531C9E",
6162 "kind" : "video",
6163 "linkId" : "EE183339-F801-4116-A9EF-D64A77C5996D",
6164 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
6165 "muted" : false,
6166 "newShot" : false,
6167 "speed" : 1,
6168 "srcIn" : 1722.372881355932,
6169 "start" : 501.2881355932203,
6170 "track" : "v0"
6171 },
6172 {
6173 "duration" : 5.491525423728831,
6174 "fadeIn" : 0,
6175 "fadeOut" : 0,
6176 "id" : "E764AB44-88DC-41E9-A83E-E32A15AD06C5",
6177 "kind" : "audio",
6178 "linkId" : "EE183339-F801-4116-A9EF-D64A77C5996D",
6179 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
6180 "muted" : true,
6181 "newShot" : false,
6182 "speed" : 1,
6183 "srcIn" : 1722.2890098145908,
6184 "start" : 501.2881355932203,
6185 "track" : "v1"
6186 },
6187 {
6188 "duration" : 5.491525423728831,
6189 "fadeIn" : 0,
6190 "fadeOut" : 0,
6191 "id" : "A156F3B8-0CDE-4ADF-8CA0-BA16EF901769",
6192 "kind" : "audio",
6193 "linkId" : "EE183339-F801-4116-A9EF-D64A77C5996D",
6194 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
6195 "muted" : false,
6196 "newShot" : false,
6197 "speed" : 1,
6198 "srcIn" : 1722.3356861178218,
6199 "start" : 501.2881355932203,
6200 "track" : "v2"
6201 },
6202 {
6203 "duration" : 5.491525423728831,
6204 "fadeIn" : 0,
6205 "fadeOut" : 0,
6206 "id" : "60A808DE-37D0-4C45-925D-276CA1D59EB7",
6207 "kind" : "video",
6208 "linkId" : "EE183339-F801-4116-A9EF-D64A77C5996D",
6209 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
6210 "muted" : false,
6211 "newShot" : false,
6212 "speed" : 1,
6213 "srcIn" : 1722.1990789815936,
6214 "start" : 501.2881355932203,
6215 "track" : "v3"
6216 },
6217 {
6218 "duration" : 5.491525423728831,
6219 "fadeIn" : 0,
6220 "fadeOut" : 0,
6221 "id" : "C8564707-EE02-4CAD-BFA6-0089C454CE2A",
6222 "kind" : "video",
6223 "linkId" : "EE183339-F801-4116-A9EF-D64A77C5996D",
6224 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
6225 "muted" : false,
6226 "newShot" : false,
6227 "speed" : 1,
6228 "srcIn" : 1722.1990743975948,
6229 "start" : 501.2881355932203,
6230 "track" : "v4"
6231 },
6232 {
6233 "duration" : 7.627118644067764,
6234 "fadeIn" : 0,
6235 "fadeOut" : 0,
6236 "id" : "BB4F9A82-C969-4E01-BDBE-E481B17343CF",
6237 "kind" : "video",
6238 "linkId" : "DFDDB477-FE81-42F4-A11E-389F9E0A1439",
6239 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
6240 "muted" : false,
6241 "newShot" : false,
6242 "speed" : 1,
6243 "srcIn" : 1729.050847457627,
6244 "start" : 506.77966101694915,
6245 "track" : "v0"
6246 },
6247 {
6248 "duration" : 7.627118644067764,
6249 "fadeIn" : 0,
6250 "fadeOut" : 0,
6251 "id" : "D539326D-CFC6-4F6B-A59C-770061337CBD",
6252 "kind" : "audio",
6253 "linkId" : "DFDDB477-FE81-42F4-A11E-389F9E0A1439",
6254 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
6255 "muted" : true,
6256 "newShot" : false,
6257 "speed" : 1,
6258 "srcIn" : 1728.966975916286,
6259 "start" : 506.77966101694915,
6260 "track" : "v1"
6261 },
6262 {
6263 "duration" : 7.627118644067764,
6264 "fadeIn" : 0,
6265 "fadeOut" : 0,
6266 "id" : "28F991A4-9D42-4CBC-BE96-E89FEBFAF96C",
6267 "kind" : "audio",
6268 "linkId" : "DFDDB477-FE81-42F4-A11E-389F9E0A1439",
6269 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
6270 "muted" : false,
6271 "newShot" : false,
6272 "speed" : 1,
6273 "srcIn" : 1729.0136522195169,
6274 "start" : 506.77966101694915,
6275 "track" : "v2"
6276 },
6277 {
6278 "duration" : 7.627118644067764,
6279 "fadeIn" : 0,
6280 "fadeOut" : 0,
6281 "id" : "D4D4B878-3981-44A7-9435-B6B607F17D8E",
6282 "kind" : "video",
6283 "linkId" : "DFDDB477-FE81-42F4-A11E-389F9E0A1439",
6284 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
6285 "muted" : false,
6286 "newShot" : false,
6287 "speed" : 1,
6288 "srcIn" : 1728.8770450832887,
6289 "start" : 506.77966101694915,
6290 "track" : "v3"
6291 },
6292 {
6293 "duration" : 7.627118644067764,
6294 "fadeIn" : 0,
6295 "fadeOut" : 0,
6296 "id" : "BFB922A9-8931-4826-A5AE-4A8F51627F00",
6297 "kind" : "video",
6298 "linkId" : "DFDDB477-FE81-42F4-A11E-389F9E0A1439",
6299 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
6300 "muted" : false,
6301 "newShot" : false,
6302 "speed" : 1,
6303 "srcIn" : 1728.87704049929,
6304 "start" : 506.77966101694915,
6305 "track" : "v4"
6306 },
6307 {
6308 "duration" : 7.491525423728831,
6309 "fadeIn" : 0,
6310 "fadeOut" : 0,
6311 "id" : "08360F23-5B06-4769-94DC-42A31CD3BD46",
6312 "kind" : "video",
6313 "linkId" : "2C7554F2-E73F-41CD-85CB-697100DCD6C1",
6314 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
6315 "muted" : false,
6316 "newShot" : false,
6317 "speed" : 1,
6318 "srcIn" : 1747.593220338983,
6319 "start" : 514.4067796610169,
6320 "track" : "v0"
6321 },
6322 {
6323 "duration" : 7.491525423728831,
6324 "fadeIn" : 0,
6325 "fadeOut" : 0,
6326 "id" : "8BCF92B0-D87A-42BB-A168-F889F41CD1AB",
6327 "kind" : "audio",
6328 "linkId" : "2C7554F2-E73F-41CD-85CB-697100DCD6C1",
6329 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
6330 "muted" : true,
6331 "newShot" : false,
6332 "speed" : 1,
6333 "srcIn" : 1747.509348797642,
6334 "start" : 514.4067796610169,
6335 "track" : "v1"
6336 },
6337 {
6338 "duration" : 7.491525423728831,
6339 "fadeIn" : 0,
6340 "fadeOut" : 0,
6341 "id" : "2753AF8E-8B68-4B0D-A289-CCF8ABFA4EA6",
6342 "kind" : "audio",
6343 "linkId" : "2C7554F2-E73F-41CD-85CB-697100DCD6C1",
6344 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
6345 "muted" : false,
6346 "newShot" : false,
6347 "speed" : 1,
6348 "srcIn" : 1747.5560251008728,
6349 "start" : 514.4067796610169,
6350 "track" : "v2"
6351 },
6352 {
6353 "duration" : 7.491525423728831,
6354 "fadeIn" : 0,
6355 "fadeOut" : 0,
6356 "id" : "913D7AB9-49C1-4FEE-B538-D81313C8CDAA",
6357 "kind" : "video",
6358 "linkId" : "2C7554F2-E73F-41CD-85CB-697100DCD6C1",
6359 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
6360 "muted" : false,
6361 "newShot" : false,
6362 "speed" : 1,
6363 "srcIn" : 1747.4194179646447,
6364 "start" : 514.4067796610169,
6365 "track" : "v3"
6366 },
6367 {
6368 "duration" : 7.491525423728831,
6369 "fadeIn" : 0,
6370 "fadeOut" : 0,
6371 "id" : "A4078960-1488-4073-A3C0-10A87E8A55C7",
6372 "kind" : "video",
6373 "linkId" : "2C7554F2-E73F-41CD-85CB-697100DCD6C1",
6374 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
6375 "muted" : false,
6376 "newShot" : false,
6377 "speed" : 1,
6378 "srcIn" : 1747.419413380646,
6379 "start" : 514.4067796610169,
6380 "track" : "v4"
6381 },
6382 {
6383 "duration" : 10.677966101694892,
6384 "fadeIn" : 0,
6385 "fadeOut" : 0,
6386 "id" : "B0A75210-7D43-4E54-906B-32BFB4856787",
6387 "kind" : "video",
6388 "linkId" : "68BA82C2-1D37-4341-BBD5-0BE799A2C6FA",
6389 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
6390 "muted" : false,
6391 "newShot" : false,
6392 "speed" : 1,
6393 "srcIn" : 1755.084745762712,
6394 "start" : 521.8983050847457,
6395 "track" : "v0"
6396 },
6397 {
6398 "duration" : 10.677966101694892,
6399 "fadeIn" : 0,
6400 "fadeOut" : 0,
6401 "id" : "581E6DD4-D543-4188-9C1B-BC9D2F1F908C",
6402 "kind" : "audio",
6403 "linkId" : "68BA82C2-1D37-4341-BBD5-0BE799A2C6FA",
6404 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
6405 "muted" : true,
6406 "newShot" : false,
6407 "speed" : 1,
6408 "srcIn" : 1755.0008742213709,
6409 "start" : 521.8983050847457,
6410 "track" : "v1"
6411 },
6412 {
6413 "duration" : 10.677966101694892,
6414 "fadeIn" : 0,
6415 "fadeOut" : 0,
6416 "id" : "B8BFF2A6-3F9E-4D5E-8DFE-EDF5101E1AE5",
6417 "kind" : "audio",
6418 "linkId" : "68BA82C2-1D37-4341-BBD5-0BE799A2C6FA",
6419 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
6420 "muted" : false,
6421 "newShot" : false,
6422 "speed" : 1,
6423 "srcIn" : 1755.0475505246018,
6424 "start" : 521.8983050847457,
6425 "track" : "v2"
6426 },
6427 {
6428 "duration" : 10.677966101694892,
6429 "fadeIn" : 0,
6430 "fadeOut" : 0,
6431 "id" : "6669C09D-E39D-46DE-A862-6BA3B20F558E",
6432 "kind" : "video",
6433 "linkId" : "68BA82C2-1D37-4341-BBD5-0BE799A2C6FA",
6434 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
6435 "muted" : false,
6436 "newShot" : false,
6437 "speed" : 1,
6438 "srcIn" : 1754.9109433883737,
6439 "start" : 521.8983050847457,
6440 "track" : "v3"
6441 },
6442 {
6443 "duration" : 10.677966101694892,
6444 "fadeIn" : 0,
6445 "fadeOut" : 0,
6446 "id" : "C6619902-8ED4-46F4-907C-4F6DF342850B",
6447 "kind" : "video",
6448 "linkId" : "68BA82C2-1D37-4341-BBD5-0BE799A2C6FA",
6449 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
6450 "muted" : false,
6451 "newShot" : false,
6452 "speed" : 1,
6453 "srcIn" : 1754.9109388043748,
6454 "start" : 521.8983050847457,
6455 "track" : "v4"
6456 },
6457 {
6458 "duration" : 3.152542372881385,
6459 "fadeIn" : 0,
6460 "fadeOut" : 0,
6461 "id" : "1E1F6B29-A2BC-44CA-BC6B-B2DA54F93FE5",
6462 "kind" : "video",
6463 "linkId" : "F2447BC6-DF74-439E-8FD2-0FBAC39A205C",
6464 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
6465 "muted" : false,
6466 "newShot" : false,
6467 "speed" : 1,
6468 "srcIn" : 1766.6101694915255,
6469 "start" : 532.5762711864406,
6470 "track" : "v0"
6471 },
6472 {
6473 "duration" : 3.152542372881385,
6474 "fadeIn" : 0,
6475 "fadeOut" : 0,
6476 "id" : "97A49AEA-492D-4D8E-A171-E4F0831755E7",
6477 "kind" : "audio",
6478 "linkId" : "F2447BC6-DF74-439E-8FD2-0FBAC39A205C",
6479 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
6480 "muted" : true,
6481 "newShot" : false,
6482 "speed" : 1,
6483 "srcIn" : 1766.5262979501845,
6484 "start" : 532.5762711864406,
6485 "track" : "v1"
6486 },
6487 {
6488 "duration" : 3.152542372881385,
6489 "fadeIn" : 0,
6490 "fadeOut" : 0,
6491 "id" : "C0542DA4-D2C3-4854-B510-3701574E9CD1",
6492 "kind" : "audio",
6493 "linkId" : "F2447BC6-DF74-439E-8FD2-0FBAC39A205C",
6494 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
6495 "muted" : false,
6496 "newShot" : false,
6497 "speed" : 1,
6498 "srcIn" : 1766.5729742534154,
6499 "start" : 532.5762711864406,
6500 "track" : "v2"
6501 },
6502 {
6503 "duration" : 3.152542372881385,
6504 "fadeIn" : 0,
6505 "fadeOut" : 0,
6506 "id" : "85D95CB3-B6F4-4DFA-9F00-55E80014D2B2",
6507 "kind" : "video",
6508 "linkId" : "F2447BC6-DF74-439E-8FD2-0FBAC39A205C",
6509 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
6510 "muted" : false,
6511 "newShot" : false,
6512 "speed" : 1,
6513 "srcIn" : 1766.4363671171873,
6514 "start" : 532.5762711864406,
6515 "track" : "v3"
6516 },
6517 {
6518 "duration" : 3.152542372881385,
6519 "fadeIn" : 0,
6520 "fadeOut" : 0,
6521 "id" : "3AD412AE-C961-4FE7-B65F-3266A0980B36",
6522 "kind" : "video",
6523 "linkId" : "F2447BC6-DF74-439E-8FD2-0FBAC39A205C",
6524 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
6525 "muted" : false,
6526 "newShot" : false,
6527 "speed" : 1,
6528 "srcIn" : 1766.4363625331885,
6529 "start" : 532.5762711864406,
6530 "track" : "v4"
6531 },
6532 {
6533 "duration" : 5.491525423728831,
6534 "fadeIn" : 0,
6535 "fadeOut" : 0,
6536 "id" : "1F116DEE-E2FC-4C19-B84E-F62E8C7FB58E",
6537 "kind" : "video",
6538 "linkId" : "CC83DD13-876C-4795-A07C-8A9F20E0FCD4",
6539 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
6540 "muted" : false,
6541 "newShot" : false,
6542 "speed" : 1,
6543 "srcIn" : 1771.898305084746,
6544 "start" : 535.728813559322,
6545 "track" : "v0"
6546 },
6547 {
6548 "duration" : 5.491525423728831,
6549 "fadeIn" : 0,
6550 "fadeOut" : 0,
6551 "id" : "26F01770-1063-4614-838E-9C7408CB8EA7",
6552 "kind" : "audio",
6553 "linkId" : "CC83DD13-876C-4795-A07C-8A9F20E0FCD4",
6554 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
6555 "muted" : true,
6556 "newShot" : false,
6557 "speed" : 1,
6558 "srcIn" : 1771.814433543405,
6559 "start" : 535.728813559322,
6560 "track" : "v1"
6561 },
6562 {
6563 "duration" : 5.491525423728831,
6564 "fadeIn" : 0,
6565 "fadeOut" : 0,
6566 "id" : "E27B6F72-E807-4B80-B045-EF94CBEAE518",
6567 "kind" : "audio",
6568 "linkId" : "CC83DD13-876C-4795-A07C-8A9F20E0FCD4",
6569 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
6570 "muted" : false,
6571 "newShot" : false,
6572 "speed" : 1,
6573 "srcIn" : 1771.8611098466358,
6574 "start" : 535.728813559322,
6575 "track" : "v2"
6576 },
6577 {
6578 "duration" : 5.491525423728831,
6579 "fadeIn" : 0,
6580 "fadeOut" : 0,
6581 "id" : "780E91E1-D956-46CE-AD94-132F9146FF16",
6582 "kind" : "video",
6583 "linkId" : "CC83DD13-876C-4795-A07C-8A9F20E0FCD4",
6584 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
6585 "muted" : false,
6586 "newShot" : false,
6587 "speed" : 1,
6588 "srcIn" : 1771.7245027104077,
6589 "start" : 535.728813559322,
6590 "track" : "v3"
6591 },
6592 {
6593 "duration" : 5.491525423728831,
6594 "fadeIn" : 0,
6595 "fadeOut" : 0,
6596 "id" : "502017FB-B15D-472A-92CD-864B01FA9D5A",
6597 "kind" : "video",
6598 "linkId" : "CC83DD13-876C-4795-A07C-8A9F20E0FCD4",
6599 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
6600 "muted" : false,
6601 "newShot" : false,
6602 "speed" : 1,
6603 "srcIn" : 1771.724498126409,
6604 "start" : 535.728813559322,
6605 "track" : "v4"
6606 },
6607 {
6608 "duration" : 4.0677966101694665,
6609 "fadeIn" : 0,
6610 "fadeOut" : 0,
6611 "id" : "C08F7E72-3AD4-4B1D-8250-1512128E7940",
6612 "kind" : "video",
6613 "linkId" : "A2118321-9E02-4656-BCE9-0E3980E8372E",
6614 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
6615 "muted" : false,
6616 "newShot" : false,
6617 "speed" : 1,
6618 "srcIn" : 1779.5593220338988,
6619 "start" : 541.2203389830509,
6620 "track" : "v0"
6621 },
6622 {
6623 "duration" : 4.0677966101694665,
6624 "fadeIn" : 0,
6625 "fadeOut" : 0,
6626 "id" : "B58ABDEF-6C61-45A8-95FF-6445D0C0EABC",
6627 "kind" : "audio",
6628 "linkId" : "A2118321-9E02-4656-BCE9-0E3980E8372E",
6629 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
6630 "muted" : true,
6631 "newShot" : false,
6632 "speed" : 1,
6633 "srcIn" : 1779.4754504925577,
6634 "start" : 541.2203389830509,
6635 "track" : "v1"
6636 },
6637 {
6638 "duration" : 4.0677966101694665,
6639 "fadeIn" : 0,
6640 "fadeOut" : 0,
6641 "id" : "141D7901-0677-44F5-8296-15BC113C1EBE",
6642 "kind" : "audio",
6643 "linkId" : "A2118321-9E02-4656-BCE9-0E3980E8372E",
6644 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
6645 "muted" : false,
6646 "newShot" : false,
6647 "speed" : 1,
6648 "srcIn" : 1779.5221267957886,
6649 "start" : 541.2203389830509,
6650 "track" : "v2"
6651 },
6652 {
6653 "duration" : 4.0677966101694665,
6654 "fadeIn" : 0,
6655 "fadeOut" : 0,
6656 "id" : "BADB3830-CF8B-4208-A04F-439958ACADDA",
6657 "kind" : "video",
6658 "linkId" : "A2118321-9E02-4656-BCE9-0E3980E8372E",
6659 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
6660 "muted" : false,
6661 "newShot" : false,
6662 "speed" : 1,
6663 "srcIn" : 1779.3855196595605,
6664 "start" : 541.2203389830509,
6665 "track" : "v3"
6666 },
6667 {
6668 "duration" : 4.0677966101694665,
6669 "fadeIn" : 0,
6670 "fadeOut" : 0,
6671 "id" : "83BD9C59-2FC5-4F64-9963-A86B647F6F52",
6672 "kind" : "video",
6673 "linkId" : "A2118321-9E02-4656-BCE9-0E3980E8372E",
6674 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
6675 "muted" : false,
6676 "newShot" : false,
6677 "speed" : 1,
6678 "srcIn" : 1779.3855150755617,
6679 "start" : 541.2203389830509,
6680 "track" : "v4"
6681 },
6682 {
6683 "duration" : 1.661016949152554,
6684 "fadeIn" : 0,
6685 "fadeOut" : 0,
6686 "id" : "81042DC7-94FE-4359-959F-C9E29D4F9461",
6687 "kind" : "video",
6688 "linkId" : "CDF46AA7-2655-4344-81E6-6C7AB4F4B303",
6689 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
6690 "muted" : false,
6691 "newShot" : false,
6692 "speed" : 1,
6693 "srcIn" : 1815.5932203389834,
6694 "start" : 545.2881355932203,
6695 "track" : "v0"
6696 },
6697 {
6698 "duration" : 1.661016949152554,
6699 "fadeIn" : 0,
6700 "fadeOut" : 0,
6701 "id" : "EB5D9E21-E9FF-430D-B90A-143FFFC161D0",
6702 "kind" : "audio",
6703 "linkId" : "CDF46AA7-2655-4344-81E6-6C7AB4F4B303",
6704 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
6705 "muted" : true,
6706 "newShot" : false,
6707 "speed" : 1,
6708 "srcIn" : 1815.5093487976424,
6709 "start" : 545.2881355932203,
6710 "track" : "v1"
6711 },
6712 {
6713 "duration" : 1.661016949152554,
6714 "fadeIn" : 0,
6715 "fadeOut" : 0,
6716 "id" : "E5C8677F-212F-43D4-95D6-F86D218562F5",
6717 "kind" : "audio",
6718 "linkId" : "CDF46AA7-2655-4344-81E6-6C7AB4F4B303",
6719 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
6720 "muted" : false,
6721 "newShot" : false,
6722 "speed" : 1,
6723 "srcIn" : 1815.5560251008733,
6724 "start" : 545.2881355932203,
6725 "track" : "v2"
6726 },
6727 {
6728 "duration" : 1.661016949152554,
6729 "fadeIn" : 0,
6730 "fadeOut" : 0,
6731 "id" : "899CE36B-86FD-4463-ACF9-64EFDF419140",
6732 "kind" : "video",
6733 "linkId" : "CDF46AA7-2655-4344-81E6-6C7AB4F4B303",
6734 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
6735 "muted" : false,
6736 "newShot" : false,
6737 "speed" : 1,
6738 "srcIn" : 1815.4194179646452,
6739 "start" : 545.2881355932203,
6740 "track" : "v3"
6741 },
6742 {
6743 "duration" : 1.661016949152554,
6744 "fadeIn" : 0,
6745 "fadeOut" : 0,
6746 "id" : "B0B393DE-7AAD-46D0-8783-D2E08335DC13",
6747 "kind" : "video",
6748 "linkId" : "CDF46AA7-2655-4344-81E6-6C7AB4F4B303",
6749 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
6750 "muted" : false,
6751 "newShot" : false,
6752 "speed" : 1,
6753 "srcIn" : 1815.4194133806463,
6754 "start" : 545.2881355932203,
6755 "track" : "v4"
6756 },
6757 {
6758 "duration" : 1.1525423728813848,
6759 "fadeIn" : 0,
6760 "fadeOut" : 0,
6761 "id" : "18E9C9F7-AA69-4D86-9DD9-5278392186CD",
6762 "kind" : "video",
6763 "linkId" : "2426D580-BCAA-49B5-BF7C-9DCE2A558605",
6764 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
6765 "muted" : false,
6766 "newShot" : false,
6767 "speed" : 1,
6768 "srcIn" : 1817.5593220338988,
6769 "start" : 546.9491525423729,
6770 "track" : "v0"
6771 },
6772 {
6773 "duration" : 1.1525423728813848,
6774 "fadeIn" : 0,
6775 "fadeOut" : 0,
6776 "id" : "098E9E68-8FCE-4C2E-8A24-81EFEECCD127",
6777 "kind" : "audio",
6778 "linkId" : "2426D580-BCAA-49B5-BF7C-9DCE2A558605",
6779 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
6780 "muted" : true,
6781 "newShot" : false,
6782 "speed" : 1,
6783 "srcIn" : 1817.4754504925577,
6784 "start" : 546.9491525423729,
6785 "track" : "v1"
6786 },
6787 {
6788 "duration" : 1.1525423728813848,
6789 "fadeIn" : 0,
6790 "fadeOut" : 0,
6791 "id" : "3A68B520-1B13-4837-98C7-4715D6250B50",
6792 "kind" : "audio",
6793 "linkId" : "2426D580-BCAA-49B5-BF7C-9DCE2A558605",
6794 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
6795 "muted" : false,
6796 "newShot" : false,
6797 "speed" : 1,
6798 "srcIn" : 1817.5221267957886,
6799 "start" : 546.9491525423729,
6800 "track" : "v2"
6801 },
6802 {
6803 "duration" : 1.1525423728813848,
6804 "fadeIn" : 0,
6805 "fadeOut" : 0,
6806 "id" : "3D51136D-FC09-46D4-A29C-C1E8B7022D04",
6807 "kind" : "video",
6808 "linkId" : "2426D580-BCAA-49B5-BF7C-9DCE2A558605",
6809 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
6810 "muted" : false,
6811 "newShot" : false,
6812 "speed" : 1,
6813 "srcIn" : 1817.3855196595605,
6814 "start" : 546.9491525423729,
6815 "track" : "v3"
6816 },
6817 {
6818 "duration" : 1.1525423728813848,
6819 "fadeIn" : 0,
6820 "fadeOut" : 0,
6821 "id" : "879B5431-78D7-4E34-98CD-811F0D7D8D2B",
6822 "kind" : "video",
6823 "linkId" : "2426D580-BCAA-49B5-BF7C-9DCE2A558605",
6824 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
6825 "muted" : false,
6826 "newShot" : false,
6827 "speed" : 1,
6828 "srcIn" : 1817.3855150755617,
6829 "start" : 546.9491525423729,
6830 "track" : "v4"
6831 },
6832 {
6833 "duration" : 11.525423728813507,
6834 "fadeIn" : 0,
6835 "fadeOut" : 0,
6836 "id" : "B6AD6E92-A164-48D3-962D-8D5472AC5ED8",
6837 "kind" : "video",
6838 "linkId" : "5F7B4F1D-CE0A-49F1-A14C-6C7D1809E682",
6839 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
6840 "muted" : false,
6841 "newShot" : false,
6842 "speed" : 1,
6843 "srcIn" : 1823.5593220338988,
6844 "start" : 548.1016949152543,
6845 "track" : "v0"
6846 },
6847 {
6848 "duration" : 11.525423728813507,
6849 "fadeIn" : 0,
6850 "fadeOut" : 0,
6851 "id" : "15310F42-B66D-4DDB-B571-28824E2AA8F2",
6852 "kind" : "audio",
6853 "linkId" : "5F7B4F1D-CE0A-49F1-A14C-6C7D1809E682",
6854 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
6855 "muted" : true,
6856 "newShot" : false,
6857 "speed" : 1,
6858 "srcIn" : 1823.4754504925577,
6859 "start" : 548.1016949152543,
6860 "track" : "v1"
6861 },
6862 {
6863 "duration" : 11.525423728813507,
6864 "fadeIn" : 0,
6865 "fadeOut" : 0,
6866 "id" : "60DB4DB9-D800-414F-8313-D3C0C9CF620C",
6867 "kind" : "audio",
6868 "linkId" : "5F7B4F1D-CE0A-49F1-A14C-6C7D1809E682",
6869 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
6870 "muted" : false,
6871 "newShot" : false,
6872 "speed" : 1,
6873 "srcIn" : 1823.5221267957886,
6874 "start" : 548.1016949152543,
6875 "track" : "v2"
6876 },
6877 {
6878 "duration" : 11.525423728813507,
6879 "fadeIn" : 0,
6880 "fadeOut" : 0,
6881 "id" : "66EBCF41-7ABC-4B4F-B3D6-670B5DE5FF4B",
6882 "kind" : "video",
6883 "linkId" : "5F7B4F1D-CE0A-49F1-A14C-6C7D1809E682",
6884 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
6885 "muted" : false,
6886 "newShot" : false,
6887 "speed" : 1,
6888 "srcIn" : 1823.3855196595605,
6889 "start" : 548.1016949152543,
6890 "track" : "v3"
6891 },
6892 {
6893 "duration" : 11.525423728813507,
6894 "fadeIn" : 0,
6895 "fadeOut" : 0,
6896 "id" : "74B48312-8BD1-4CF9-967A-E7EED8D50E2A",
6897 "kind" : "video",
6898 "linkId" : "5F7B4F1D-CE0A-49F1-A14C-6C7D1809E682",
6899 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
6900 "muted" : false,
6901 "newShot" : false,
6902 "speed" : 1,
6903 "srcIn" : 1823.3855150755617,
6904 "start" : 548.1016949152543,
6905 "track" : "v4"
6906 },
6907 {
6908 "duration" : 3.6271186440678775,
6909 "fadeIn" : 0,
6910 "fadeOut" : 0,
6911 "id" : "7E495919-B008-4616-BB9C-17E3BDE688A9",
6912 "kind" : "video",
6913 "linkId" : "9769E036-22A2-4F7C-95DD-F0F9F243A183",
6914 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
6915 "muted" : false,
6916 "newShot" : false,
6917 "speed" : 1,
6918 "srcIn" : 1836.474576271187,
6919 "start" : 559.6271186440678,
6920 "track" : "v0"
6921 },
6922 {
6923 "duration" : 3.6271186440678775,
6924 "fadeIn" : 0,
6925 "fadeOut" : 0,
6926 "id" : "7E503A80-A896-4D03-9DC1-896D78C0F03A",
6927 "kind" : "audio",
6928 "linkId" : "9769E036-22A2-4F7C-95DD-F0F9F243A183",
6929 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
6930 "muted" : true,
6931 "newShot" : false,
6932 "speed" : 1,
6933 "srcIn" : 1836.390704729846,
6934 "start" : 559.6271186440678,
6935 "track" : "v1"
6936 },
6937 {
6938 "duration" : 3.6271186440678775,
6939 "fadeIn" : 0,
6940 "fadeOut" : 0,
6941 "id" : "37A7F54D-1185-4016-9C07-149400B154B8",
6942 "kind" : "audio",
6943 "linkId" : "9769E036-22A2-4F7C-95DD-F0F9F243A183",
6944 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
6945 "muted" : false,
6946 "newShot" : false,
6947 "speed" : 1,
6948 "srcIn" : 1836.437381033077,
6949 "start" : 559.6271186440678,
6950 "track" : "v2"
6951 },
6952 {
6953 "duration" : 3.6271186440678775,
6954 "fadeIn" : 0,
6955 "fadeOut" : 0,
6956 "id" : "12B7D82E-9520-4486-82BA-A1EFA3443CC5",
6957 "kind" : "video",
6958 "linkId" : "9769E036-22A2-4F7C-95DD-F0F9F243A183",
6959 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
6960 "muted" : false,
6961 "newShot" : false,
6962 "speed" : 1,
6963 "srcIn" : 1836.3007738968488,
6964 "start" : 559.6271186440678,
6965 "track" : "v3"
6966 },
6967 {
6968 "duration" : 3.6271186440678775,
6969 "fadeIn" : 0,
6970 "fadeOut" : 0,
6971 "id" : "467D66A4-ABC0-4A30-B666-EC09C21670C2",
6972 "kind" : "video",
6973 "linkId" : "9769E036-22A2-4F7C-95DD-F0F9F243A183",
6974 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
6975 "muted" : false,
6976 "newShot" : false,
6977 "speed" : 1,
6978 "srcIn" : 1836.30076931285,
6979 "start" : 559.6271186440678,
6980 "track" : "v4"
6981 },
6982 {
6983 "duration" : 5.118644067796595,
6984 "fadeIn" : 0,
6985 "fadeOut" : 0,
6986 "id" : "F7C9CC46-173D-4CEA-B84A-F83B2D94A2CE",
6987 "kind" : "video",
6988 "linkId" : "B85938EF-37B5-4F22-B0B6-0C3F5749452C",
6989 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
6990 "muted" : false,
6991 "newShot" : false,
6992 "speed" : 1,
6993 "srcIn" : 1849.1186440677973,
6994 "start" : 563.2542372881356,
6995 "track" : "v0"
6996 },
6997 {
6998 "duration" : 5.118644067796595,
6999 "fadeIn" : 0,
7000 "fadeOut" : 0,
7001 "id" : "AA8C606C-3D23-4965-B5D1-E17B4413C421",
7002 "kind" : "audio",
7003 "linkId" : "B85938EF-37B5-4F22-B0B6-0C3F5749452C",
7004 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
7005 "muted" : true,
7006 "newShot" : false,
7007 "speed" : 1,
7008 "srcIn" : 1849.0347725264562,
7009 "start" : 563.2542372881356,
7010 "track" : "v1"
7011 },
7012 {
7013 "duration" : 5.118644067796595,
7014 "fadeIn" : 0,
7015 "fadeOut" : 0,
7016 "id" : "FB4942C0-5842-419B-A4CA-242596D7CF03",
7017 "kind" : "audio",
7018 "linkId" : "B85938EF-37B5-4F22-B0B6-0C3F5749452C",
7019 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
7020 "muted" : false,
7021 "newShot" : false,
7022 "speed" : 1,
7023 "srcIn" : 1849.0814488296871,
7024 "start" : 563.2542372881356,
7025 "track" : "v2"
7026 },
7027 {
7028 "duration" : 5.118644067796595,
7029 "fadeIn" : 0,
7030 "fadeOut" : 0,
7031 "id" : "8D875994-D892-44D9-A74C-9AE0403A1240",
7032 "kind" : "video",
7033 "linkId" : "B85938EF-37B5-4F22-B0B6-0C3F5749452C",
7034 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
7035 "muted" : false,
7036 "newShot" : false,
7037 "speed" : 1,
7038 "srcIn" : 1848.944841693459,
7039 "start" : 563.2542372881356,
7040 "track" : "v3"
7041 },
7042 {
7043 "duration" : 5.118644067796595,
7044 "fadeIn" : 0,
7045 "fadeOut" : 0,
7046 "id" : "D7122A1E-0F1A-4EDA-BA83-5032DCB51740",
7047 "kind" : "video",
7048 "linkId" : "B85938EF-37B5-4F22-B0B6-0C3F5749452C",
7049 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
7050 "muted" : false,
7051 "newShot" : false,
7052 "speed" : 1,
7053 "srcIn" : 1848.9448371094602,
7054 "start" : 563.2542372881356,
7055 "track" : "v4"
7056 },
7057 {
7058 "duration" : 8.2033898305084,
7059 "fadeIn" : 0,
7060 "fadeOut" : 0,
7061 "id" : "54B76DE8-2057-470C-97CB-916B91B32B1C",
7062 "kind" : "video",
7063 "linkId" : "65CA1374-003F-413C-9F77-8A22B7EECDC3",
7064 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
7065 "muted" : false,
7066 "newShot" : false,
7067 "speed" : 1,
7068 "srcIn" : 1854.9830508474583,
7069 "start" : 568.3728813559322,
7070 "track" : "v0"
7071 },
7072 {
7073 "duration" : 8.2033898305084,
7074 "fadeIn" : 0,
7075 "fadeOut" : 0,
7076 "id" : "12851101-B365-4F9E-BFB3-FDEABAE1453A",
7077 "kind" : "audio",
7078 "linkId" : "65CA1374-003F-413C-9F77-8A22B7EECDC3",
7079 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
7080 "muted" : true,
7081 "newShot" : false,
7082 "speed" : 1,
7083 "srcIn" : 1854.8991793061173,
7084 "start" : 568.3728813559322,
7085 "track" : "v1"
7086 },
7087 {
7088 "duration" : 8.2033898305084,
7089 "fadeIn" : 0,
7090 "fadeOut" : 0,
7091 "id" : "38DCBA3E-C18E-447C-B319-9E3A3DE4DC21",
7092 "kind" : "audio",
7093 "linkId" : "65CA1374-003F-413C-9F77-8A22B7EECDC3",
7094 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
7095 "muted" : false,
7096 "newShot" : false,
7097 "speed" : 1,
7098 "srcIn" : 1854.9458556093482,
7099 "start" : 568.3728813559322,
7100 "track" : "v2"
7101 },
7102 {
7103 "duration" : 8.2033898305084,
7104 "fadeIn" : 0,
7105 "fadeOut" : 0,
7106 "id" : "FE05A76D-4FB5-43E6-9960-DE92DC23FD6C",
7107 "kind" : "video",
7108 "linkId" : "65CA1374-003F-413C-9F77-8A22B7EECDC3",
7109 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
7110 "muted" : false,
7111 "newShot" : false,
7112 "speed" : 1,
7113 "srcIn" : 1854.80924847312,
7114 "start" : 568.3728813559322,
7115 "track" : "v3"
7116 },
7117 {
7118 "duration" : 8.2033898305084,
7119 "fadeIn" : 0,
7120 "fadeOut" : 0,
7121 "id" : "35C17BC8-29A1-44B4-A655-A1AF25C6D713",
7122 "kind" : "video",
7123 "linkId" : "65CA1374-003F-413C-9F77-8A22B7EECDC3",
7124 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
7125 "muted" : false,
7126 "newShot" : false,
7127 "speed" : 1,
7128 "srcIn" : 1854.8092438891213,
7129 "start" : 568.3728813559322,
7130 "track" : "v4"
7131 },
7132 {
7133 "duration" : 5.559322033898297,
7134 "fadeIn" : 0,
7135 "fadeOut" : 0,
7136 "id" : "ED705251-681D-4EEE-81C8-D077FBB072B5",
7137 "kind" : "video",
7138 "linkId" : "FD6E0C12-08F2-474C-B889-10F178952804",
7139 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
7140 "muted" : false,
7141 "newShot" : false,
7142 "speed" : 1,
7143 "srcIn" : 1865.525423728814,
7144 "start" : 576.5762711864406,
7145 "track" : "v0"
7146 },
7147 {
7148 "duration" : 5.559322033898297,
7149 "fadeIn" : 0,
7150 "fadeOut" : 0,
7151 "id" : "A57F8B52-605A-4BB2-8B24-0CBC8B78EBAB",
7152 "kind" : "audio",
7153 "linkId" : "FD6E0C12-08F2-474C-B889-10F178952804",
7154 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
7155 "muted" : true,
7156 "newShot" : false,
7157 "speed" : 1,
7158 "srcIn" : 1865.441552187473,
7159 "start" : 576.5762711864406,
7160 "track" : "v1"
7161 },
7162 {
7163 "duration" : 5.559322033898297,
7164 "fadeIn" : 0,
7165 "fadeOut" : 0,
7166 "id" : "93D09BC6-1947-40D5-82BA-E6E1035452E2",
7167 "kind" : "audio",
7168 "linkId" : "FD6E0C12-08F2-474C-B889-10F178952804",
7169 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
7170 "muted" : false,
7171 "newShot" : false,
7172 "speed" : 1,
7173 "srcIn" : 1865.488228490704,
7174 "start" : 576.5762711864406,
7175 "track" : "v2"
7176 },
7177 {
7178 "duration" : 5.559322033898297,
7179 "fadeIn" : 0,
7180 "fadeOut" : 0,
7181 "id" : "48D771B8-EC5B-45CD-842D-CFC4F85FC9D3",
7182 "kind" : "video",
7183 "linkId" : "FD6E0C12-08F2-474C-B889-10F178952804",
7184 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
7185 "muted" : false,
7186 "newShot" : false,
7187 "speed" : 1,
7188 "srcIn" : 1865.3516213544758,
7189 "start" : 576.5762711864406,
7190 "track" : "v3"
7191 },
7192 {
7193 "duration" : 5.559322033898297,
7194 "fadeIn" : 0,
7195 "fadeOut" : 0,
7196 "id" : "6F0B9EDA-F628-4403-9AC9-D23443721AA7",
7197 "kind" : "video",
7198 "linkId" : "FD6E0C12-08F2-474C-B889-10F178952804",
7199 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
7200 "muted" : false,
7201 "newShot" : false,
7202 "speed" : 1,
7203 "srcIn" : 1865.351616770477,
7204 "start" : 576.5762711864406,
7205 "track" : "v4"
7206 },
7207 {
7208 "duration" : 18.881355932203405,
7209 "fadeIn" : 0,
7210 "fadeOut" : 0,
7211 "id" : "B0DF95C7-AB53-4E4B-B365-DA47C88E233F",
7212 "kind" : "video",
7213 "linkId" : "17C6BBA2-9064-425D-8592-73511CF2CF98",
7214 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
7215 "muted" : false,
7216 "newShot" : false,
7217 "speed" : 1,
7218 "srcIn" : 1938.101694915255,
7219 "start" : 582.1355932203389,
7220 "track" : "v0"
7221 },
7222 {
7223 "duration" : 18.881355932203405,
7224 "fadeIn" : 0,
7225 "fadeOut" : 0,
7226 "id" : "10C2DDB7-13BF-4707-A836-146E1C1DF546",
7227 "kind" : "audio",
7228 "linkId" : "17C6BBA2-9064-425D-8592-73511CF2CF98",
7229 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
7230 "muted" : true,
7231 "newShot" : false,
7232 "speed" : 1,
7233 "srcIn" : 1938.0178233739139,
7234 "start" : 582.1355932203389,
7235 "track" : "v1"
7236 },
7237 {
7238 "duration" : 18.881355932203405,
7239 "fadeIn" : 0,
7240 "fadeOut" : 0,
7241 "id" : "D426D540-67E5-4B2C-8355-17C9D9356D6E",
7242 "kind" : "audio",
7243 "linkId" : "17C6BBA2-9064-425D-8592-73511CF2CF98",
7244 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
7245 "muted" : false,
7246 "newShot" : false,
7247 "speed" : 1,
7248 "srcIn" : 1938.0644996771448,
7249 "start" : 582.1355932203389,
7250 "track" : "v2"
7251 },
7252 {
7253 "duration" : 18.881355932203405,
7254 "fadeIn" : 0,
7255 "fadeOut" : 0,
7256 "id" : "4270D3EC-C6D9-44CD-BAEF-E087D96DEE47",
7257 "kind" : "video",
7258 "linkId" : "17C6BBA2-9064-425D-8592-73511CF2CF98",
7259 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
7260 "muted" : false,
7261 "newShot" : false,
7262 "speed" : 1,
7263 "srcIn" : 1937.9278925409167,
7264 "start" : 582.1355932203389,
7265 "track" : "v3"
7266 },
7267 {
7268 "duration" : 18.881355932203405,
7269 "fadeIn" : 0,
7270 "fadeOut" : 0,
7271 "id" : "4D883F8E-FE22-4FD7-80D8-05BB6DF23E69",
7272 "kind" : "video",
7273 "linkId" : "17C6BBA2-9064-425D-8592-73511CF2CF98",
7274 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
7275 "muted" : false,
7276 "newShot" : false,
7277 "speed" : 1,
7278 "srcIn" : 1937.9278879569179,
7279 "start" : 582.1355932203389,
7280 "track" : "v4"
7281 },
7282 {
7283 "duration" : 6.237288135593303,
7284 "fadeIn" : 0,
7285 "fadeOut" : 0,
7286 "id" : "73E5F742-AA2F-4AD8-89B1-D1CD80D854E7",
7287 "kind" : "video",
7288 "linkId" : "338C9A6C-870B-46F8-8240-2067335F8F8A",
7289 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
7290 "muted" : false,
7291 "newShot" : false,
7292 "speed" : 1,
7293 "srcIn" : 1956.9830508474583,
7294 "start" : 601.0169491525423,
7295 "track" : "v0"
7296 },
7297 {
7298 "duration" : 6.237288135593303,
7299 "fadeIn" : 0,
7300 "fadeOut" : 0,
7301 "id" : "01D1EA2C-B8BB-4B95-8C2F-716EE57867DE",
7302 "kind" : "audio",
7303 "linkId" : "338C9A6C-870B-46F8-8240-2067335F8F8A",
7304 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
7305 "muted" : true,
7306 "newShot" : false,
7307 "speed" : 1,
7308 "srcIn" : 1956.8991793061173,
7309 "start" : 601.0169491525423,
7310 "track" : "v1"
7311 },
7312 {
7313 "duration" : 6.237288135593303,
7314 "fadeIn" : 0,
7315 "fadeOut" : 0,
7316 "id" : "588AAA62-D950-4167-88E1-3A7E9016735E",
7317 "kind" : "audio",
7318 "linkId" : "338C9A6C-870B-46F8-8240-2067335F8F8A",
7319 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
7320 "muted" : false,
7321 "newShot" : false,
7322 "speed" : 1,
7323 "srcIn" : 1956.9458556093482,
7324 "start" : 601.0169491525423,
7325 "track" : "v2"
7326 },
7327 {
7328 "duration" : 6.237288135593303,
7329 "fadeIn" : 0,
7330 "fadeOut" : 0,
7331 "id" : "5F41A84E-E3E3-417E-B7E8-82995513D4E2",
7332 "kind" : "video",
7333 "linkId" : "338C9A6C-870B-46F8-8240-2067335F8F8A",
7334 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
7335 "muted" : false,
7336 "newShot" : false,
7337 "speed" : 1,
7338 "srcIn" : 1956.80924847312,
7339 "start" : 601.0169491525423,
7340 "track" : "v3"
7341 },
7342 {
7343 "duration" : 6.237288135593303,
7344 "fadeIn" : 0,
7345 "fadeOut" : 0,
7346 "id" : "06DE79D5-6984-41FF-8120-821047F987C5",
7347 "kind" : "video",
7348 "linkId" : "338C9A6C-870B-46F8-8240-2067335F8F8A",
7349 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
7350 "muted" : false,
7351 "newShot" : false,
7352 "speed" : 1,
7353 "srcIn" : 1956.8092438891213,
7354 "start" : 601.0169491525423,
7355 "track" : "v4"
7356 },
7357 {
7358 "duration" : 10.677966101694892,
7359 "fadeIn" : 0,
7360 "fadeOut" : 0,
7361 "id" : "F7574C8D-473A-44C7-BB33-CA9C31856531",
7362 "kind" : "video",
7363 "linkId" : "5715D6B2-69B7-4806-A021-82A001C82B8D",
7364 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
7365 "muted" : false,
7366 "newShot" : false,
7367 "speed" : 1,
7368 "srcIn" : 2022.8474576271192,
7369 "start" : 607.2542372881356,
7370 "track" : "v0"
7371 },
7372 {
7373 "duration" : 10.677966101694892,
7374 "fadeIn" : 0,
7375 "fadeOut" : 0,
7376 "id" : "6F7C3CE2-9238-415C-A314-90B677ECAE17",
7377 "kind" : "audio",
7378 "linkId" : "5715D6B2-69B7-4806-A021-82A001C82B8D",
7379 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
7380 "muted" : true,
7381 "newShot" : false,
7382 "speed" : 1,
7383 "srcIn" : 2022.7635860857781,
7384 "start" : 607.2542372881356,
7385 "track" : "v1"
7386 },
7387 {
7388 "duration" : 10.677966101694892,
7389 "fadeIn" : 0,
7390 "fadeOut" : 0,
7391 "id" : "BC1D7F8D-872D-4749-97D4-9CBF947EB008",
7392 "kind" : "audio",
7393 "linkId" : "5715D6B2-69B7-4806-A021-82A001C82B8D",
7394 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
7395 "muted" : false,
7396 "newShot" : false,
7397 "speed" : 1,
7398 "srcIn" : 2022.810262389009,
7399 "start" : 607.2542372881356,
7400 "track" : "v2"
7401 },
7402 {
7403 "duration" : 10.677966101694892,
7404 "fadeIn" : 0,
7405 "fadeOut" : 0,
7406 "id" : "5AC16FB7-ABB0-462A-926D-6657645660DB",
7407 "kind" : "video",
7408 "linkId" : "5715D6B2-69B7-4806-A021-82A001C82B8D",
7409 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
7410 "muted" : false,
7411 "newShot" : false,
7412 "speed" : 1,
7413 "srcIn" : 2022.673655252781,
7414 "start" : 607.2542372881356,
7415 "track" : "v3"
7416 },
7417 {
7418 "duration" : 10.677966101694892,
7419 "fadeIn" : 0,
7420 "fadeOut" : 0,
7421 "id" : "7EC1D808-B244-481D-AEED-29FBAD17A539",
7422 "kind" : "video",
7423 "linkId" : "5715D6B2-69B7-4806-A021-82A001C82B8D",
7424 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
7425 "muted" : false,
7426 "newShot" : false,
7427 "speed" : 1,
7428 "srcIn" : 2022.673650668782,
7429 "start" : 607.2542372881356,
7430 "track" : "v4"
7431 },
7432 {
7433 "duration" : 3.627118644067764,
7434 "fadeIn" : 0,
7435 "fadeOut" : 0,
7436 "id" : "60AAA140-4B79-4568-92FB-E8B2070677B4",
7437 "kind" : "video",
7438 "linkId" : "7A62ADBD-0427-472A-880B-89AAE3A2A8F4",
7439 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
7440 "muted" : false,
7441 "newShot" : false,
7442 "speed" : 1,
7443 "srcIn" : 2045.2881355932209,
7444 "start" : 617.9322033898305,
7445 "track" : "v0"
7446 },
7447 {
7448 "duration" : 3.627118644067764,
7449 "fadeIn" : 0,
7450 "fadeOut" : 0,
7451 "id" : "AA1ADCAE-DA36-4FF1-A7CC-6C3B75A693B7",
7452 "kind" : "audio",
7453 "linkId" : "7A62ADBD-0427-472A-880B-89AAE3A2A8F4",
7454 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
7455 "muted" : true,
7456 "newShot" : false,
7457 "speed" : 1,
7458 "srcIn" : 2045.2042640518798,
7459 "start" : 617.9322033898305,
7460 "track" : "v1"
7461 },
7462 {
7463 "duration" : 3.627118644067764,
7464 "fadeIn" : 0,
7465 "fadeOut" : 0,
7466 "id" : "C3588EA8-F756-402E-B6EC-A9C9772EB504",
7467 "kind" : "audio",
7468 "linkId" : "7A62ADBD-0427-472A-880B-89AAE3A2A8F4",
7469 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
7470 "muted" : false,
7471 "newShot" : false,
7472 "speed" : 1,
7473 "srcIn" : 2045.2509403551107,
7474 "start" : 617.9322033898305,
7475 "track" : "v2"
7476 },
7477 {
7478 "duration" : 3.627118644067764,
7479 "fadeIn" : 0,
7480 "fadeOut" : 0,
7481 "id" : "8A86F818-17D6-4DA6-806A-BD6E61EDD16A",
7482 "kind" : "video",
7483 "linkId" : "7A62ADBD-0427-472A-880B-89AAE3A2A8F4",
7484 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
7485 "muted" : false,
7486 "newShot" : false,
7487 "speed" : 1,
7488 "srcIn" : 2045.1143332188826,
7489 "start" : 617.9322033898305,
7490 "track" : "v3"
7491 },
7492 {
7493 "duration" : 3.627118644067764,
7494 "fadeIn" : 0,
7495 "fadeOut" : 0,
7496 "id" : "DF8175BF-3E2D-4B78-8761-2798CBEEB331",
7497 "kind" : "video",
7498 "linkId" : "7A62ADBD-0427-472A-880B-89AAE3A2A8F4",
7499 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
7500 "muted" : false,
7501 "newShot" : false,
7502 "speed" : 1,
7503 "srcIn" : 2045.1143286348838,
7504 "start" : 617.9322033898305,
7505 "track" : "v4"
7506 },
7507 {
7508 "duration" : 2.4067796610169125,
7509 "fadeIn" : 0,
7510 "fadeOut" : 0,
7511 "id" : "0079348D-917B-4B45-A779-BE7BDA967808",
7512 "kind" : "video",
7513 "linkId" : "B5A3F4FE-CCAA-45AE-A6FB-0579F3BF1536",
7514 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
7515 "muted" : false,
7516 "newShot" : false,
7517 "speed" : 1,
7518 "srcIn" : 2050.542372881356,
7519 "start" : 621.5593220338983,
7520 "track" : "v0"
7521 },
7522 {
7523 "duration" : 2.4067796610169125,
7524 "fadeIn" : 0,
7525 "fadeOut" : 0,
7526 "id" : "2D8FE96C-46AE-4C66-B4A5-30D8857C61FB",
7527 "kind" : "audio",
7528 "linkId" : "B5A3F4FE-CCAA-45AE-A6FB-0579F3BF1536",
7529 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
7530 "muted" : true,
7531 "newShot" : false,
7532 "speed" : 1,
7533 "srcIn" : 2050.458501340015,
7534 "start" : 621.5593220338983,
7535 "track" : "v1"
7536 },
7537 {
7538 "duration" : 2.4067796610169125,
7539 "fadeIn" : 0,
7540 "fadeOut" : 0,
7541 "id" : "B675F446-CEBF-4F4A-B344-F62184FADF3B",
7542 "kind" : "audio",
7543 "linkId" : "B5A3F4FE-CCAA-45AE-A6FB-0579F3BF1536",
7544 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
7545 "muted" : false,
7546 "newShot" : false,
7547 "speed" : 1,
7548 "srcIn" : 2050.505177643246,
7549 "start" : 621.5593220338983,
7550 "track" : "v2"
7551 },
7552 {
7553 "duration" : 2.4067796610169125,
7554 "fadeIn" : 0,
7555 "fadeOut" : 0,
7556 "id" : "C21A6286-77CE-4A32-B617-2710BCE8CBD9",
7557 "kind" : "video",
7558 "linkId" : "B5A3F4FE-CCAA-45AE-A6FB-0579F3BF1536",
7559 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
7560 "muted" : false,
7561 "newShot" : false,
7562 "speed" : 1,
7563 "srcIn" : 2050.368570507018,
7564 "start" : 621.5593220338983,
7565 "track" : "v3"
7566 },
7567 {
7568 "duration" : 2.4067796610169125,
7569 "fadeIn" : 0,
7570 "fadeOut" : 0,
7571 "id" : "64279978-B9D7-4E8D-AFED-D7913E1B9532",
7572 "kind" : "video",
7573 "linkId" : "B5A3F4FE-CCAA-45AE-A6FB-0579F3BF1536",
7574 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
7575 "muted" : false,
7576 "newShot" : false,
7577 "speed" : 1,
7578 "srcIn" : 2050.368565923019,
7579 "start" : 621.5593220338983,
7580 "track" : "v4"
7581 },
7582 {
7583 "duration" : 3.4237288135593644,
7584 "fadeIn" : 0,
7585 "fadeOut" : 0,
7586 "id" : "A040FD6A-EC96-415F-8D28-7B4691F99BF3",
7587 "kind" : "video",
7588 "linkId" : "0F6F0554-6C53-4419-B9B3-D9C2EE7D2267",
7589 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
7590 "muted" : false,
7591 "newShot" : false,
7592 "speed" : 1,
7593 "srcIn" : 2052.949152542373,
7594 "start" : 623.9661016949152,
7595 "track" : "v0"
7596 },
7597 {
7598 "duration" : 3.4237288135593644,
7599 "fadeIn" : 0,
7600 "fadeOut" : 0,
7601 "id" : "6AC76567-F75D-4A33-B204-A10F431BBFDD",
7602 "kind" : "audio",
7603 "linkId" : "0F6F0554-6C53-4419-B9B3-D9C2EE7D2267",
7604 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
7605 "muted" : true,
7606 "newShot" : false,
7607 "speed" : 1,
7608 "srcIn" : 2052.865281001032,
7609 "start" : 623.9661016949152,
7610 "track" : "v1"
7611 },
7612 {
7613 "duration" : 3.4237288135593644,
7614 "fadeIn" : 0,
7615 "fadeOut" : 0,
7616 "id" : "FA9C2797-0EB0-4B75-A617-5D4CF590AC43",
7617 "kind" : "audio",
7618 "linkId" : "0F6F0554-6C53-4419-B9B3-D9C2EE7D2267",
7619 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
7620 "muted" : false,
7621 "newShot" : false,
7622 "speed" : 1,
7623 "srcIn" : 2052.911957304263,
7624 "start" : 623.9661016949152,
7625 "track" : "v2"
7626 },
7627 {
7628 "duration" : 3.4237288135593644,
7629 "fadeIn" : 0,
7630 "fadeOut" : 0,
7631 "id" : "429309C1-5820-4F55-97F5-8FBD04302BA5",
7632 "kind" : "video",
7633 "linkId" : "0F6F0554-6C53-4419-B9B3-D9C2EE7D2267",
7634 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
7635 "muted" : false,
7636 "newShot" : false,
7637 "speed" : 1,
7638 "srcIn" : 2052.775350168035,
7639 "start" : 623.9661016949152,
7640 "track" : "v3"
7641 },
7642 {
7643 "duration" : 3.4237288135593644,
7644 "fadeIn" : 0,
7645 "fadeOut" : 0,
7646 "id" : "89C2D014-50A7-43ED-AE56-0AA708FA28ED",
7647 "kind" : "video",
7648 "linkId" : "0F6F0554-6C53-4419-B9B3-D9C2EE7D2267",
7649 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
7650 "muted" : false,
7651 "newShot" : false,
7652 "speed" : 1,
7653 "srcIn" : 2052.775345584036,
7654 "start" : 623.9661016949152,
7655 "track" : "v4"
7656 },
7657 {
7658 "duration" : 5.69491525423723,
7659 "fadeIn" : 0,
7660 "fadeOut" : 0,
7661 "id" : "01E331DE-F3BA-42B2-B95C-4FA7802600C4",
7662 "kind" : "video",
7663 "linkId" : "681599CE-614B-4D54-9956-0A2F59A0C020",
7664 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
7665 "muted" : false,
7666 "newShot" : false,
7667 "speed" : 1,
7668 "srcIn" : 2114.6779661016953,
7669 "start" : 627.3898305084746,
7670 "track" : "v0"
7671 },
7672 {
7673 "duration" : 5.69491525423723,
7674 "fadeIn" : 0,
7675 "fadeOut" : 0,
7676 "id" : "602A3DDB-AB74-45B5-8FAE-A660E6CA2700",
7677 "kind" : "audio",
7678 "linkId" : "681599CE-614B-4D54-9956-0A2F59A0C020",
7679 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
7680 "muted" : true,
7681 "newShot" : false,
7682 "speed" : 1,
7683 "srcIn" : 2114.5940945603543,
7684 "start" : 627.3898305084746,
7685 "track" : "v1"
7686 },
7687 {
7688 "duration" : 5.69491525423723,
7689 "fadeIn" : 0,
7690 "fadeOut" : 0,
7691 "id" : "0BA636D9-34A5-47E7-9BC5-0BDD5579680E",
7692 "kind" : "audio",
7693 "linkId" : "681599CE-614B-4D54-9956-0A2F59A0C020",
7694 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
7695 "muted" : false,
7696 "newShot" : false,
7697 "speed" : 1,
7698 "srcIn" : 2114.640770863585,
7699 "start" : 627.3898305084746,
7700 "track" : "v2"
7701 },
7702 {
7703 "duration" : 5.69491525423723,
7704 "fadeIn" : 0,
7705 "fadeOut" : 0,
7706 "id" : "4E01905E-2230-4C63-9855-875F6C89C92B",
7707 "kind" : "video",
7708 "linkId" : "681599CE-614B-4D54-9956-0A2F59A0C020",
7709 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
7710 "muted" : false,
7711 "newShot" : false,
7712 "speed" : 1,
7713 "srcIn" : 2114.504163727357,
7714 "start" : 627.3898305084746,
7715 "track" : "v3"
7716 },
7717 {
7718 "duration" : 5.69491525423723,
7719 "fadeIn" : 0,
7720 "fadeOut" : 0,
7721 "id" : "78183D76-36AF-4F77-8481-6336BB1F1340",
7722 "kind" : "video",
7723 "linkId" : "681599CE-614B-4D54-9956-0A2F59A0C020",
7724 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
7725 "muted" : false,
7726 "newShot" : false,
7727 "speed" : 1,
7728 "srcIn" : 2114.5041591433583,
7729 "start" : 627.3898305084746,
7730 "track" : "v4"
7731 },
7732 {
7733 "duration" : 15.76271186440681,
7734 "fadeIn" : 0,
7735 "fadeOut" : 0,
7736 "id" : "581AB06A-CA87-43B3-9083-3BF4CA1B488F",
7737 "kind" : "video",
7738 "linkId" : "54C20EBF-C3C2-46DA-929B-9C51CBA0B7D6",
7739 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
7740 "muted" : false,
7741 "newShot" : false,
7742 "speed" : 1,
7743 "srcIn" : 2230.1694915254243,
7744 "start" : 633.0847457627118,
7745 "track" : "v0"
7746 },
7747 {
7748 "duration" : 15.76271186440681,
7749 "fadeIn" : 0,
7750 "fadeOut" : 0,
7751 "id" : "39BA14C0-0C38-4545-B0B0-C33EB72363EF",
7752 "kind" : "audio",
7753 "linkId" : "54C20EBF-C3C2-46DA-929B-9C51CBA0B7D6",
7754 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
7755 "muted" : true,
7756 "newShot" : false,
7757 "speed" : 1,
7758 "srcIn" : 2230.085619984083,
7759 "start" : 633.0847457627118,
7760 "track" : "v1"
7761 },
7762 {
7763 "duration" : 15.76271186440681,
7764 "fadeIn" : 0,
7765 "fadeOut" : 0,
7766 "id" : "2161011F-CC8E-4C61-8661-7A947C751069",
7767 "kind" : "audio",
7768 "linkId" : "54C20EBF-C3C2-46DA-929B-9C51CBA0B7D6",
7769 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
7770 "muted" : false,
7771 "newShot" : false,
7772 "speed" : 1,
7773 "srcIn" : 2230.132296287314,
7774 "start" : 633.0847457627118,
7775 "track" : "v2"
7776 },
7777 {
7778 "duration" : 15.76271186440681,
7779 "fadeIn" : 0,
7780 "fadeOut" : 0,
7781 "id" : "7265B315-57C1-47FB-A27C-3AB7B8CA1004",
7782 "kind" : "video",
7783 "linkId" : "54C20EBF-C3C2-46DA-929B-9C51CBA0B7D6",
7784 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
7785 "muted" : false,
7786 "newShot" : false,
7787 "speed" : 1,
7788 "srcIn" : 2229.995689151086,
7789 "start" : 633.0847457627118,
7790 "track" : "v3"
7791 },
7792 {
7793 "duration" : 15.76271186440681,
7794 "fadeIn" : 0,
7795 "fadeOut" : 0,
7796 "id" : "EFC80E51-0F05-4B16-8596-E78F73CB045F",
7797 "kind" : "video",
7798 "linkId" : "54C20EBF-C3C2-46DA-929B-9C51CBA0B7D6",
7799 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
7800 "muted" : false,
7801 "newShot" : false,
7802 "speed" : 1,
7803 "srcIn" : 2229.995684567087,
7804 "start" : 633.0847457627118,
7805 "track" : "v4"
7806 },
7807 {
7808 "duration" : 3.3898305084745743,
7809 "fadeIn" : 0,
7810 "fadeOut" : 0,
7811 "id" : "8BD87ADB-1AA9-4B14-9370-4DF20E448896",
7812 "kind" : "video",
7813 "linkId" : "C2740056-FAEB-4E91-9E00-112253797C46",
7814 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
7815 "muted" : false,
7816 "newShot" : false,
7817 "speed" : 1,
7818 "srcIn" : 2256.372881355933,
7819 "start" : 648.8474576271186,
7820 "track" : "v0"
7821 },
7822 {
7823 "duration" : 3.3898305084745743,
7824 "fadeIn" : 0,
7825 "fadeOut" : 0,
7826 "id" : "7A48AFF3-E08F-437C-AC57-92133246569E",
7827 "kind" : "audio",
7828 "linkId" : "C2740056-FAEB-4E91-9E00-112253797C46",
7829 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
7830 "muted" : true,
7831 "newShot" : false,
7832 "speed" : 1,
7833 "srcIn" : 2256.2890098145917,
7834 "start" : 648.8474576271186,
7835 "track" : "v1"
7836 },
7837 {
7838 "duration" : 3.3898305084745743,
7839 "fadeIn" : 0,
7840 "fadeOut" : 0,
7841 "id" : "E427E26F-1FDB-4742-A1E4-015E32255D38",
7842 "kind" : "audio",
7843 "linkId" : "C2740056-FAEB-4E91-9E00-112253797C46",
7844 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
7845 "muted" : false,
7846 "newShot" : false,
7847 "speed" : 1,
7848 "srcIn" : 2256.3356861178227,
7849 "start" : 648.8474576271186,
7850 "track" : "v2"
7851 },
7852 {
7853 "duration" : 3.3898305084745743,
7854 "fadeIn" : 0,
7855 "fadeOut" : 0,
7856 "id" : "107EE1C9-66CC-4E8E-9743-C6900E649621",
7857 "kind" : "video",
7858 "linkId" : "C2740056-FAEB-4E91-9E00-112253797C46",
7859 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
7860 "muted" : false,
7861 "newShot" : false,
7862 "speed" : 1,
7863 "srcIn" : 2256.1990789815945,
7864 "start" : 648.8474576271186,
7865 "track" : "v3"
7866 },
7867 {
7868 "duration" : 3.3898305084745743,
7869 "fadeIn" : 0,
7870 "fadeOut" : 0,
7871 "id" : "D7AAC444-EA58-4486-9341-92E3C325D17F",
7872 "kind" : "video",
7873 "linkId" : "C2740056-FAEB-4E91-9E00-112253797C46",
7874 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
7875 "muted" : false,
7876 "newShot" : false,
7877 "speed" : 1,
7878 "srcIn" : 2256.1990743975957,
7879 "start" : 648.8474576271186,
7880 "track" : "v4"
7881 },
7882 {
7883 "duration" : 15.423728813559364,
7884 "fadeIn" : 0,
7885 "fadeOut" : 0,
7886 "id" : "4754E275-63E9-4C43-8DBD-3771650BDEB8",
7887 "kind" : "video",
7888 "linkId" : "FF0D729E-3013-4F28-A488-DBD70296587F",
7889 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
7890 "muted" : false,
7891 "newShot" : false,
7892 "speed" : 1,
7893 "srcIn" : 2280.9491525423737,
7894 "start" : 652.2372881355932,
7895 "track" : "v0"
7896 },
7897 {
7898 "duration" : 15.423728813559364,
7899 "fadeIn" : 0,
7900 "fadeOut" : 0,
7901 "id" : "A8BCA6E8-0804-4D39-8229-30DF2DFBF320",
7902 "kind" : "audio",
7903 "linkId" : "FF0D729E-3013-4F28-A488-DBD70296587F",
7904 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
7905 "muted" : true,
7906 "newShot" : false,
7907 "speed" : 1,
7908 "srcIn" : 2280.8652810010326,
7909 "start" : 652.2372881355932,
7910 "track" : "v1"
7911 },
7912 {
7913 "duration" : 15.423728813559364,
7914 "fadeIn" : 0,
7915 "fadeOut" : 0,
7916 "id" : "2A8D5833-B3BC-4A82-A7E7-6319DFF51DC3",
7917 "kind" : "audio",
7918 "linkId" : "FF0D729E-3013-4F28-A488-DBD70296587F",
7919 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
7920 "muted" : false,
7921 "newShot" : false,
7922 "speed" : 1,
7923 "srcIn" : 2280.9119573042635,
7924 "start" : 652.2372881355932,
7925 "track" : "v2"
7926 },
7927 {
7928 "duration" : 15.423728813559364,
7929 "fadeIn" : 0,
7930 "fadeOut" : 0,
7931 "id" : "B8963AE1-23D3-4C35-B241-00A5396F4DD7",
7932 "kind" : "video",
7933 "linkId" : "FF0D729E-3013-4F28-A488-DBD70296587F",
7934 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
7935 "muted" : false,
7936 "newShot" : false,
7937 "speed" : 1,
7938 "srcIn" : 2280.7753501680354,
7939 "start" : 652.2372881355932,
7940 "track" : "v3"
7941 },
7942 {
7943 "duration" : 15.423728813559364,
7944 "fadeIn" : 0,
7945 "fadeOut" : 0,
7946 "id" : "1FC80A57-7A45-4C7D-9200-0C1A48A3D6F5",
7947 "kind" : "video",
7948 "linkId" : "FF0D729E-3013-4F28-A488-DBD70296587F",
7949 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
7950 "muted" : false,
7951 "newShot" : false,
7952 "speed" : 1,
7953 "srcIn" : 2280.7753455840366,
7954 "start" : 652.2372881355932,
7955 "track" : "v4"
7956 },
7957 {
7958 "duration" : 12.745762711864359,
7959 "fadeIn" : 0,
7960 "fadeOut" : 0,
7961 "id" : "0CA818E8-62B3-474A-8050-49368421A90E",
7962 "kind" : "video",
7963 "linkId" : "C5B9230A-8789-4FD4-B107-6702F012D99A",
7964 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
7965 "muted" : false,
7966 "newShot" : false,
7967 "speed" : 1,
7968 "srcIn" : 2296.372881355933,
7969 "start" : 667.6610169491526,
7970 "track" : "v0"
7971 },
7972 {
7973 "duration" : 12.745762711864359,
7974 "fadeIn" : 0,
7975 "fadeOut" : 0,
7976 "id" : "EB3722EC-6C38-40B2-B519-694180267E3A",
7977 "kind" : "audio",
7978 "linkId" : "C5B9230A-8789-4FD4-B107-6702F012D99A",
7979 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
7980 "muted" : true,
7981 "newShot" : false,
7982 "speed" : 1,
7983 "srcIn" : 2296.2890098145917,
7984 "start" : 667.6610169491526,
7985 "track" : "v1"
7986 },
7987 {
7988 "duration" : 12.745762711864359,
7989 "fadeIn" : 0,
7990 "fadeOut" : 0,
7991 "id" : "A797E488-873A-4FC3-816A-D61A73E2F1D9",
7992 "kind" : "audio",
7993 "linkId" : "C5B9230A-8789-4FD4-B107-6702F012D99A",
7994 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
7995 "muted" : false,
7996 "newShot" : false,
7997 "speed" : 1,
7998 "srcIn" : 2296.3356861178227,
7999 "start" : 667.6610169491526,
8000 "track" : "v2"
8001 },
8002 {
8003 "duration" : 12.745762711864359,
8004 "fadeIn" : 0,
8005 "fadeOut" : 0,
8006 "id" : "75FC92A1-B149-4AB0-A366-35F476711F59",
8007 "kind" : "video",
8008 "linkId" : "C5B9230A-8789-4FD4-B107-6702F012D99A",
8009 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
8010 "muted" : false,
8011 "newShot" : false,
8012 "speed" : 1,
8013 "srcIn" : 2296.1990789815945,
8014 "start" : 667.6610169491526,
8015 "track" : "v3"
8016 },
8017 {
8018 "duration" : 12.745762711864359,
8019 "fadeIn" : 0,
8020 "fadeOut" : 0,
8021 "id" : "5B3388A0-5AC5-481F-8DD8-BED12840C1D9",
8022 "kind" : "video",
8023 "linkId" : "C5B9230A-8789-4FD4-B107-6702F012D99A",
8024 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
8025 "muted" : false,
8026 "newShot" : false,
8027 "speed" : 1,
8028 "srcIn" : 2296.1990743975957,
8029 "start" : 667.6610169491526,
8030 "track" : "v4"
8031 },
8032 {
8033 "duration" : 4.677966101695006,
8034 "fadeIn" : 0,
8035 "fadeOut" : 0,
8036 "id" : "5A8649A2-00AB-447F-BC8C-3085E8695184",
8037 "kind" : "video",
8038 "linkId" : "2782DABC-F157-44FA-B15C-F2D70A28D22C",
8039 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
8040 "muted" : false,
8041 "newShot" : false,
8042 "speed" : 1,
8043 "srcIn" : 2313.254237288136,
8044 "start" : 680.4067796610169,
8045 "track" : "v0"
8046 },
8047 {
8048 "duration" : 4.677966101695006,
8049 "fadeIn" : 0,
8050 "fadeOut" : 0,
8051 "id" : "EAC07860-D644-40FF-9A4D-08BD56F332C4",
8052 "kind" : "audio",
8053 "linkId" : "2782DABC-F157-44FA-B15C-F2D70A28D22C",
8054 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
8055 "muted" : true,
8056 "newShot" : false,
8057 "speed" : 1,
8058 "srcIn" : 2313.170365746795,
8059 "start" : 680.4067796610169,
8060 "track" : "v1"
8061 },
8062 {
8063 "duration" : 4.677966101695006,
8064 "fadeIn" : 0,
8065 "fadeOut" : 0,
8066 "id" : "0950EA6E-C41F-453A-9C62-F24BC14CD76E",
8067 "kind" : "audio",
8068 "linkId" : "2782DABC-F157-44FA-B15C-F2D70A28D22C",
8069 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
8070 "muted" : false,
8071 "newShot" : false,
8072 "speed" : 1,
8073 "srcIn" : 2313.217042050026,
8074 "start" : 680.4067796610169,
8075 "track" : "v2"
8076 },
8077 {
8078 "duration" : 4.677966101695006,
8079 "fadeIn" : 0,
8080 "fadeOut" : 0,
8081 "id" : "C0EBCCE6-06A8-4958-8643-35B908EA7C98",
8082 "kind" : "video",
8083 "linkId" : "2782DABC-F157-44FA-B15C-F2D70A28D22C",
8084 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
8085 "muted" : false,
8086 "newShot" : false,
8087 "speed" : 1,
8088 "srcIn" : 2313.080434913798,
8089 "start" : 680.4067796610169,
8090 "track" : "v3"
8091 },
8092 {
8093 "duration" : 4.677966101695006,
8094 "fadeIn" : 0,
8095 "fadeOut" : 0,
8096 "id" : "7D753BA0-2851-4A2B-B1AB-52234E029F7C",
8097 "kind" : "video",
8098 "linkId" : "2782DABC-F157-44FA-B15C-F2D70A28D22C",
8099 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
8100 "muted" : false,
8101 "newShot" : false,
8102 "speed" : 1,
8103 "srcIn" : 2313.080430329799,
8104 "start" : 680.4067796610169,
8105 "track" : "v4"
8106 },
8107 {
8108 "duration" : 19.186440677966175,
8109 "fadeIn" : 0,
8110 "fadeOut" : 0,
8111 "id" : "B4AB14C4-4491-499A-8505-0934B993F07B",
8112 "kind" : "video",
8113 "linkId" : "028E7FFC-FA84-47CF-8683-266C5E78C890",
8114 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
8115 "muted" : false,
8116 "newShot" : false,
8117 "speed" : 1,
8118 "srcIn" : 2414.6440677966107,
8119 "start" : 685.0847457627119,
8120 "track" : "v0"
8121 },
8122 {
8123 "duration" : 19.186440677966175,
8124 "fadeIn" : 0,
8125 "fadeOut" : 0,
8126 "id" : "36653084-A594-47D9-82C7-05282A42CE6C",
8127 "kind" : "audio",
8128 "linkId" : "028E7FFC-FA84-47CF-8683-266C5E78C890",
8129 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
8130 "muted" : true,
8131 "newShot" : false,
8132 "speed" : 1,
8133 "srcIn" : 2414.5601962552696,
8134 "start" : 685.0847457627119,
8135 "track" : "v1"
8136 },
8137 {
8138 "duration" : 19.186440677966175,
8139 "fadeIn" : 0,
8140 "fadeOut" : 0,
8141 "id" : "5A23146F-8246-498B-AC51-C4FE4B6C4DDA",
8142 "kind" : "audio",
8143 "linkId" : "028E7FFC-FA84-47CF-8683-266C5E78C890",
8144 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
8145 "muted" : false,
8146 "newShot" : false,
8147 "speed" : 1,
8148 "srcIn" : 2414.6068725585005,
8149 "start" : 685.0847457627119,
8150 "track" : "v2"
8151 },
8152 {
8153 "duration" : 19.186440677966175,
8154 "fadeIn" : 0,
8155 "fadeOut" : 0,
8156 "id" : "C7D8CAF3-5789-4F76-924A-24654359FD7F",
8157 "kind" : "video",
8158 "linkId" : "028E7FFC-FA84-47CF-8683-266C5E78C890",
8159 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
8160 "muted" : false,
8161 "newShot" : false,
8162 "speed" : 1,
8163 "srcIn" : 2414.4702654222724,
8164 "start" : 685.0847457627119,
8165 "track" : "v3"
8166 },
8167 {
8168 "duration" : 19.186440677966175,
8169 "fadeIn" : 0,
8170 "fadeOut" : 0,
8171 "id" : "DFB2B3EE-B824-4672-A351-269C20BF2ADE",
8172 "kind" : "video",
8173 "linkId" : "028E7FFC-FA84-47CF-8683-266C5E78C890",
8174 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
8175 "muted" : false,
8176 "newShot" : false,
8177 "speed" : 1,
8178 "srcIn" : 2414.4702608382736,
8179 "start" : 685.0847457627119,
8180 "track" : "v4"
8181 },
8182 {
8183 "duration" : 5.694915254237344,
8184 "fadeIn" : 0,
8185 "fadeOut" : 0,
8186 "id" : "EB2F9D36-DD5F-468C-9DA8-90D56D42E0A9",
8187 "kind" : "video",
8188 "linkId" : "DDA38179-F497-4A55-8BF0-09672B42936C",
8189 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
8190 "muted" : false,
8191 "newShot" : false,
8192 "speed" : 1,
8193 "srcIn" : 2446.1355932203396,
8194 "start" : 704.2711864406781,
8195 "track" : "v0"
8196 },
8197 {
8198 "duration" : 5.694915254237344,
8199 "fadeIn" : 0,
8200 "fadeOut" : 0,
8201 "id" : "D07C45CE-44A0-4FEF-8F34-54E4FD3F7D39",
8202 "kind" : "audio",
8203 "linkId" : "DDA38179-F497-4A55-8BF0-09672B42936C",
8204 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
8205 "muted" : true,
8206 "newShot" : false,
8207 "speed" : 1,
8208 "srcIn" : 2446.0517216789985,
8209 "start" : 704.2711864406781,
8210 "track" : "v1"
8211 },
8212 {
8213 "duration" : 5.694915254237344,
8214 "fadeIn" : 0,
8215 "fadeOut" : 0,
8216 "id" : "A22E3A44-8F76-4713-9D7F-7948A74D9CCC",
8217 "kind" : "audio",
8218 "linkId" : "DDA38179-F497-4A55-8BF0-09672B42936C",
8219 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
8220 "muted" : false,
8221 "newShot" : false,
8222 "speed" : 1,
8223 "srcIn" : 2446.0983979822295,
8224 "start" : 704.2711864406781,
8225 "track" : "v2"
8226 },
8227 {
8228 "duration" : 5.694915254237344,
8229 "fadeIn" : 0,
8230 "fadeOut" : 0,
8231 "id" : "15C53598-6F8F-4C89-B6EE-8BEAEA74F23A",
8232 "kind" : "video",
8233 "linkId" : "DDA38179-F497-4A55-8BF0-09672B42936C",
8234 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
8235 "muted" : false,
8236 "newShot" : false,
8237 "speed" : 1,
8238 "srcIn" : 2445.9617908460013,
8239 "start" : 704.2711864406781,
8240 "track" : "v3"
8241 },
8242 {
8243 "duration" : 5.694915254237344,
8244 "fadeIn" : 0,
8245 "fadeOut" : 0,
8246 "id" : "FB595553-A66D-4B22-8173-3AAE10CB58D0",
8247 "kind" : "video",
8248 "linkId" : "DDA38179-F497-4A55-8BF0-09672B42936C",
8249 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
8250 "muted" : false,
8251 "newShot" : false,
8252 "speed" : 1,
8253 "srcIn" : 2445.9617862620025,
8254 "start" : 704.2711864406781,
8255 "track" : "v4"
8256 },
8257 {
8258 "duration" : 9.59322033898286,
8259 "fadeIn" : 0,
8260 "fadeOut" : 0,
8261 "id" : "815EF8E2-F1E8-4B38-829B-D2C6971FDCD7",
8262 "kind" : "video",
8263 "linkId" : "1D15D7CC-5494-459C-BAF8-DD757B81547D",
8264 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
8265 "muted" : false,
8266 "newShot" : false,
8267 "speed" : 1,
8268 "srcIn" : 2454.1355932203396,
8269 "start" : 709.9661016949154,
8270 "track" : "v0"
8271 },
8272 {
8273 "duration" : 9.59322033898286,
8274 "fadeIn" : 0,
8275 "fadeOut" : 0,
8276 "id" : "6599B7E9-8BC7-4339-8544-580C7E40A6B3",
8277 "kind" : "audio",
8278 "linkId" : "1D15D7CC-5494-459C-BAF8-DD757B81547D",
8279 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
8280 "muted" : true,
8281 "newShot" : false,
8282 "speed" : 1,
8283 "srcIn" : 2454.0517216789985,
8284 "start" : 709.9661016949154,
8285 "track" : "v1"
8286 },
8287 {
8288 "duration" : 9.59322033898286,
8289 "fadeIn" : 0,
8290 "fadeOut" : 0,
8291 "id" : "913AD485-580A-4CF5-832E-3A866323A594",
8292 "kind" : "audio",
8293 "linkId" : "1D15D7CC-5494-459C-BAF8-DD757B81547D",
8294 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
8295 "muted" : false,
8296 "newShot" : false,
8297 "speed" : 1,
8298 "srcIn" : 2454.0983979822295,
8299 "start" : 709.9661016949154,
8300 "track" : "v2"
8301 },
8302 {
8303 "duration" : 9.59322033898286,
8304 "fadeIn" : 0,
8305 "fadeOut" : 0,
8306 "id" : "3F04DEF9-CD7C-486B-913A-989ECE77DE57",
8307 "kind" : "video",
8308 "linkId" : "1D15D7CC-5494-459C-BAF8-DD757B81547D",
8309 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
8310 "muted" : false,
8311 "newShot" : false,
8312 "speed" : 1,
8313 "srcIn" : 2453.9617908460013,
8314 "start" : 709.9661016949154,
8315 "track" : "v3"
8316 },
8317 {
8318 "duration" : 9.59322033898286,
8319 "fadeIn" : 0,
8320 "fadeOut" : 0,
8321 "id" : "715FA793-E1B3-4C26-B441-31D597B1938B",
8322 "kind" : "video",
8323 "linkId" : "1D15D7CC-5494-459C-BAF8-DD757B81547D",
8324 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
8325 "muted" : false,
8326 "newShot" : false,
8327 "speed" : 1,
8328 "srcIn" : 2453.9617862620025,
8329 "start" : 709.9661016949154,
8330 "track" : "v4"
8331 },
8332 {
8333 "duration" : 14.474576271186379,
8334 "fadeIn" : 0,
8335 "fadeOut" : 0,
8336 "id" : "40CCDD9D-5568-40CA-AEF5-90D94B8580BB",
8337 "kind" : "video",
8338 "linkId" : "F3271449-9FF2-4A35-8582-579E52CA2EFC",
8339 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
8340 "muted" : false,
8341 "newShot" : false,
8342 "speed" : 1,
8343 "srcIn" : 2526.6779661016953,
8344 "start" : 723.7627118644068,
8345 "track" : "v0"
8346 },
8347 {
8348 "duration" : 14.474576271186379,
8349 "fadeIn" : 0,
8350 "fadeOut" : 0,
8351 "id" : "C9DD81B8-F688-49A6-8638-2A626AF597C9",
8352 "kind" : "audio",
8353 "linkId" : "F3271449-9FF2-4A35-8582-579E52CA2EFC",
8354 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
8355 "muted" : true,
8356 "newShot" : false,
8357 "speed" : 1,
8358 "srcIn" : 2526.5940945603543,
8359 "start" : 723.7627118644068,
8360 "track" : "v1"
8361 },
8362 {
8363 "duration" : 14.474576271186379,
8364 "fadeIn" : 0,
8365 "fadeOut" : 0,
8366 "id" : "BC7A036D-1FA0-4738-A7F4-AD8EC3488B82",
8367 "kind" : "audio",
8368 "linkId" : "F3271449-9FF2-4A35-8582-579E52CA2EFC",
8369 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
8370 "muted" : false,
8371 "newShot" : false,
8372 "speed" : 1,
8373 "srcIn" : 2526.640770863585,
8374 "start" : 723.7627118644068,
8375 "track" : "v2"
8376 },
8377 {
8378 "duration" : 14.474576271186379,
8379 "fadeIn" : 0,
8380 "fadeOut" : 0,
8381 "id" : "A34F945B-0445-4497-9DD0-1E68E72ADA27",
8382 "kind" : "video",
8383 "linkId" : "F3271449-9FF2-4A35-8582-579E52CA2EFC",
8384 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
8385 "muted" : false,
8386 "newShot" : false,
8387 "speed" : 1,
8388 "srcIn" : 2526.504163727357,
8389 "start" : 723.7627118644068,
8390 "track" : "v3"
8391 },
8392 {
8393 "duration" : 14.474576271186379,
8394 "fadeIn" : 0,
8395 "fadeOut" : 0,
8396 "id" : "E2ECA174-0D6E-4E40-961D-0804BCB7E988",
8397 "kind" : "video",
8398 "linkId" : "F3271449-9FF2-4A35-8582-579E52CA2EFC",
8399 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
8400 "muted" : false,
8401 "newShot" : false,
8402 "speed" : 1,
8403 "srcIn" : 2526.5041591433583,
8404 "start" : 723.7627118644068,
8405 "track" : "v4"
8406 },
8407 {
8408 "duration" : 12.372881355932236,
8409 "fadeIn" : 0,
8410 "fadeOut" : 0,
8411 "id" : "793A00B4-D559-495E-BF6E-23E5FF440C18",
8412 "kind" : "video",
8413 "linkId" : "57734964-46B6-4D2E-8D3E-63DAA6A4B487",
8414 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
8415 "muted" : false,
8416 "newShot" : false,
8417 "speed" : 1,
8418 "srcIn" : 2542.203389830509,
8419 "start" : 738.2372881355932,
8420 "track" : "v0"
8421 },
8422 {
8423 "duration" : 12.372881355932236,
8424 "fadeIn" : 0,
8425 "fadeOut" : 0,
8426 "id" : "C4AC4AF2-ACA7-4B5B-B85A-13CC8E338B0C",
8427 "kind" : "audio",
8428 "linkId" : "57734964-46B6-4D2E-8D3E-63DAA6A4B487",
8429 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
8430 "muted" : true,
8431 "newShot" : false,
8432 "speed" : 1,
8433 "srcIn" : 2542.119518289168,
8434 "start" : 738.2372881355932,
8435 "track" : "v1"
8436 },
8437 {
8438 "duration" : 12.372881355932236,
8439 "fadeIn" : 0,
8440 "fadeOut" : 0,
8441 "id" : "0A904774-EBBE-42E8-BC32-42DDB4E21185",
8442 "kind" : "audio",
8443 "linkId" : "57734964-46B6-4D2E-8D3E-63DAA6A4B487",
8444 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
8445 "muted" : false,
8446 "newShot" : false,
8447 "speed" : 1,
8448 "srcIn" : 2542.166194592399,
8449 "start" : 738.2372881355932,
8450 "track" : "v2"
8451 },
8452 {
8453 "duration" : 12.372881355932236,
8454 "fadeIn" : 0,
8455 "fadeOut" : 0,
8456 "id" : "8CA5857B-2D5B-4E8A-90EB-7BE589C6785D",
8457 "kind" : "video",
8458 "linkId" : "57734964-46B6-4D2E-8D3E-63DAA6A4B487",
8459 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
8460 "muted" : false,
8461 "newShot" : false,
8462 "speed" : 1,
8463 "srcIn" : 2542.0295874561707,
8464 "start" : 738.2372881355932,
8465 "track" : "v3"
8466 },
8467 {
8468 "duration" : 12.372881355932236,
8469 "fadeIn" : 0,
8470 "fadeOut" : 0,
8471 "id" : "A1354766-A4E2-423C-A148-D457C645DEED",
8472 "kind" : "video",
8473 "linkId" : "57734964-46B6-4D2E-8D3E-63DAA6A4B487",
8474 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
8475 "muted" : false,
8476 "newShot" : false,
8477 "speed" : 1,
8478 "srcIn" : 2542.029582872172,
8479 "start" : 738.2372881355932,
8480 "track" : "v4"
8481 },
8482 {
8483 "duration" : 15112.237288,
8484 "fadeIn" : 0,
8485 "fadeOut" : 0,
8486 "id" : "1EDE863A-2897-4FFD-A49E-B943B438866F",
8487 "kind" : "video",
8488 "linkId" : "FD2B233D-ADAA-43D0-86FE-BD93A20C9129",
8489 "mediaId" : "CC300C55-7090-4A6E-81FA-55CB6644FAC9",
8490 "muted" : false,
8491 "newShot" : false,
8492 "speed" : 1,
8493 "srcIn" : 0,
8494 "start" : 4193.627118644067,
8495 "track" : "v0"
8496 },
8497 {
8498 "duration" : 15112.106667,
8499 "fadeIn" : 0,
8500 "fadeOut" : 0,
8501 "id" : "A3D2A572-1789-4532-9D15-1312C8312A41",
8502 "kind" : "audio",
8503 "linkId" : "FD2B233D-ADAA-43D0-86FE-BD93A20C9129",
8504 "mediaId" : "2613683E-12E6-402E-BF4D-6D9ACD7C1491",
8505 "muted" : true,
8506 "newShot" : false,
8507 "speed" : 1,
8508 "srcIn" : 0,
8509 "start" : 4193.710543352058,
8510 "track" : "v1"
8511 },
8512 {
8513 "duration" : 15112.213333,
8514 "fadeIn" : 0,
8515 "fadeOut" : 0,
8516 "id" : "387BD48F-5D42-4795-A2FE-DA7340E094B2",
8517 "kind" : "audio",
8518 "linkId" : "FD2B233D-ADAA-43D0-86FE-BD93A20C9129",
8519 "mediaId" : "7889E9EC-F67A-491B-81C0-0CF8338965A3",
8520 "muted" : false,
8521 "newShot" : false,
8522 "speed" : 1,
8523 "srcIn" : 0,
8524 "start" : 4193.660488258577,
8525 "track" : "v2"
8526 },
8527 {
8528 "duration" : 15112.066667,
8529 "fadeIn" : 0,
8530 "fadeOut" : 0,
8531 "id" : "15742468-AF06-408A-B0E3-DE8D543186D0",
8532 "kind" : "video",
8533 "linkId" : "FD2B233D-ADAA-43D0-86FE-BD93A20C9129",
8534 "mediaId" : "BC4E74C5-4C45-4633-B31A-3CAA58D4B2BF",
8535 "muted" : false,
8536 "newShot" : false,
8537 "speed" : 1,
8538 "srcIn" : 0,
8539 "start" : 4193.796015394061,
8540 "track" : "v3"
8541 },
8542 {
8543 "duration" : 15111.366667,
8544 "fadeIn" : 0,
8545 "fadeOut" : 0,
8546 "id" : "14D49BF0-68EB-4A4B-A627-9A55600AD1C2",
8547 "kind" : "video",
8548 "linkId" : "FD2B233D-ADAA-43D0-86FE-BD93A20C9129",
8549 "mediaId" : "98882264-A2ED-477B-A7B8-B63E54EDEACE",
8550 "muted" : false,
8551 "newShot" : false,
8552 "speed" : 1,
8553 "srcIn" : 0,
8554 "start" : 4193.796020644072,
8555 "track" : "v4"
8556 },
8557 {
8558 "duration" : 10045.254237,
8559 "fadeIn" : 0,
8560 "fadeOut" : 0,
8561 "id" : "82C83010-6BFE-4B26-81BC-CB291FAEB910",
8562 "kind" : "video",
8563 "linkId" : "F0D0664C-0131-446A-9354-CEB5E8E228DE",
8564 "mediaId" : "2D5EED57-87EB-44B4-9623-2CF48314C5F9",
8565 "muted" : false,
8566 "newShot" : false,
8567 "speed" : 1,
8568 "srcIn" : 0,
8569 "start" : 19305.864406779656,
8570 "track" : "v0"
8571 },
8572 {
8573 "duration" : 10045.162667,
8574 "fadeIn" : 0,
8575 "fadeOut" : 0,
8576 "id" : "83C1AFB5-E771-4D3A-8092-30A98A34B026",
8577 "kind" : "audio",
8578 "linkId" : "F0D0664C-0131-446A-9354-CEB5E8E228DE",
8579 "mediaId" : "0ECB1F4F-9065-432F-B0B1-7F47BD31FDCC",
8580 "muted" : false,
8581 "newShot" : false,
8582 "speed" : 1,
8583 "srcIn" : 0,
8584 "start" : 19305.9261039463,
8585 "track" : "v1"
8586 },
8587 {
8588 "duration" : 10045.248,
8589 "fadeIn" : 0,
8590 "fadeOut" : 0,
8591 "id" : "BD5B3686-815E-483F-8CDF-DC040625A81F",
8592 "kind" : "audio",
8593 "linkId" : "F0D0664C-0131-446A-9354-CEB5E8E228DE",
8594 "mediaId" : "02A5DFB3-9348-4102-BD13-F1856B431A70",
8595 "muted" : false,
8596 "newShot" : false,
8597 "speed" : 1,
8598 "srcIn" : 0,
8599 "start" : 19305.87683716514,
8600 "track" : "v2"
8601 },
8602 {
8603 "duration" : 10045.133333,
8604 "fadeIn" : 0,
8605 "fadeOut" : 0,
8606 "id" : "7C227E93-B4B2-44BB-81C4-0621CFA85F4E",
8607 "kind" : "video",
8608 "linkId" : "F0D0664C-0131-446A-9354-CEB5E8E228DE",
8609 "mediaId" : "9CA51332-5865-4C3D-9972-E781BDE75ECD",
8610 "muted" : false,
8611 "newShot" : false,
8612 "speed" : 1,
8613 "srcIn" : 0,
8614 "start" : 19306.0080157373,
8615 "track" : "v3"
8616 },
8617 {
8618 "duration" : 10045.166667,
8619 "fadeIn" : 0,
8620 "fadeOut" : 0,
8621 "id" : "472EB6D0-EF6A-469B-83F3-955C5C672361",
8622 "kind" : "video",
8623 "linkId" : "F0D0664C-0131-446A-9354-CEB5E8E228DE",
8624 "mediaId" : "0DEB0D7E-0C1E-49DA-9911-7773FB2158DE",
8625 "muted" : false,
8626 "newShot" : false,
8627 "speed" : 1,
8628 "srcIn" : 0,
8629 "start" : 19306.00801794631,
8630 "track" : "v4"
8631 },
8632 {
8633 "duration" : 11597.966102,
8634 "fadeIn" : 0,
8635 "fadeOut" : 0,
8636 "id" : "CE776B50-2A67-4285-9707-01DA14BBB04E",
8637 "kind" : "video",
8638 "linkId" : "F27C213D-566E-46A3-AF60-6D581EE67217",
8639 "mediaId" : "F06DF483-F96F-4DD1-8602-53609FA888F1",
8640 "muted" : false,
8641 "newShot" : false,
8642 "speed" : 1,
8643 "srcIn" : 0,
8644 "start" : 29351.18644067796,
8645 "track" : "v0"
8646 },
8647 {
8648 "duration" : 11598.101333,
8649 "fadeIn" : 0,
8650 "fadeOut" : 0,
8651 "id" : "7DB3CB28-8BB6-48ED-9B76-4FC6A5B2A106",
8652 "kind" : "audio",
8653 "linkId" : "F27C213D-566E-46A3-AF60-6D581EE67217",
8654 "mediaId" : "59213540-870A-4795-BD44-B3AFBC9F14E3",
8655 "muted" : false,
8656 "newShot" : false,
8657 "speed" : 1,
8658 "srcIn" : 0,
8659 "start" : 29351.035369510933,
8660 "track" : "v1"
8661 },
8662 {
8663 "duration" : 11598.464,
8664 "fadeIn" : 0,
8665 "fadeOut" : 0,
8666 "id" : "7941E3BC-CC88-41DB-A7BB-7C7519B8B9D6",
8667 "kind" : "audio",
8668 "linkId" : "F27C213D-566E-46A3-AF60-6D581EE67217",
8669 "mediaId" : "F4DB0AF3-2C74-4501-A58A-A519C046E3D6",
8670 "muted" : false,
8671 "newShot" : false,
8672 "speed" : 1,
8673 "srcIn" : 0,
8674 "start" : 29350.707797367297,
8675 "track" : "v2"
8676 },
8677 {
8678 "duration" : 11598.033333,
8679 "fadeIn" : 0,
8680 "fadeOut" : 0,
8681 "id" : "C6613E59-9E23-4934-B5DC-E224214F74D9",
8682 "kind" : "video",
8683 "linkId" : "F27C213D-566E-46A3-AF60-6D581EE67217",
8684 "mediaId" : "9F95D76B-EAB2-49AA-897A-C3C9A265E0CC",
8685 "muted" : false,
8686 "newShot" : false,
8687 "speed" : 1,
8688 "srcIn" : 0,
8689 "start" : 29351.122484093936,
8690 "track" : "v3"
8691 },
8692 {
8693 "duration" : 11598,
8694 "fadeIn" : 0,
8695 "fadeOut" : 0,
8696 "id" : "628E46F4-79BD-44D7-A964-903FBD21DD0F",
8697 "kind" : "video",
8698 "linkId" : "F27C213D-566E-46A3-AF60-6D581EE67217",
8699 "mediaId" : "CC497E03-E3E7-4283-A96C-F076D9F154A1",
8700 "muted" : false,
8701 "newShot" : false,
8702 "speed" : 1,
8703 "srcIn" : 0,
8704 "start" : 29351.12248226092,
8705 "track" : "v4"
8706 },
8707 {
8708 "duration" : 4.203389830508513,
8709 "fadeIn" : 0,
8710 "fadeOut" : 0,
8711 "id" : "5D2DD96A-1D28-4583-A85D-DDD1286F3EC1",
8712 "kind" : "video",
8713 "linkId" : "B7F43758-3B67-4E4D-8D71-152570BCA4AF",
8714 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
8715 "muted" : false,
8716 "newShot" : false,
8717 "speed" : 1,
8718 "srcIn" : 2491.0847457627124,
8719 "start" : 719.5593220338983,
8720 "track" : "v0"
8721 },
8722 {
8723 "duration" : 4.203389830508513,
8724 "fadeIn" : 0,
8725 "fadeOut" : 0,
8726 "id" : "B7181073-3D61-47D2-A83E-A7C19C246D68",
8727 "kind" : "audio",
8728 "linkId" : "B7F43758-3B67-4E4D-8D71-152570BCA4AF",
8729 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
8730 "muted" : true,
8731 "newShot" : false,
8732 "speed" : 1,
8733 "srcIn" : 2491.0008742213713,
8734 "start" : 719.5593220338983,
8735 "track" : "v1"
8736 },
8737 {
8738 "duration" : 4.203389830508513,
8739 "fadeIn" : 0,
8740 "fadeOut" : 0,
8741 "id" : "DF6474FD-D698-4232-9E94-EBE39BBB8545",
8742 "kind" : "audio",
8743 "linkId" : "B7F43758-3B67-4E4D-8D71-152570BCA4AF",
8744 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
8745 "muted" : false,
8746 "newShot" : false,
8747 "speed" : 1,
8748 "srcIn" : 2491.0475505246022,
8749 "start" : 719.5593220338983,
8750 "track" : "v2"
8751 },
8752 {
8753 "duration" : 4.203389830508513,
8754 "fadeIn" : 0,
8755 "fadeOut" : 0,
8756 "id" : "D06A7AA2-0BC5-4031-9EDA-7A81673249DF",
8757 "kind" : "video",
8758 "linkId" : "B7F43758-3B67-4E4D-8D71-152570BCA4AF",
8759 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
8760 "muted" : false,
8761 "newShot" : false,
8762 "speed" : 1,
8763 "srcIn" : 2490.910943388374,
8764 "start" : 719.5593220338983,
8765 "track" : "v3"
8766 },
8767 {
8768 "duration" : 4.203389830508513,
8769 "fadeIn" : 0,
8770 "fadeOut" : 0,
8771 "id" : "39DCC5F4-B663-454C-BD55-6E675B931DBA",
8772 "kind" : "video",
8773 "linkId" : "B7F43758-3B67-4E4D-8D71-152570BCA4AF",
8774 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
8775 "muted" : false,
8776 "newShot" : false,
8777 "speed" : 1,
8778 "srcIn" : 2490.9109388043753,
8779 "start" : 719.5593220338983,
8780 "track" : "v4"
8781 },
8782 {
8783 "duration" : 15.152542372881271,
8784 "fadeIn" : 0,
8785 "fadeOut" : 0,
8786 "id" : "F4373D19-EA06-4C7F-A620-FC8FD1BD6AE3",
8787 "kind" : "video",
8788 "linkId" : "DF0D7986-17C5-4CB7-B683-DD878723E8C7",
8789 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
8790 "muted" : false,
8791 "newShot" : false,
8792 "speed" : 1,
8793 "srcIn" : 2557.3898305084754,
8794 "start" : 750.6101694915254,
8795 "track" : "v0"
8796 },
8797 {
8798 "duration" : 15.152542372881271,
8799 "fadeIn" : 0,
8800 "fadeOut" : 0,
8801 "id" : "84DE87BB-60F8-41DF-933A-3B41704FE3D2",
8802 "kind" : "audio",
8803 "linkId" : "DF0D7986-17C5-4CB7-B683-DD878723E8C7",
8804 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
8805 "muted" : true,
8806 "newShot" : false,
8807 "speed" : 1,
8808 "srcIn" : 2557.3059589671343,
8809 "start" : 750.6101694915254,
8810 "track" : "v1"
8811 },
8812 {
8813 "duration" : 15.152542372881271,
8814 "fadeIn" : 0,
8815 "fadeOut" : 0,
8816 "id" : "F6A29700-D4EE-4106-AA51-21C13AD34CA4",
8817 "kind" : "audio",
8818 "linkId" : "DF0D7986-17C5-4CB7-B683-DD878723E8C7",
8819 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
8820 "muted" : false,
8821 "newShot" : false,
8822 "speed" : 1,
8823 "srcIn" : 2557.352635270365,
8824 "start" : 750.6101694915254,
8825 "track" : "v2"
8826 },
8827 {
8828 "duration" : 15.152542372881271,
8829 "fadeIn" : 0,
8830 "fadeOut" : 0,
8831 "id" : "353D8752-627E-40D9-8D76-BFB4084F392D",
8832 "kind" : "video",
8833 "linkId" : "DF0D7986-17C5-4CB7-B683-DD878723E8C7",
8834 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
8835 "muted" : false,
8836 "newShot" : false,
8837 "speed" : 1,
8838 "srcIn" : 2557.216028134137,
8839 "start" : 750.6101694915254,
8840 "track" : "v3"
8841 },
8842 {
8843 "duration" : 15.152542372881271,
8844 "fadeIn" : 0,
8845 "fadeOut" : 0,
8846 "id" : "901CEC11-981C-4F2C-8857-98E171F95538",
8847 "kind" : "video",
8848 "linkId" : "DF0D7986-17C5-4CB7-B683-DD878723E8C7",
8849 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
8850 "muted" : false,
8851 "newShot" : false,
8852 "speed" : 1,
8853 "srcIn" : 2557.2160235501383,
8854 "start" : 750.6101694915254,
8855 "track" : "v4"
8856 },
8857 {
8858 "duration" : 5.661016949152554,
8859 "fadeIn" : 0,
8860 "fadeOut" : 0,
8861 "id" : "E259582D-FC1C-4FA5-BE66-998C83FEC210",
8862 "kind" : "video",
8863 "linkId" : "9A0BE697-0A09-4342-804A-A10695A5765B",
8864 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
8865 "muted" : false,
8866 "newShot" : false,
8867 "speed" : 1,
8868 "srcIn" : 2575.42372881356,
8869 "start" : 765.7627118644068,
8870 "track" : "v0"
8871 },
8872 {
8873 "duration" : 5.661016949152554,
8874 "fadeIn" : 0,
8875 "fadeOut" : 0,
8876 "id" : "FB4E9075-64CB-4C65-A7BE-659E4788FCF8",
8877 "kind" : "audio",
8878 "linkId" : "9A0BE697-0A09-4342-804A-A10695A5765B",
8879 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
8880 "muted" : true,
8881 "newShot" : false,
8882 "speed" : 1,
8883 "srcIn" : 2575.339857272219,
8884 "start" : 765.7627118644068,
8885 "track" : "v1"
8886 },
8887 {
8888 "duration" : 5.661016949152554,
8889 "fadeIn" : 0,
8890 "fadeOut" : 0,
8891 "id" : "A6714A57-F046-4DE6-805C-A525B611FBD7",
8892 "kind" : "audio",
8893 "linkId" : "9A0BE697-0A09-4342-804A-A10695A5765B",
8894 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
8895 "muted" : false,
8896 "newShot" : false,
8897 "speed" : 1,
8898 "srcIn" : 2575.38653357545,
8899 "start" : 765.7627118644068,
8900 "track" : "v2"
8901 },
8902 {
8903 "duration" : 5.661016949152554,
8904 "fadeIn" : 0,
8905 "fadeOut" : 0,
8906 "id" : "DB4E1C97-4EDB-41E2-966E-057D14890226",
8907 "kind" : "video",
8908 "linkId" : "9A0BE697-0A09-4342-804A-A10695A5765B",
8909 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
8910 "muted" : false,
8911 "newShot" : false,
8912 "speed" : 1,
8913 "srcIn" : 2575.249926439222,
8914 "start" : 765.7627118644068,
8915 "track" : "v3"
8916 },
8917 {
8918 "duration" : 5.661016949152554,
8919 "fadeIn" : 0,
8920 "fadeOut" : 0,
8921 "id" : "E9D9C86D-9ED7-4309-8C45-79EE9034CFD1",
8922 "kind" : "video",
8923 "linkId" : "9A0BE697-0A09-4342-804A-A10695A5765B",
8924 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
8925 "muted" : false,
8926 "newShot" : false,
8927 "speed" : 1,
8928 "srcIn" : 2575.249921855223,
8929 "start" : 765.7627118644068,
8930 "track" : "v4"
8931 },
8932 {
8933 "duration" : 65.15254237288138,
8934 "fadeIn" : 0,
8935 "fadeOut" : 0,
8936 "id" : "864CC0F8-B363-4CE4-B255-61F2E32173FE",
8937 "kind" : "video",
8938 "linkId" : "FAD43732-5E8B-4F4A-AF1A-5F977F88D803",
8939 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
8940 "muted" : false,
8941 "newShot" : false,
8942 "speed" : 1,
8943 "srcIn" : 2581.0847457627124,
8944 "start" : 778.0677966101695,
8945 "track" : "v0"
8946 },
8947 {
8948 "duration" : 65.15254237288138,
8949 "fadeIn" : 0,
8950 "fadeOut" : 0,
8951 "id" : "F19656B8-AD96-45A9-AB45-41E84163B720",
8952 "kind" : "audio",
8953 "linkId" : "FAD43732-5E8B-4F4A-AF1A-5F977F88D803",
8954 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
8955 "muted" : true,
8956 "newShot" : false,
8957 "speed" : 1,
8958 "srcIn" : 2581.0008742213713,
8959 "start" : 778.0677966101695,
8960 "track" : "v1"
8961 },
8962 {
8963 "duration" : 65.15254237288138,
8964 "fadeIn" : 0,
8965 "fadeOut" : 0,
8966 "id" : "D40E7E5C-E58F-4D04-8A61-4C5A0AFE2C2C",
8967 "kind" : "audio",
8968 "linkId" : "FAD43732-5E8B-4F4A-AF1A-5F977F88D803",
8969 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
8970 "muted" : false,
8971 "newShot" : false,
8972 "speed" : 1,
8973 "srcIn" : 2581.0475505246022,
8974 "start" : 778.0677966101695,
8975 "track" : "v2"
8976 },
8977 {
8978 "duration" : 65.15254237288138,
8979 "fadeIn" : 0,
8980 "fadeOut" : 0,
8981 "id" : "0AEACF86-70AC-4999-8D26-F9BDE6003C4F",
8982 "kind" : "video",
8983 "linkId" : "FAD43732-5E8B-4F4A-AF1A-5F977F88D803",
8984 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
8985 "muted" : false,
8986 "newShot" : false,
8987 "speed" : 1,
8988 "srcIn" : 2580.910943388374,
8989 "start" : 778.0677966101695,
8990 "track" : "v3"
8991 },
8992 {
8993 "duration" : 65.15254237288138,
8994 "fadeIn" : 0,
8995 "fadeOut" : 0,
8996 "id" : "8566FA67-6A70-4CFF-9C03-5DED0AE5F620",
8997 "kind" : "video",
8998 "linkId" : "FAD43732-5E8B-4F4A-AF1A-5F977F88D803",
8999 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
9000 "muted" : false,
9001 "newShot" : false,
9002 "speed" : 1,
9003 "srcIn" : 2580.9109388043753,
9004 "start" : 778.0677966101695,
9005 "track" : "v4"
9006 },
9007 {
9008 "duration" : 2.135593220338933,
9009 "fadeIn" : 0,
9010 "fadeOut" : 0,
9011 "id" : "0FAA1CB7-A092-406F-B1FD-828EE4768B07",
9012 "kind" : "video",
9013 "linkId" : "FC57D67A-F7BF-448D-985C-05D71447DD0A",
9014 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
9015 "muted" : false,
9016 "newShot" : false,
9017 "speed" : 1,
9018 "srcIn" : 2646.2372881355936,
9019 "start" : 843.2203389830509,
9020 "track" : "v0"
9021 },
9022 {
9023 "duration" : 2.135593220338933,
9024 "fadeIn" : 0,
9025 "fadeOut" : 0,
9026 "id" : "AC5A9D19-E998-4CC0-B033-5C9AF84326C4",
9027 "kind" : "audio",
9028 "linkId" : "FC57D67A-F7BF-448D-985C-05D71447DD0A",
9029 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
9030 "muted" : true,
9031 "newShot" : false,
9032 "speed" : 1,
9033 "srcIn" : 2646.1534165942526,
9034 "start" : 843.2203389830509,
9035 "track" : "v1"
9036 },
9037 {
9038 "duration" : 2.135593220338933,
9039 "fadeIn" : 0,
9040 "fadeOut" : 0,
9041 "id" : "BF2AE534-1A06-4009-B825-8C1570D790C7",
9042 "kind" : "audio",
9043 "linkId" : "FC57D67A-F7BF-448D-985C-05D71447DD0A",
9044 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
9045 "muted" : false,
9046 "newShot" : false,
9047 "speed" : 1,
9048 "srcIn" : 2646.2000928974835,
9049 "start" : 843.2203389830509,
9050 "track" : "v2"
9051 },
9052 {
9053 "duration" : 2.135593220338933,
9054 "fadeIn" : 0,
9055 "fadeOut" : 0,
9056 "id" : "10E9AD27-80E1-4F3C-BD87-414A1B6617A8",
9057 "kind" : "video",
9058 "linkId" : "FC57D67A-F7BF-448D-985C-05D71447DD0A",
9059 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
9060 "muted" : false,
9061 "newShot" : false,
9062 "speed" : 1,
9063 "srcIn" : 2646.0634857612554,
9064 "start" : 843.2203389830509,
9065 "track" : "v3"
9066 },
9067 {
9068 "duration" : 2.135593220338933,
9069 "fadeIn" : 0,
9070 "fadeOut" : 0,
9071 "id" : "3EE49ACC-C52A-4ADF-86BE-884CE4259D4B",
9072 "kind" : "video",
9073 "linkId" : "FC57D67A-F7BF-448D-985C-05D71447DD0A",
9074 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
9075 "muted" : false,
9076 "newShot" : false,
9077 "speed" : 1,
9078 "srcIn" : 2646.0634811772566,
9079 "start" : 843.2203389830509,
9080 "track" : "v4"
9081 },
9082 {
9083 "duration" : 2.1355932203390466,
9084 "fadeIn" : 0,
9085 "fadeOut" : 0,
9086 "id" : "6F6DB3B3-42CE-4046-9988-087B5847CE7F",
9087 "kind" : "video",
9088 "linkId" : "28EA7312-79F0-4C9C-A37D-04E32716FA29",
9089 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
9090 "muted" : false,
9091 "newShot" : false,
9092 "speed" : 1,
9093 "srcIn" : 2650.203389830509,
9094 "start" : 845.3559322033898,
9095 "track" : "v0"
9096 },
9097 {
9098 "duration" : 2.1355932203390466,
9099 "fadeIn" : 0,
9100 "fadeOut" : 0,
9101 "id" : "96066F14-B10E-4826-890D-95FE249BB166",
9102 "kind" : "audio",
9103 "linkId" : "28EA7312-79F0-4C9C-A37D-04E32716FA29",
9104 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
9105 "muted" : true,
9106 "newShot" : false,
9107 "speed" : 1,
9108 "srcIn" : 2650.119518289168,
9109 "start" : 845.3559322033898,
9110 "track" : "v1"
9111 },
9112 {
9113 "duration" : 2.1355932203390466,
9114 "fadeIn" : 0,
9115 "fadeOut" : 0,
9116 "id" : "B07978FC-00CF-41C2-8657-665D128435D0",
9117 "kind" : "audio",
9118 "linkId" : "28EA7312-79F0-4C9C-A37D-04E32716FA29",
9119 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
9120 "muted" : false,
9121 "newShot" : false,
9122 "speed" : 1,
9123 "srcIn" : 2650.166194592399,
9124 "start" : 845.3559322033898,
9125 "track" : "v2"
9126 },
9127 {
9128 "duration" : 2.1355932203390466,
9129 "fadeIn" : 0,
9130 "fadeOut" : 0,
9131 "id" : "4FAD2B83-A8E8-4692-9360-C2F908C32F76",
9132 "kind" : "video",
9133 "linkId" : "28EA7312-79F0-4C9C-A37D-04E32716FA29",
9134 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
9135 "muted" : false,
9136 "newShot" : false,
9137 "speed" : 1,
9138 "srcIn" : 2650.0295874561707,
9139 "start" : 845.3559322033898,
9140 "track" : "v3"
9141 },
9142 {
9143 "duration" : 2.1355932203390466,
9144 "fadeIn" : 0,
9145 "fadeOut" : 0,
9146 "id" : "DF197166-8983-45B6-A285-54D45DECCFFB",
9147 "kind" : "video",
9148 "linkId" : "28EA7312-79F0-4C9C-A37D-04E32716FA29",
9149 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
9150 "muted" : false,
9151 "newShot" : false,
9152 "speed" : 1,
9153 "srcIn" : 2650.029582872172,
9154 "start" : 845.3559322033898,
9155 "track" : "v4"
9156 },
9157 {
9158 "duration" : 0.7118644067796822,
9159 "fadeIn" : 0,
9160 "fadeOut" : 0,
9161 "id" : "A6D0B507-6A36-4A23-B415-3888A8D66CEE",
9162 "kind" : "video",
9163 "linkId" : "E3D0D877-F5B9-4B99-B94B-44E5E47BEAE8",
9164 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
9165 "muted" : false,
9166 "newShot" : false,
9167 "speed" : 1,
9168 "srcIn" : 2653.118644067797,
9169 "start" : 847.4915254237288,
9170 "track" : "v0"
9171 },
9172 {
9173 "duration" : 0.7118644067796822,
9174 "fadeIn" : 0,
9175 "fadeOut" : 0,
9176 "id" : "AEBE3D73-9EE9-42F3-80FF-AF4E272C5BF5",
9177 "kind" : "audio",
9178 "linkId" : "E3D0D877-F5B9-4B99-B94B-44E5E47BEAE8",
9179 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
9180 "muted" : true,
9181 "newShot" : false,
9182 "speed" : 1,
9183 "srcIn" : 2653.034772526456,
9184 "start" : 847.4915254237288,
9185 "track" : "v1"
9186 },
9187 {
9188 "duration" : 0.7118644067796822,
9189 "fadeIn" : 0,
9190 "fadeOut" : 0,
9191 "id" : "A3B22B00-BA4B-4A40-91A3-F3E3C884C9BA",
9192 "kind" : "audio",
9193 "linkId" : "E3D0D877-F5B9-4B99-B94B-44E5E47BEAE8",
9194 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
9195 "muted" : false,
9196 "newShot" : false,
9197 "speed" : 1,
9198 "srcIn" : 2653.081448829687,
9199 "start" : 847.4915254237288,
9200 "track" : "v2"
9201 },
9202 {
9203 "duration" : 0.7118644067796822,
9204 "fadeIn" : 0,
9205 "fadeOut" : 0,
9206 "id" : "7B3ADFE8-7F47-4800-B8D6-77B5B07AFA99",
9207 "kind" : "video",
9208 "linkId" : "E3D0D877-F5B9-4B99-B94B-44E5E47BEAE8",
9209 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
9210 "muted" : false,
9211 "newShot" : false,
9212 "speed" : 1,
9213 "srcIn" : 2652.944841693459,
9214 "start" : 847.4915254237288,
9215 "track" : "v3"
9216 },
9217 {
9218 "duration" : 0.7118644067796822,
9219 "fadeIn" : 0,
9220 "fadeOut" : 0,
9221 "id" : "BC37902D-B6DE-4EE5-9996-467DEC167330",
9222 "kind" : "video",
9223 "linkId" : "E3D0D877-F5B9-4B99-B94B-44E5E47BEAE8",
9224 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
9225 "muted" : false,
9226 "newShot" : false,
9227 "speed" : 1,
9228 "srcIn" : 2652.94483710946,
9229 "start" : 847.4915254237288,
9230 "track" : "v4"
9231 },
9232 {
9233 "duration" : 0.7796610169491487,
9234 "fadeIn" : 0,
9235 "fadeOut" : 0,
9236 "id" : "8C61A3D8-55F3-4CCD-A02E-859B57EDA9CD",
9237 "kind" : "video",
9238 "linkId" : "86F71C9D-4CC8-49AE-85D6-08C4B076CE87",
9239 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
9240 "muted" : false,
9241 "newShot" : false,
9242 "speed" : 1,
9243 "srcIn" : 2658.71186440678,
9244 "start" : 848.2033898305085,
9245 "track" : "v0"
9246 },
9247 {
9248 "duration" : 0.7796610169491487,
9249 "fadeIn" : 0,
9250 "fadeOut" : 0,
9251 "id" : "25E99D83-FDE9-4E70-BBD3-A76D726F2D15",
9252 "kind" : "audio",
9253 "linkId" : "86F71C9D-4CC8-49AE-85D6-08C4B076CE87",
9254 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
9255 "muted" : true,
9256 "newShot" : false,
9257 "speed" : 1,
9258 "srcIn" : 2658.627992865439,
9259 "start" : 848.2033898305085,
9260 "track" : "v1"
9261 },
9262 {
9263 "duration" : 0.7796610169491487,
9264 "fadeIn" : 0,
9265 "fadeOut" : 0,
9266 "id" : "5FD3B500-E4B2-4AE8-B37D-DE251B465DBD",
9267 "kind" : "audio",
9268 "linkId" : "86F71C9D-4CC8-49AE-85D6-08C4B076CE87",
9269 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
9270 "muted" : false,
9271 "newShot" : false,
9272 "speed" : 1,
9273 "srcIn" : 2658.67466916867,
9274 "start" : 848.2033898305085,
9275 "track" : "v2"
9276 },
9277 {
9278 "duration" : 0.7796610169491487,
9279 "fadeIn" : 0,
9280 "fadeOut" : 0,
9281 "id" : "7BE9FE0B-5C6E-49DC-ACD3-613EE443EF5B",
9282 "kind" : "video",
9283 "linkId" : "86F71C9D-4CC8-49AE-85D6-08C4B076CE87",
9284 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
9285 "muted" : false,
9286 "newShot" : false,
9287 "speed" : 1,
9288 "srcIn" : 2658.5380620324418,
9289 "start" : 848.2033898305085,
9290 "track" : "v3"
9291 },
9292 {
9293 "duration" : 0.7796610169491487,
9294 "fadeIn" : 0,
9295 "fadeOut" : 0,
9296 "id" : "E51F2531-CEDB-4175-A047-516CD3142C93",
9297 "kind" : "video",
9298 "linkId" : "86F71C9D-4CC8-49AE-85D6-08C4B076CE87",
9299 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
9300 "muted" : false,
9301 "newShot" : false,
9302 "speed" : 1,
9303 "srcIn" : 2658.538057448443,
9304 "start" : 848.2033898305085,
9305 "track" : "v4"
9306 },
9307 {
9308 "duration" : 4.881355932203405,
9309 "fadeIn" : 0,
9310 "fadeOut" : 0,
9311 "id" : "352DB16B-39D1-475A-8C95-A0D712C61DA1",
9312 "kind" : "video",
9313 "linkId" : "3EABBA0E-4C24-4221-9E58-79D35BC9B08D",
9314 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
9315 "muted" : false,
9316 "newShot" : false,
9317 "speed" : 1,
9318 "srcIn" : 2672.77966101695,
9319 "start" : 848.9830508474577,
9320 "track" : "v0"
9321 },
9322 {
9323 "duration" : 4.881355932203405,
9324 "fadeIn" : 0,
9325 "fadeOut" : 0,
9326 "id" : "8904F908-4ED1-49D6-A064-B85CD9585F94",
9327 "kind" : "audio",
9328 "linkId" : "3EABBA0E-4C24-4221-9E58-79D35BC9B08D",
9329 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
9330 "muted" : true,
9331 "newShot" : false,
9332 "speed" : 1,
9333 "srcIn" : 2672.6957894756088,
9334 "start" : 848.9830508474577,
9335 "track" : "v1"
9336 },
9337 {
9338 "duration" : 4.881355932203405,
9339 "fadeIn" : 0,
9340 "fadeOut" : 0,
9341 "id" : "F88FC5BF-C07B-4659-96CD-F0C6423F09A3",
9342 "kind" : "audio",
9343 "linkId" : "3EABBA0E-4C24-4221-9E58-79D35BC9B08D",
9344 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
9345 "muted" : false,
9346 "newShot" : false,
9347 "speed" : 1,
9348 "srcIn" : 2672.7424657788397,
9349 "start" : 848.9830508474577,
9350 "track" : "v2"
9351 },
9352 {
9353 "duration" : 4.881355932203405,
9354 "fadeIn" : 0,
9355 "fadeOut" : 0,
9356 "id" : "319CEC14-6215-4445-8F0D-91C308A3420E",
9357 "kind" : "video",
9358 "linkId" : "3EABBA0E-4C24-4221-9E58-79D35BC9B08D",
9359 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
9360 "muted" : false,
9361 "newShot" : false,
9362 "speed" : 1,
9363 "srcIn" : 2672.6058586426116,
9364 "start" : 848.9830508474577,
9365 "track" : "v3"
9366 },
9367 {
9368 "duration" : 4.881355932203405,
9369 "fadeIn" : 0,
9370 "fadeOut" : 0,
9371 "id" : "EE17ADAE-984B-467F-8219-EC6C5F5D962B",
9372 "kind" : "video",
9373 "linkId" : "3EABBA0E-4C24-4221-9E58-79D35BC9B08D",
9374 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
9375 "muted" : false,
9376 "newShot" : false,
9377 "speed" : 1,
9378 "srcIn" : 2672.6058540586127,
9379 "start" : 848.9830508474577,
9380 "track" : "v4"
9381 },
9382 {
9383 "duration" : 4.101694915254257,
9384 "fadeIn" : 0,
9385 "fadeOut" : 0,
9386 "id" : "FCB1D834-DEAA-473C-A554-9F80416158C2",
9387 "kind" : "video",
9388 "linkId" : "3B4E4A04-CB5C-4738-B65E-F7076495C9D8",
9389 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
9390 "muted" : false,
9391 "newShot" : false,
9392 "speed" : 1,
9393 "srcIn" : 2710.7118644067805,
9394 "start" : 853.8644067796611,
9395 "track" : "v0"
9396 },
9397 {
9398 "duration" : 4.101694915254257,
9399 "fadeIn" : 0,
9400 "fadeOut" : 0,
9401 "id" : "881E7DB1-B037-40A9-9296-A99F279D7B54",
9402 "kind" : "audio",
9403 "linkId" : "3B4E4A04-CB5C-4738-B65E-F7076495C9D8",
9404 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
9405 "muted" : true,
9406 "newShot" : false,
9407 "speed" : 1,
9408 "srcIn" : 2710.6279928654394,
9409 "start" : 853.8644067796611,
9410 "track" : "v1"
9411 },
9412 {
9413 "duration" : 4.101694915254257,
9414 "fadeIn" : 0,
9415 "fadeOut" : 0,
9416 "id" : "5D38E6BE-385D-45A3-9AF3-30FF2BB3F39B",
9417 "kind" : "audio",
9418 "linkId" : "3B4E4A04-CB5C-4738-B65E-F7076495C9D8",
9419 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
9420 "muted" : false,
9421 "newShot" : false,
9422 "speed" : 1,
9423 "srcIn" : 2710.6746691686703,
9424 "start" : 853.8644067796611,
9425 "track" : "v2"
9426 },
9427 {
9428 "duration" : 4.101694915254257,
9429 "fadeIn" : 0,
9430 "fadeOut" : 0,
9431 "id" : "7B349858-7AE2-48A3-991E-DB25F74D2807",
9432 "kind" : "video",
9433 "linkId" : "3B4E4A04-CB5C-4738-B65E-F7076495C9D8",
9434 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
9435 "muted" : false,
9436 "newShot" : false,
9437 "speed" : 1,
9438 "srcIn" : 2710.538062032442,
9439 "start" : 853.8644067796611,
9440 "track" : "v3"
9441 },
9442 {
9443 "duration" : 4.101694915254257,
9444 "fadeIn" : 0,
9445 "fadeOut" : 0,
9446 "id" : "58E4F08F-1403-43B7-B93B-C6BFA5127F8D",
9447 "kind" : "video",
9448 "linkId" : "3B4E4A04-CB5C-4738-B65E-F7076495C9D8",
9449 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
9450 "muted" : false,
9451 "newShot" : false,
9452 "speed" : 1,
9453 "srcIn" : 2710.5380574484434,
9454 "start" : 853.8644067796611,
9455 "track" : "v4"
9456 },
9457 {
9458 "duration" : 30.27118644067798,
9459 "fadeIn" : 0,
9460 "fadeOut" : 0,
9461 "id" : "52F2190F-1C89-44EA-94D3-8A7085B8172A",
9462 "kind" : "video",
9463 "linkId" : "AE414DA9-138E-4B31-BCEA-EBD522E81175",
9464 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
9465 "muted" : false,
9466 "newShot" : false,
9467 "speed" : 1,
9468 "srcIn" : 2721.322033898306,
9469 "start" : 857.9661016949152,
9470 "track" : "v0"
9471 },
9472 {
9473 "duration" : 30.27118644067798,
9474 "fadeIn" : 0,
9475 "fadeOut" : 0,
9476 "id" : "369AF7CA-F05E-4BAF-B4DA-92398E680470",
9477 "kind" : "audio",
9478 "linkId" : "AE414DA9-138E-4B31-BCEA-EBD522E81175",
9479 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
9480 "muted" : true,
9481 "newShot" : false,
9482 "speed" : 1,
9483 "srcIn" : 2721.238162356965,
9484 "start" : 857.9661016949152,
9485 "track" : "v1"
9486 },
9487 {
9488 "duration" : 30.27118644067798,
9489 "fadeIn" : 0,
9490 "fadeOut" : 0,
9491 "id" : "E0C2832B-D290-4F69-B311-6CCB13D81611",
9492 "kind" : "audio",
9493 "linkId" : "AE414DA9-138E-4B31-BCEA-EBD522E81175",
9494 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
9495 "muted" : false,
9496 "newShot" : false,
9497 "speed" : 1,
9498 "srcIn" : 2721.284838660196,
9499 "start" : 857.9661016949152,
9500 "track" : "v2"
9501 },
9502 {
9503 "duration" : 30.27118644067798,
9504 "fadeIn" : 0,
9505 "fadeOut" : 0,
9506 "id" : "435A5598-D5FB-4880-AF72-ACF1883BA634",
9507 "kind" : "video",
9508 "linkId" : "AE414DA9-138E-4B31-BCEA-EBD522E81175",
9509 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
9510 "muted" : false,
9511 "newShot" : false,
9512 "speed" : 1,
9513 "srcIn" : 2721.1482315239678,
9514 "start" : 857.9661016949152,
9515 "track" : "v3"
9516 },
9517 {
9518 "duration" : 30.27118644067798,
9519 "fadeIn" : 0,
9520 "fadeOut" : 0,
9521 "id" : "E1A300BD-01C6-44F8-A372-7EDEAAA3407A",
9522 "kind" : "video",
9523 "linkId" : "AE414DA9-138E-4B31-BCEA-EBD522E81175",
9524 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
9525 "muted" : false,
9526 "newShot" : false,
9527 "speed" : 1,
9528 "srcIn" : 2721.148226939969,
9529 "start" : 857.9661016949152,
9530 "track" : "v4"
9531 },
9532 {
9533 "duration" : 2.8135593220339388,
9534 "fadeIn" : 0,
9535 "fadeOut" : 0,
9536 "id" : "F79BA8C9-1EEA-4EAD-9408-BEB36A06F6EC",
9537 "kind" : "video",
9538 "linkId" : "4045FA44-3B4D-4BFC-A67F-515E3C553BA0",
9539 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
9540 "muted" : false,
9541 "newShot" : false,
9542 "speed" : 1,
9543 "srcIn" : 2760.915254237289,
9544 "start" : 888.2372881355932,
9545 "track" : "v0"
9546 },
9547 {
9548 "duration" : 2.8135593220339388,
9549 "fadeIn" : 0,
9550 "fadeOut" : 0,
9551 "id" : "E4CC2092-0AFA-454F-A056-DC1D4B96EAE3",
9552 "kind" : "audio",
9553 "linkId" : "4045FA44-3B4D-4BFC-A67F-515E3C553BA0",
9554 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
9555 "muted" : true,
9556 "newShot" : false,
9557 "speed" : 1,
9558 "srcIn" : 2760.831382695948,
9559 "start" : 888.2372881355932,
9560 "track" : "v1"
9561 },
9562 {
9563 "duration" : 2.8135593220339388,
9564 "fadeIn" : 0,
9565 "fadeOut" : 0,
9566 "id" : "C6569D08-E232-40FD-AD5E-38A850503FDF",
9567 "kind" : "audio",
9568 "linkId" : "4045FA44-3B4D-4BFC-A67F-515E3C553BA0",
9569 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
9570 "muted" : false,
9571 "newShot" : false,
9572 "speed" : 1,
9573 "srcIn" : 2760.878058999179,
9574 "start" : 888.2372881355932,
9575 "track" : "v2"
9576 },
9577 {
9578 "duration" : 2.8135593220339388,
9579 "fadeIn" : 0,
9580 "fadeOut" : 0,
9581 "id" : "5D424C43-D60A-4A52-A33B-BBEB380053F2",
9582 "kind" : "video",
9583 "linkId" : "4045FA44-3B4D-4BFC-A67F-515E3C553BA0",
9584 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
9585 "muted" : false,
9586 "newShot" : false,
9587 "speed" : 1,
9588 "srcIn" : 2760.7414518629507,
9589 "start" : 888.2372881355932,
9590 "track" : "v3"
9591 },
9592 {
9593 "duration" : 2.8135593220339388,
9594 "fadeIn" : 0,
9595 "fadeOut" : 0,
9596 "id" : "C9ECC1B1-1D5A-4D84-9012-9864635A322A",
9597 "kind" : "video",
9598 "linkId" : "4045FA44-3B4D-4BFC-A67F-515E3C553BA0",
9599 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
9600 "muted" : false,
9601 "newShot" : false,
9602 "speed" : 1,
9603 "srcIn" : 2760.741447278952,
9604 "start" : 888.2372881355932,
9605 "track" : "v4"
9606 },
9607 {
9608 "duration" : 5.288135593220318,
9609 "fadeIn" : 0,
9610 "fadeOut" : 0,
9611 "id" : "1B56AEBA-0FE3-40D6-A485-407844047F55",
9612 "kind" : "video",
9613 "linkId" : "F572F9CE-FD52-495A-A477-9D6D62AC1086",
9614 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
9615 "muted" : false,
9616 "newShot" : false,
9617 "speed" : 1,
9618 "srcIn" : 2783.186440677967,
9619 "start" : 891.0508474576271,
9620 "track" : "v0"
9621 },
9622 {
9623 "duration" : 5.288135593220318,
9624 "fadeIn" : 0,
9625 "fadeOut" : 0,
9626 "id" : "47F3B364-BE6C-473E-85C4-AD4A9DADF041",
9627 "kind" : "audio",
9628 "linkId" : "F572F9CE-FD52-495A-A477-9D6D62AC1086",
9629 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
9630 "muted" : true,
9631 "newShot" : false,
9632 "speed" : 1,
9633 "srcIn" : 2783.102569136626,
9634 "start" : 891.0508474576271,
9635 "track" : "v1"
9636 },
9637 {
9638 "duration" : 5.288135593220318,
9639 "fadeIn" : 0,
9640 "fadeOut" : 0,
9641 "id" : "E99A647D-A1D7-4937-883D-517D2AB47C45",
9642 "kind" : "audio",
9643 "linkId" : "F572F9CE-FD52-495A-A477-9D6D62AC1086",
9644 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
9645 "muted" : false,
9646 "newShot" : false,
9647 "speed" : 1,
9648 "srcIn" : 2783.1492454398567,
9649 "start" : 891.0508474576271,
9650 "track" : "v2"
9651 },
9652 {
9653 "duration" : 5.288135593220318,
9654 "fadeIn" : 0,
9655 "fadeOut" : 0,
9656 "id" : "68CBC79C-1130-4411-9396-AF8D0501E229",
9657 "kind" : "video",
9658 "linkId" : "F572F9CE-FD52-495A-A477-9D6D62AC1086",
9659 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
9660 "muted" : false,
9661 "newShot" : false,
9662 "speed" : 1,
9663 "srcIn" : 2783.0126383036286,
9664 "start" : 891.0508474576271,
9665 "track" : "v3"
9666 },
9667 {
9668 "duration" : 5.288135593220318,
9669 "fadeIn" : 0,
9670 "fadeOut" : 0,
9671 "id" : "B4AEE7C6-2A5A-4B03-B760-5FB897266EB9",
9672 "kind" : "video",
9673 "linkId" : "F572F9CE-FD52-495A-A477-9D6D62AC1086",
9674 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
9675 "muted" : false,
9676 "newShot" : false,
9677 "speed" : 1,
9678 "srcIn" : 2783.01263371963,
9679 "start" : 891.0508474576271,
9680 "track" : "v4"
9681 },
9682 {
9683 "duration" : 25.96610169491521,
9684 "fadeIn" : 0,
9685 "fadeOut" : 0,
9686 "id" : "3A767D55-B04B-4029-9717-1C032BD7E834",
9687 "kind" : "video",
9688 "linkId" : "B52B0E53-2FA8-47CD-B126-B2D828808856",
9689 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
9690 "muted" : false,
9691 "newShot" : false,
9692 "speed" : 1,
9693 "srcIn" : 2807.152542372882,
9694 "start" : 896.3389830508474,
9695 "track" : "v0"
9696 },
9697 {
9698 "duration" : 25.96610169491521,
9699 "fadeIn" : 0,
9700 "fadeOut" : 0,
9701 "id" : "CF8D639C-6FA9-4562-BF86-A28192C1D6C3",
9702 "kind" : "audio",
9703 "linkId" : "B52B0E53-2FA8-47CD-B126-B2D828808856",
9704 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
9705 "muted" : true,
9706 "newShot" : false,
9707 "speed" : 1,
9708 "srcIn" : 2807.068670831541,
9709 "start" : 896.3389830508474,
9710 "track" : "v1"
9711 },
9712 {
9713 "duration" : 25.96610169491521,
9714 "fadeIn" : 0,
9715 "fadeOut" : 0,
9716 "id" : "304BEE54-6B93-44B2-BF5C-F5E0F8864DBB",
9717 "kind" : "audio",
9718 "linkId" : "B52B0E53-2FA8-47CD-B126-B2D828808856",
9719 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
9720 "muted" : false,
9721 "newShot" : false,
9722 "speed" : 1,
9723 "srcIn" : 2807.115347134772,
9724 "start" : 896.3389830508474,
9725 "track" : "v2"
9726 },
9727 {
9728 "duration" : 25.96610169491521,
9729 "fadeIn" : 0,
9730 "fadeOut" : 0,
9731 "id" : "7DAAE3B6-0D4A-4029-9891-8A2D9DD41EB6",
9732 "kind" : "video",
9733 "linkId" : "B52B0E53-2FA8-47CD-B126-B2D828808856",
9734 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
9735 "muted" : false,
9736 "newShot" : false,
9737 "speed" : 1,
9738 "srcIn" : 2806.978739998544,
9739 "start" : 896.3389830508474,
9740 "track" : "v3"
9741 },
9742 {
9743 "duration" : 25.96610169491521,
9744 "fadeIn" : 0,
9745 "fadeOut" : 0,
9746 "id" : "A7EC127B-861B-436B-973A-566A7282AB56",
9747 "kind" : "video",
9748 "linkId" : "B52B0E53-2FA8-47CD-B126-B2D828808856",
9749 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
9750 "muted" : false,
9751 "newShot" : false,
9752 "speed" : 1,
9753 "srcIn" : 2806.978735414545,
9754 "start" : 896.3389830508474,
9755 "track" : "v4"
9756 },
9757 {
9758 "duration" : 1.9322033898305335,
9759 "fadeIn" : 0,
9760 "fadeOut" : 0,
9761 "id" : "B9DADEF6-8878-42FB-BD66-05228CB67D3F",
9762 "kind" : "video",
9763 "linkId" : "6C868D02-D8F5-470C-891B-B0D8A8F52B42",
9764 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
9765 "muted" : false,
9766 "newShot" : false,
9767 "speed" : 1,
9768 "srcIn" : 2833.1186440677975,
9769 "start" : 922.3050847457627,
9770 "track" : "v0"
9771 },
9772 {
9773 "duration" : 1.9322033898305335,
9774 "fadeIn" : 0,
9775 "fadeOut" : 0,
9776 "id" : "43CC1239-2AD4-4790-8627-80441483FA43",
9777 "kind" : "audio",
9778 "linkId" : "6C868D02-D8F5-470C-891B-B0D8A8F52B42",
9779 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
9780 "muted" : true,
9781 "newShot" : false,
9782 "speed" : 1,
9783 "srcIn" : 2833.0347725264564,
9784 "start" : 922.3050847457627,
9785 "track" : "v1"
9786 },
9787 {
9788 "duration" : 1.9322033898305335,
9789 "fadeIn" : 0,
9790 "fadeOut" : 0,
9791 "id" : "3E10D3AF-E56C-4B77-B15A-7A6A433E78B5",
9792 "kind" : "audio",
9793 "linkId" : "6C868D02-D8F5-470C-891B-B0D8A8F52B42",
9794 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
9795 "muted" : false,
9796 "newShot" : false,
9797 "speed" : 1,
9798 "srcIn" : 2833.0814488296874,
9799 "start" : 922.3050847457627,
9800 "track" : "v2"
9801 },
9802 {
9803 "duration" : 1.9322033898305335,
9804 "fadeIn" : 0,
9805 "fadeOut" : 0,
9806 "id" : "0AE9423C-B5CA-40D7-A25F-1D5950077D5C",
9807 "kind" : "video",
9808 "linkId" : "6C868D02-D8F5-470C-891B-B0D8A8F52B42",
9809 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
9810 "muted" : false,
9811 "newShot" : false,
9812 "speed" : 1,
9813 "srcIn" : 2832.9448416934592,
9814 "start" : 922.3050847457627,
9815 "track" : "v3"
9816 },
9817 {
9818 "duration" : 1.9322033898305335,
9819 "fadeIn" : 0,
9820 "fadeOut" : 0,
9821 "id" : "FA6C223A-7F5A-4150-A5F4-D2B4B445D9C0",
9822 "kind" : "video",
9823 "linkId" : "6C868D02-D8F5-470C-891B-B0D8A8F52B42",
9824 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
9825 "muted" : false,
9826 "newShot" : false,
9827 "speed" : 1,
9828 "srcIn" : 2832.9448371094604,
9829 "start" : 922.3050847457627,
9830 "track" : "v4"
9831 },
9832 {
9833 "duration" : 7.186440677966175,
9834 "fadeIn" : 0,
9835 "fadeOut" : 0,
9836 "id" : "934F79A8-575E-43F0-AD6C-F24F5199B7CB",
9837 "kind" : "video",
9838 "linkId" : "851CBD5D-70B1-429E-9236-7ECFEDAAA252",
9839 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
9840 "muted" : false,
9841 "newShot" : false,
9842 "speed" : 1,
9843 "srcIn" : 2838.7118644067805,
9844 "start" : 924.2372881355932,
9845 "track" : "v0"
9846 },
9847 {
9848 "duration" : 7.186440677966175,
9849 "fadeIn" : 0,
9850 "fadeOut" : 0,
9851 "id" : "A6FAE2FB-6564-42B5-ACC5-56029B3331BE",
9852 "kind" : "audio",
9853 "linkId" : "851CBD5D-70B1-429E-9236-7ECFEDAAA252",
9854 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
9855 "muted" : true,
9856 "newShot" : false,
9857 "speed" : 1,
9858 "srcIn" : 2838.6279928654394,
9859 "start" : 924.2372881355932,
9860 "track" : "v1"
9861 },
9862 {
9863 "duration" : 7.186440677966175,
9864 "fadeIn" : 0,
9865 "fadeOut" : 0,
9866 "id" : "EE147F25-D8F4-43EB-B6A4-DE05CB21BB12",
9867 "kind" : "audio",
9868 "linkId" : "851CBD5D-70B1-429E-9236-7ECFEDAAA252",
9869 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
9870 "muted" : false,
9871 "newShot" : false,
9872 "speed" : 1,
9873 "srcIn" : 2838.6746691686703,
9874 "start" : 924.2372881355932,
9875 "track" : "v2"
9876 },
9877 {
9878 "duration" : 7.186440677966175,
9879 "fadeIn" : 0,
9880 "fadeOut" : 0,
9881 "id" : "4890D002-161D-4D3E-A662-D441F2232FFF",
9882 "kind" : "video",
9883 "linkId" : "851CBD5D-70B1-429E-9236-7ECFEDAAA252",
9884 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
9885 "muted" : false,
9886 "newShot" : false,
9887 "speed" : 1,
9888 "srcIn" : 2838.538062032442,
9889 "start" : 924.2372881355932,
9890 "track" : "v3"
9891 },
9892 {
9893 "duration" : 7.186440677966175,
9894 "fadeIn" : 0,
9895 "fadeOut" : 0,
9896 "id" : "9BAE233F-FFA4-4044-A029-3B4EC16018C3",
9897 "kind" : "video",
9898 "linkId" : "851CBD5D-70B1-429E-9236-7ECFEDAAA252",
9899 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
9900 "muted" : false,
9901 "newShot" : false,
9902 "speed" : 1,
9903 "srcIn" : 2838.5380574484434,
9904 "start" : 924.2372881355932,
9905 "track" : "v4"
9906 },
9907 {
9908 "duration" : 3.830508474576277,
9909 "fadeIn" : 0,
9910 "fadeOut" : 0,
9911 "id" : "FB84EAA1-5B89-4489-89B4-D080098A1CD8",
9912 "kind" : "video",
9913 "linkId" : "6206B630-2FC1-4084-9937-5E08F99F118D",
9914 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
9915 "muted" : false,
9916 "newShot" : false,
9917 "speed" : 1,
9918 "srcIn" : 2890.5084745762715,
9919 "start" : 931.4237288135594,
9920 "track" : "v0"
9921 },
9922 {
9923 "duration" : 3.830508474576277,
9924 "fadeIn" : 0,
9925 "fadeOut" : 0,
9926 "id" : "EE6692DD-2585-4607-A46A-C6A25FB86B72",
9927 "kind" : "audio",
9928 "linkId" : "6206B630-2FC1-4084-9937-5E08F99F118D",
9929 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
9930 "muted" : true,
9931 "newShot" : false,
9932 "speed" : 1,
9933 "srcIn" : 2890.4246030349304,
9934 "start" : 931.4237288135594,
9935 "track" : "v1"
9936 },
9937 {
9938 "duration" : 3.830508474576277,
9939 "fadeIn" : 0,
9940 "fadeOut" : 0,
9941 "id" : "78A13F6F-DCA5-491A-8B52-0FC2409760AB",
9942 "kind" : "audio",
9943 "linkId" : "6206B630-2FC1-4084-9937-5E08F99F118D",
9944 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
9945 "muted" : false,
9946 "newShot" : false,
9947 "speed" : 1,
9948 "srcIn" : 2890.4712793381614,
9949 "start" : 931.4237288135594,
9950 "track" : "v2"
9951 },
9952 {
9953 "duration" : 3.830508474576277,
9954 "fadeIn" : 0,
9955 "fadeOut" : 0,
9956 "id" : "629569A0-6C4A-466A-AFA1-918C22E2E018",
9957 "kind" : "video",
9958 "linkId" : "6206B630-2FC1-4084-9937-5E08F99F118D",
9959 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
9960 "muted" : false,
9961 "newShot" : false,
9962 "speed" : 1,
9963 "srcIn" : 2890.3346722019332,
9964 "start" : 931.4237288135594,
9965 "track" : "v3"
9966 },
9967 {
9968 "duration" : 3.830508474576277,
9969 "fadeIn" : 0,
9970 "fadeOut" : 0,
9971 "id" : "55E9F328-E350-4B59-ACEA-6C4EF5E5A345",
9972 "kind" : "video",
9973 "linkId" : "6206B630-2FC1-4084-9937-5E08F99F118D",
9974 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
9975 "muted" : false,
9976 "newShot" : false,
9977 "speed" : 1,
9978 "srcIn" : 2890.3346676179344,
9979 "start" : 931.4237288135594,
9980 "track" : "v4"
9981 },
9982 {
9983 "duration" : 3.016949152542338,
9984 "fadeIn" : 0,
9985 "fadeOut" : 0,
9986 "id" : "61D97AD3-FA77-4D5E-85F2-0D8E088424D8",
9987 "kind" : "video",
9988 "linkId" : "03409808-1CF1-4E22-8CFC-D7F337CA20C0",
9989 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
9990 "muted" : false,
9991 "newShot" : false,
9992 "speed" : 1,
9993 "srcIn" : 2897.593220338983,
9994 "start" : 935.2542372881356,
9995 "track" : "v0"
9996 },
9997 {
9998 "duration" : 3.016949152542338,
9999 "fadeIn" : 0,
10000 "fadeOut" : 0,
10001 "id" : "A4B63EE7-142A-4568-B3A3-9BD1DFE06CE5",
10002 "kind" : "audio",
10003 "linkId" : "03409808-1CF1-4E22-8CFC-D7F337CA20C0",
10004 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
10005 "muted" : true,
10006 "newShot" : false,
10007 "speed" : 1,
10008 "srcIn" : 2897.509348797642,
10009 "start" : 935.2542372881356,
10010 "track" : "v1"
10011 },
10012 {
10013 "duration" : 3.016949152542338,
10014 "fadeIn" : 0,
10015 "fadeOut" : 0,
10016 "id" : "2195C3FF-C478-48A3-B480-A8D661ECF7EF",
10017 "kind" : "audio",
10018 "linkId" : "03409808-1CF1-4E22-8CFC-D7F337CA20C0",
10019 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
10020 "muted" : false,
10021 "newShot" : false,
10022 "speed" : 1,
10023 "srcIn" : 2897.556025100873,
10024 "start" : 935.2542372881356,
10025 "track" : "v2"
10026 },
10027 {
10028 "duration" : 3.016949152542338,
10029 "fadeIn" : 0,
10030 "fadeOut" : 0,
10031 "id" : "12215B20-BCE9-4ED2-BEC6-1EF6791F42B1",
10032 "kind" : "video",
10033 "linkId" : "03409808-1CF1-4E22-8CFC-D7F337CA20C0",
10034 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
10035 "muted" : false,
10036 "newShot" : false,
10037 "speed" : 1,
10038 "srcIn" : 2897.4194179646447,
10039 "start" : 935.2542372881356,
10040 "track" : "v3"
10041 },
10042 {
10043 "duration" : 3.016949152542338,
10044 "fadeIn" : 0,
10045 "fadeOut" : 0,
10046 "id" : "416FCED0-3BEE-4052-95C4-0D4395DF944A",
10047 "kind" : "video",
10048 "linkId" : "03409808-1CF1-4E22-8CFC-D7F337CA20C0",
10049 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
10050 "muted" : false,
10051 "newShot" : false,
10052 "speed" : 1,
10053 "srcIn" : 2897.419413380646,
10054 "start" : 935.2542372881356,
10055 "track" : "v4"
10056 },
10057 {
10058 "duration" : 0.8135593220338251,
10059 "fadeIn" : 0,
10060 "fadeOut" : 0,
10061 "id" : "3D695665-26AC-4ECE-B055-5ED89B2AEF2E",
10062 "kind" : "video",
10063 "linkId" : "D7000904-0663-4407-824B-32447EFF33F6",
10064 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
10065 "muted" : false,
10066 "newShot" : false,
10067 "speed" : 1,
10068 "srcIn" : 2904.5423728813553,
10069 "start" : 938.271186440678,
10070 "track" : "v0"
10071 },
10072 {
10073 "duration" : 0.8135593220338251,
10074 "fadeIn" : 0,
10075 "fadeOut" : 0,
10076 "id" : "1895810B-430E-4DE6-8E5B-653139F5FBEE",
10077 "kind" : "audio",
10078 "linkId" : "D7000904-0663-4407-824B-32447EFF33F6",
10079 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
10080 "muted" : true,
10081 "newShot" : false,
10082 "speed" : 1,
10083 "srcIn" : 2904.458501340014,
10084 "start" : 938.271186440678,
10085 "track" : "v1"
10086 },
10087 {
10088 "duration" : 0.8135593220338251,
10089 "fadeIn" : 0,
10090 "fadeOut" : 0,
10091 "id" : "0E102FE5-7926-4A40-9B94-CF405C37EB75",
10092 "kind" : "audio",
10093 "linkId" : "D7000904-0663-4407-824B-32447EFF33F6",
10094 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
10095 "muted" : false,
10096 "newShot" : false,
10097 "speed" : 1,
10098 "srcIn" : 2904.505177643245,
10099 "start" : 938.271186440678,
10100 "track" : "v2"
10101 },
10102 {
10103 "duration" : 0.8135593220338251,
10104 "fadeIn" : 0,
10105 "fadeOut" : 0,
10106 "id" : "222B3055-014A-4379-98CD-69EC43A35470",
10107 "kind" : "video",
10108 "linkId" : "D7000904-0663-4407-824B-32447EFF33F6",
10109 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
10110 "muted" : false,
10111 "newShot" : false,
10112 "speed" : 1,
10113 "srcIn" : 2904.368570507017,
10114 "start" : 938.271186440678,
10115 "track" : "v3"
10116 },
10117 {
10118 "duration" : 0.8135593220338251,
10119 "fadeIn" : 0,
10120 "fadeOut" : 0,
10121 "id" : "28CA102D-CDC8-46C5-988E-86F0EEF5EEDC",
10122 "kind" : "video",
10123 "linkId" : "D7000904-0663-4407-824B-32447EFF33F6",
10124 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
10125 "muted" : false,
10126 "newShot" : false,
10127 "speed" : 1,
10128 "srcIn" : 2904.368565923018,
10129 "start" : 938.271186440678,
10130 "track" : "v4"
10131 },
10132 {
10133 "duration" : 16.169491525423723,
10134 "fadeIn" : 0,
10135 "fadeOut" : 0,
10136 "id" : "1BBD2846-8898-43ED-B9EF-51ABB977D172",
10137 "kind" : "video",
10138 "linkId" : "441E27DB-C392-40BA-85B8-B3F421EF14E8",
10139 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
10140 "muted" : false,
10141 "newShot" : false,
10142 "speed" : 1,
10143 "srcIn" : 2905.3559322033893,
10144 "start" : 939.0847457627119,
10145 "track" : "v0"
10146 },
10147 {
10148 "duration" : 16.169491525423723,
10149 "fadeIn" : 0,
10150 "fadeOut" : 0,
10151 "id" : "0238A063-2A08-4FDB-920C-7372B455BCB8",
10152 "kind" : "audio",
10153 "linkId" : "441E27DB-C392-40BA-85B8-B3F421EF14E8",
10154 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
10155 "muted" : true,
10156 "newShot" : false,
10157 "speed" : 1,
10158 "srcIn" : 2905.2720606620483,
10159 "start" : 939.0847457627119,
10160 "track" : "v1"
10161 },
10162 {
10163 "duration" : 16.169491525423723,
10164 "fadeIn" : 0,
10165 "fadeOut" : 0,
10166 "id" : "CAF3113A-611A-4A6B-9A03-3539D6A0E4B1",
10167 "kind" : "audio",
10168 "linkId" : "441E27DB-C392-40BA-85B8-B3F421EF14E8",
10169 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
10170 "muted" : false,
10171 "newShot" : false,
10172 "speed" : 1,
10173 "srcIn" : 2905.318736965279,
10174 "start" : 939.0847457627119,
10175 "track" : "v2"
10176 },
10177 {
10178 "duration" : 16.169491525423723,
10179 "fadeIn" : 0,
10180 "fadeOut" : 0,
10181 "id" : "19424805-E973-46BC-BD0D-3547754230B4",
10182 "kind" : "video",
10183 "linkId" : "441E27DB-C392-40BA-85B8-B3F421EF14E8",
10184 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
10185 "muted" : false,
10186 "newShot" : false,
10187 "speed" : 1,
10188 "srcIn" : 2905.182129829051,
10189 "start" : 939.0847457627119,
10190 "track" : "v3"
10191 },
10192 {
10193 "duration" : 16.169491525423723,
10194 "fadeIn" : 0,
10195 "fadeOut" : 0,
10196 "id" : "D38B18BD-6C89-4507-A1EF-9B817B8E10CA",
10197 "kind" : "video",
10198 "linkId" : "441E27DB-C392-40BA-85B8-B3F421EF14E8",
10199 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
10200 "muted" : false,
10201 "newShot" : false,
10202 "speed" : 1,
10203 "srcIn" : 2905.1821252450522,
10204 "start" : 939.0847457627119,
10205 "track" : "v4"
10206 },
10207 {
10208 "duration" : 32.4406779661017,
10209 "fadeIn" : 0,
10210 "fadeOut" : 0,
10211 "id" : "92828BED-54D1-43DA-ADCC-A2784A7E60A8",
10212 "kind" : "video",
10213 "linkId" : "C90940D8-882F-4CBD-839A-3F097CE89ED2",
10214 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
10215 "muted" : false,
10216 "newShot" : false,
10217 "speed" : 1,
10218 "srcIn" : 2950.3389830508468,
10219 "start" : 955.2542372881356,
10220 "track" : "v0"
10221 },
10222 {
10223 "duration" : 32.4406779661017,
10224 "fadeIn" : 0,
10225 "fadeOut" : 0,
10226 "id" : "64CCF21E-AA71-4C5F-9CB3-EAE93F031C97",
10227 "kind" : "audio",
10228 "linkId" : "C90940D8-882F-4CBD-839A-3F097CE89ED2",
10229 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
10230 "muted" : true,
10231 "newShot" : false,
10232 "speed" : 1,
10233 "srcIn" : 2950.2551115095057,
10234 "start" : 955.2542372881356,
10235 "track" : "v1"
10236 },
10237 {
10238 "duration" : 32.4406779661017,
10239 "fadeIn" : 0,
10240 "fadeOut" : 0,
10241 "id" : "0EF2F1D8-B27D-425B-A77E-024D9E8ED834",
10242 "kind" : "audio",
10243 "linkId" : "C90940D8-882F-4CBD-839A-3F097CE89ED2",
10244 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
10245 "muted" : false,
10246 "newShot" : false,
10247 "speed" : 1,
10248 "srcIn" : 2950.3017878127366,
10249 "start" : 955.2542372881356,
10250 "track" : "v2"
10251 },
10252 {
10253 "duration" : 32.4406779661017,
10254 "fadeIn" : 0,
10255 "fadeOut" : 0,
10256 "id" : "7F966B3B-7499-46E3-A145-F4F6CB9899E5",
10257 "kind" : "video",
10258 "linkId" : "C90940D8-882F-4CBD-839A-3F097CE89ED2",
10259 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
10260 "muted" : false,
10261 "newShot" : false,
10262 "speed" : 1,
10263 "srcIn" : 2950.1651806765085,
10264 "start" : 955.2542372881356,
10265 "track" : "v3"
10266 },
10267 {
10268 "duration" : 32.4406779661017,
10269 "fadeIn" : 0,
10270 "fadeOut" : 0,
10271 "id" : "869C0AAC-694F-4A83-ACBD-BA02B2F1EBCA",
10272 "kind" : "video",
10273 "linkId" : "C90940D8-882F-4CBD-839A-3F097CE89ED2",
10274 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
10275 "muted" : false,
10276 "newShot" : false,
10277 "speed" : 1,
10278 "srcIn" : 2950.1651760925097,
10279 "start" : 955.2542372881356,
10280 "track" : "v4"
10281 },
10282 {
10283 "duration" : 2.5762711864406356,
10284 "fadeIn" : 0,
10285 "fadeOut" : 0,
10286 "id" : "25EDDE61-86BB-4F70-A496-23BF74AE9006",
10287 "kind" : "video",
10288 "linkId" : "9D7E5089-781D-46B7-83D2-8B95043855FA",
10289 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
10290 "muted" : false,
10291 "newShot" : false,
10292 "speed" : 1,
10293 "srcIn" : 2983.661016949152,
10294 "start" : 987.6949152542373,
10295 "track" : "v0"
10296 },
10297 {
10298 "duration" : 2.5762711864406356,
10299 "fadeIn" : 0,
10300 "fadeOut" : 0,
10301 "id" : "AAAAE9EC-5D0B-4491-B3AE-40D47947A105",
10302 "kind" : "audio",
10303 "linkId" : "9D7E5089-781D-46B7-83D2-8B95043855FA",
10304 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
10305 "muted" : true,
10306 "newShot" : false,
10307 "speed" : 1,
10308 "srcIn" : 2983.577145407811,
10309 "start" : 987.6949152542373,
10310 "track" : "v1"
10311 },
10312 {
10313 "duration" : 2.5762711864406356,
10314 "fadeIn" : 0,
10315 "fadeOut" : 0,
10316 "id" : "A75BBCB8-738D-4750-B235-F5EC59113946",
10317 "kind" : "audio",
10318 "linkId" : "9D7E5089-781D-46B7-83D2-8B95043855FA",
10319 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
10320 "muted" : false,
10321 "newShot" : false,
10322 "speed" : 1,
10323 "srcIn" : 2983.6238217110417,
10324 "start" : 987.6949152542373,
10325 "track" : "v2"
10326 },
10327 {
10328 "duration" : 2.5762711864406356,
10329 "fadeIn" : 0,
10330 "fadeOut" : 0,
10331 "id" : "71F23B1C-6343-4637-9DFC-DF10EEE27AD4",
10332 "kind" : "video",
10333 "linkId" : "9D7E5089-781D-46B7-83D2-8B95043855FA",
10334 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
10335 "muted" : false,
10336 "newShot" : false,
10337 "speed" : 1,
10338 "srcIn" : 2983.4872145748136,
10339 "start" : 987.6949152542373,
10340 "track" : "v3"
10341 },
10342 {
10343 "duration" : 2.5762711864406356,
10344 "fadeIn" : 0,
10345 "fadeOut" : 0,
10346 "id" : "BFF6C236-F0FD-45E2-B714-265E9B8F2102",
10347 "kind" : "video",
10348 "linkId" : "9D7E5089-781D-46B7-83D2-8B95043855FA",
10349 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
10350 "muted" : false,
10351 "newShot" : false,
10352 "speed" : 1,
10353 "srcIn" : 2983.487209990815,
10354 "start" : 987.6949152542373,
10355 "track" : "v4"
10356 },
10357 {
10358 "duration" : 14.101694915254257,
10359 "fadeIn" : 0,
10360 "fadeOut" : 0,
10361 "id" : "2B6D6836-A007-4343-9775-70DD28CB6EB9",
10362 "kind" : "video",
10363 "linkId" : "EFD99B70-8810-42EC-8F2B-5AA76217327E",
10364 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
10365 "muted" : false,
10366 "newShot" : false,
10367 "speed" : 1,
10368 "srcIn" : 2986.542372881355,
10369 "start" : 990.271186440678,
10370 "track" : "v0"
10371 },
10372 {
10373 "duration" : 14.101694915254257,
10374 "fadeIn" : 0,
10375 "fadeOut" : 0,
10376 "id" : "7E48882C-AD53-4651-90D8-9CBDFEF95F0C",
10377 "kind" : "audio",
10378 "linkId" : "EFD99B70-8810-42EC-8F2B-5AA76217327E",
10379 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
10380 "muted" : true,
10381 "newShot" : false,
10382 "speed" : 1,
10383 "srcIn" : 2986.4585013400138,
10384 "start" : 990.271186440678,
10385 "track" : "v1"
10386 },
10387 {
10388 "duration" : 14.101694915254257,
10389 "fadeIn" : 0,
10390 "fadeOut" : 0,
10391 "id" : "BA449FE2-AFB9-4E77-8799-8FE2429251A7",
10392 "kind" : "audio",
10393 "linkId" : "EFD99B70-8810-42EC-8F2B-5AA76217327E",
10394 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
10395 "muted" : false,
10396 "newShot" : false,
10397 "speed" : 1,
10398 "srcIn" : 2986.5051776432447,
10399 "start" : 990.271186440678,
10400 "track" : "v2"
10401 },
10402 {
10403 "duration" : 14.101694915254257,
10404 "fadeIn" : 0,
10405 "fadeOut" : 0,
10406 "id" : "A3C307CE-4B58-4DB3-B763-3452763F1277",
10407 "kind" : "video",
10408 "linkId" : "EFD99B70-8810-42EC-8F2B-5AA76217327E",
10409 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
10410 "muted" : false,
10411 "newShot" : false,
10412 "speed" : 1,
10413 "srcIn" : 2986.3685705070166,
10414 "start" : 990.271186440678,
10415 "track" : "v3"
10416 },
10417 {
10418 "duration" : 14.101694915254257,
10419 "fadeIn" : 0,
10420 "fadeOut" : 0,
10421 "id" : "322BFB74-8CE6-4C2F-950C-898A113AB445",
10422 "kind" : "video",
10423 "linkId" : "EFD99B70-8810-42EC-8F2B-5AA76217327E",
10424 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
10425 "muted" : false,
10426 "newShot" : false,
10427 "speed" : 1,
10428 "srcIn" : 2986.3685659230177,
10429 "start" : 990.271186440678,
10430 "track" : "v4"
10431 },
10432 {
10433 "duration" : 41.18644067796606,
10434 "fadeIn" : 0,
10435 "fadeOut" : 0,
10436 "id" : "1C0BB1B6-1862-4A8E-B78B-26DBECD3F37B",
10437 "kind" : "video",
10438 "linkId" : "9F7D047B-0DE6-4655-9D6E-5B8ADDD0267B",
10439 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
10440 "muted" : false,
10441 "newShot" : false,
10442 "speed" : 1,
10443 "srcIn" : 3016.4745762711855,
10444 "start" : 1004.3728813559322,
10445 "track" : "v0"
10446 },
10447 {
10448 "duration" : 41.18644067796606,
10449 "fadeIn" : 0,
10450 "fadeOut" : 0,
10451 "id" : "3DF07444-8948-4073-A7B7-DCD022163D06",
10452 "kind" : "audio",
10453 "linkId" : "9F7D047B-0DE6-4655-9D6E-5B8ADDD0267B",
10454 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
10455 "muted" : true,
10456 "newShot" : false,
10457 "speed" : 1,
10458 "srcIn" : 3016.3907047298444,
10459 "start" : 1004.3728813559322,
10460 "track" : "v1"
10461 },
10462 {
10463 "duration" : 41.18644067796606,
10464 "fadeIn" : 0,
10465 "fadeOut" : 0,
10466 "id" : "17518456-794B-46AE-8B57-11535384C6DD",
10467 "kind" : "audio",
10468 "linkId" : "9F7D047B-0DE6-4655-9D6E-5B8ADDD0267B",
10469 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
10470 "muted" : false,
10471 "newShot" : false,
10472 "speed" : 1,
10473 "srcIn" : 3016.4373810330753,
10474 "start" : 1004.3728813559322,
10475 "track" : "v2"
10476 },
10477 {
10478 "duration" : 41.18644067796606,
10479 "fadeIn" : 0,
10480 "fadeOut" : 0,
10481 "id" : "D83C6190-0569-4756-B034-B56F5EA95C6A",
10482 "kind" : "video",
10483 "linkId" : "9F7D047B-0DE6-4655-9D6E-5B8ADDD0267B",
10484 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
10485 "muted" : false,
10486 "newShot" : false,
10487 "speed" : 1,
10488 "srcIn" : 3016.300773896847,
10489 "start" : 1004.3728813559322,
10490 "track" : "v3"
10491 },
10492 {
10493 "duration" : 41.18644067796606,
10494 "fadeIn" : 0,
10495 "fadeOut" : 0,
10496 "id" : "04909166-A121-49EA-B441-2498FB853E8E",
10497 "kind" : "video",
10498 "linkId" : "9F7D047B-0DE6-4655-9D6E-5B8ADDD0267B",
10499 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
10500 "muted" : false,
10501 "newShot" : false,
10502 "speed" : 1,
10503 "srcIn" : 3016.3007693128484,
10504 "start" : 1004.3728813559322,
10505 "track" : "v4"
10506 },
10507 {
10508 "duration" : 1.661016949152554,
10509 "fadeIn" : 0,
10510 "fadeOut" : 0,
10511 "id" : "AB7E6E67-21DC-473D-A4F5-32C751A46EE1",
10512 "kind" : "video",
10513 "linkId" : "0B407464-C16F-4EE0-8709-4530856B5481",
10514 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
10515 "muted" : false,
10516 "newShot" : false,
10517 "speed" : 1,
10518 "srcIn" : 3065.593220338982,
10519 "start" : 1045.5593220338983,
10520 "track" : "v0"
10521 },
10522 {
10523 "duration" : 1.661016949152554,
10524 "fadeIn" : 0,
10525 "fadeOut" : 0,
10526 "id" : "3945B5C6-C770-47E5-BF52-BC27D9ABFAE0",
10527 "kind" : "audio",
10528 "linkId" : "0B407464-C16F-4EE0-8709-4530856B5481",
10529 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
10530 "muted" : true,
10531 "newShot" : false,
10532 "speed" : 1,
10533 "srcIn" : 3065.509348797641,
10534 "start" : 1045.5593220338983,
10535 "track" : "v1"
10536 },
10537 {
10538 "duration" : 1.661016949152554,
10539 "fadeIn" : 0,
10540 "fadeOut" : 0,
10541 "id" : "D21DCFAE-885B-46AB-BBCD-46F5F68A4841",
10542 "kind" : "audio",
10543 "linkId" : "0B407464-C16F-4EE0-8709-4530856B5481",
10544 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
10545 "muted" : false,
10546 "newShot" : false,
10547 "speed" : 1,
10548 "srcIn" : 3065.556025100872,
10549 "start" : 1045.5593220338983,
10550 "track" : "v2"
10551 },
10552 {
10553 "duration" : 1.661016949152554,
10554 "fadeIn" : 0,
10555 "fadeOut" : 0,
10556 "id" : "09CE071F-2BC1-44E9-80B4-A3B494754324",
10557 "kind" : "video",
10558 "linkId" : "0B407464-C16F-4EE0-8709-4530856B5481",
10559 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
10560 "muted" : false,
10561 "newShot" : false,
10562 "speed" : 1,
10563 "srcIn" : 3065.419417964644,
10564 "start" : 1045.5593220338983,
10565 "track" : "v3"
10566 },
10567 {
10568 "duration" : 1.661016949152554,
10569 "fadeIn" : 0,
10570 "fadeOut" : 0,
10571 "id" : "E59CB212-BAFB-4792-9E87-27750E214AFF",
10572 "kind" : "video",
10573 "linkId" : "0B407464-C16F-4EE0-8709-4530856B5481",
10574 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
10575 "muted" : false,
10576 "newShot" : false,
10577 "speed" : 1,
10578 "srcIn" : 3065.419413380645,
10579 "start" : 1045.5593220338983,
10580 "track" : "v4"
10581 },
10582 {
10583 "duration" : 4.745762711864472,
10584 "fadeIn" : 0,
10585 "fadeOut" : 0,
10586 "id" : "E92C9D2B-F806-42DF-BBBD-6444CED9CFA0",
10587 "kind" : "video",
10588 "linkId" : "BBA33081-9695-4391-B23B-D376880D264E",
10589 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
10590 "muted" : false,
10591 "newShot" : false,
10592 "speed" : 1,
10593 "srcIn" : 3070.203389830507,
10594 "start" : 1047.2203389830509,
10595 "track" : "v0"
10596 },
10597 {
10598 "duration" : 4.745762711864472,
10599 "fadeIn" : 0,
10600 "fadeOut" : 0,
10601 "id" : "C13910BD-C1D5-42AA-B25A-F4AFA7E0807E",
10602 "kind" : "audio",
10603 "linkId" : "BBA33081-9695-4391-B23B-D376880D264E",
10604 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
10605 "muted" : true,
10606 "newShot" : false,
10607 "speed" : 1,
10608 "srcIn" : 3070.119518289166,
10609 "start" : 1047.2203389830509,
10610 "track" : "v1"
10611 },
10612 {
10613 "duration" : 4.745762711864472,
10614 "fadeIn" : 0,
10615 "fadeOut" : 0,
10616 "id" : "6A6D3754-91FF-45E9-9A9C-EC342D95E136",
10617 "kind" : "audio",
10618 "linkId" : "BBA33081-9695-4391-B23B-D376880D264E",
10619 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
10620 "muted" : false,
10621 "newShot" : false,
10622 "speed" : 1,
10623 "srcIn" : 3070.166194592397,
10624 "start" : 1047.2203389830509,
10625 "track" : "v2"
10626 },
10627 {
10628 "duration" : 4.745762711864472,
10629 "fadeIn" : 0,
10630 "fadeOut" : 0,
10631 "id" : "122D9228-98AA-4877-A0C4-2DFD8CAE845B",
10632 "kind" : "video",
10633 "linkId" : "BBA33081-9695-4391-B23B-D376880D264E",
10634 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
10635 "muted" : false,
10636 "newShot" : false,
10637 "speed" : 1,
10638 "srcIn" : 3070.029587456169,
10639 "start" : 1047.2203389830509,
10640 "track" : "v3"
10641 },
10642 {
10643 "duration" : 4.745762711864472,
10644 "fadeIn" : 0,
10645 "fadeOut" : 0,
10646 "id" : "9124C8F4-A9B8-40C9-A87D-03794D9BFD5B",
10647 "kind" : "video",
10648 "linkId" : "BBA33081-9695-4391-B23B-D376880D264E",
10649 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
10650 "muted" : false,
10651 "newShot" : false,
10652 "speed" : 1,
10653 "srcIn" : 3070.02958287217,
10654 "start" : 1047.2203389830509,
10655 "track" : "v4"
10656 },
10657 {
10658 "duration" : 11.93220338983042,
10659 "fadeIn" : 0,
10660 "fadeOut" : 0,
10661 "id" : "28D0F5F8-073A-4EB4-ABCD-C489AF0CA087",
10662 "kind" : "video",
10663 "linkId" : "318B4FD4-E1D7-41FD-B760-CA9E5DAB9B1F",
10664 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
10665 "muted" : false,
10666 "newShot" : false,
10667 "speed" : 1,
10668 "srcIn" : 3082.271186440677,
10669 "start" : 1051.9661016949153,
10670 "track" : "v0"
10671 },
10672 {
10673 "duration" : 11.93220338983042,
10674 "fadeIn" : 0,
10675 "fadeOut" : 0,
10676 "id" : "2B967FFC-3967-43AC-B348-992E7603AE2C",
10677 "kind" : "audio",
10678 "linkId" : "318B4FD4-E1D7-41FD-B760-CA9E5DAB9B1F",
10679 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
10680 "muted" : true,
10681 "newShot" : false,
10682 "speed" : 1,
10683 "srcIn" : 3082.187314899336,
10684 "start" : 1051.9661016949153,
10685 "track" : "v1"
10686 },
10687 {
10688 "duration" : 11.93220338983042,
10689 "fadeIn" : 0,
10690 "fadeOut" : 0,
10691 "id" : "5DCE6786-7E43-4D1A-9415-C2E364A3C1E5",
10692 "kind" : "audio",
10693 "linkId" : "318B4FD4-E1D7-41FD-B760-CA9E5DAB9B1F",
10694 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
10695 "muted" : false,
10696 "newShot" : false,
10697 "speed" : 1,
10698 "srcIn" : 3082.233991202567,
10699 "start" : 1051.9661016949153,
10700 "track" : "v2"
10701 },
10702 {
10703 "duration" : 11.93220338983042,
10704 "fadeIn" : 0,
10705 "fadeOut" : 0,
10706 "id" : "CCB5A926-E4B6-4AA9-9EF3-2E8A78E7B2F2",
10707 "kind" : "video",
10708 "linkId" : "318B4FD4-E1D7-41FD-B760-CA9E5DAB9B1F",
10709 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
10710 "muted" : false,
10711 "newShot" : false,
10712 "speed" : 1,
10713 "srcIn" : 3082.0973840663387,
10714 "start" : 1051.9661016949153,
10715 "track" : "v3"
10716 },
10717 {
10718 "duration" : 11.93220338983042,
10719 "fadeIn" : 0,
10720 "fadeOut" : 0,
10721 "id" : "296DECA8-17FA-4732-85F9-E7E62AB4390E",
10722 "kind" : "video",
10723 "linkId" : "318B4FD4-E1D7-41FD-B760-CA9E5DAB9B1F",
10724 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
10725 "muted" : false,
10726 "newShot" : false,
10727 "speed" : 1,
10728 "srcIn" : 3082.09737948234,
10729 "start" : 1051.9661016949153,
10730 "track" : "v4"
10731 },
10732 {
10733 "duration" : 2.7118644067795685,
10734 "fadeIn" : 0,
10735 "fadeOut" : 0,
10736 "id" : "F118809D-1E6F-4E06-9DD8-59627D7FD31E",
10737 "kind" : "video",
10738 "linkId" : "D27F02ED-F905-4600-B733-AF4B73B5174F",
10739 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
10740 "muted" : false,
10741 "newShot" : false,
10742 "speed" : 1,
10743 "srcIn" : 3097.559322033897,
10744 "start" : 1063.8983050847457,
10745 "track" : "v0"
10746 },
10747 {
10748 "duration" : 2.7118644067795685,
10749 "fadeIn" : 0,
10750 "fadeOut" : 0,
10751 "id" : "4A2040D0-20BE-429F-AD73-5B3ED24C0A6D",
10752 "kind" : "audio",
10753 "linkId" : "D27F02ED-F905-4600-B733-AF4B73B5174F",
10754 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
10755 "muted" : true,
10756 "newShot" : false,
10757 "speed" : 1,
10758 "srcIn" : 3097.475450492556,
10759 "start" : 1063.8983050847457,
10760 "track" : "v1"
10761 },
10762 {
10763 "duration" : 2.7118644067795685,
10764 "fadeIn" : 0,
10765 "fadeOut" : 0,
10766 "id" : "3D418272-0D62-4126-A9C7-71E7D7FC255D",
10767 "kind" : "audio",
10768 "linkId" : "D27F02ED-F905-4600-B733-AF4B73B5174F",
10769 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
10770 "muted" : false,
10771 "newShot" : false,
10772 "speed" : 1,
10773 "srcIn" : 3097.522126795787,
10774 "start" : 1063.8983050847457,
10775 "track" : "v2"
10776 },
10777 {
10778 "duration" : 2.7118644067795685,
10779 "fadeIn" : 0,
10780 "fadeOut" : 0,
10781 "id" : "B50F2156-B9AC-4F52-B362-40B0B0D732A4",
10782 "kind" : "video",
10783 "linkId" : "D27F02ED-F905-4600-B733-AF4B73B5174F",
10784 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
10785 "muted" : false,
10786 "newShot" : false,
10787 "speed" : 1,
10788 "srcIn" : 3097.3855196595587,
10789 "start" : 1063.8983050847457,
10790 "track" : "v3"
10791 },
10792 {
10793 "duration" : 2.7118644067795685,
10794 "fadeIn" : 0,
10795 "fadeOut" : 0,
10796 "id" : "8C35ED3F-0857-4157-9DDF-8937A65375E5",
10797 "kind" : "video",
10798 "linkId" : "D27F02ED-F905-4600-B733-AF4B73B5174F",
10799 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
10800 "muted" : false,
10801 "newShot" : false,
10802 "speed" : 1,
10803 "srcIn" : 3097.38551507556,
10804 "start" : 1063.8983050847457,
10805 "track" : "v4"
10806 },
10807 {
10808 "duration" : 29.864406779661067,
10809 "fadeIn" : 0,
10810 "fadeOut" : 0,
10811 "id" : "5F3F6F18-5791-47A8-BCA4-A9790558A314",
10812 "kind" : "video",
10813 "linkId" : "0F08CC53-E7F9-4A91-9D1E-F73D91D3ECB9",
10814 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
10815 "muted" : false,
10816 "newShot" : false,
10817 "speed" : 1,
10818 "srcIn" : 3101.3559322033884,
10819 "start" : 1066.6101694915253,
10820 "track" : "v0"
10821 },
10822 {
10823 "duration" : 29.864406779661067,
10824 "fadeIn" : 0,
10825 "fadeOut" : 0,
10826 "id" : "CF1D8F7D-26D5-4CD2-8D99-2370B29915C0",
10827 "kind" : "audio",
10828 "linkId" : "0F08CC53-E7F9-4A91-9D1E-F73D91D3ECB9",
10829 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
10830 "muted" : true,
10831 "newShot" : false,
10832 "speed" : 1,
10833 "srcIn" : 3101.2720606620474,
10834 "start" : 1066.6101694915253,
10835 "track" : "v1"
10836 },
10837 {
10838 "duration" : 29.864406779661067,
10839 "fadeIn" : 0,
10840 "fadeOut" : 0,
10841 "id" : "A1160BCB-0740-4FC5-BEA2-A90D6030BFA6",
10842 "kind" : "audio",
10843 "linkId" : "0F08CC53-E7F9-4A91-9D1E-F73D91D3ECB9",
10844 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
10845 "muted" : false,
10846 "newShot" : false,
10847 "speed" : 1,
10848 "srcIn" : 3101.3187369652783,
10849 "start" : 1066.6101694915253,
10850 "track" : "v2"
10851 },
10852 {
10853 "duration" : 29.864406779661067,
10854 "fadeIn" : 0,
10855 "fadeOut" : 0,
10856 "id" : "8E323D18-4C84-4AFA-B90D-264EE10DBF94",
10857 "kind" : "video",
10858 "linkId" : "0F08CC53-E7F9-4A91-9D1E-F73D91D3ECB9",
10859 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
10860 "muted" : false,
10861 "newShot" : false,
10862 "speed" : 1,
10863 "srcIn" : 3101.18212982905,
10864 "start" : 1066.6101694915253,
10865 "track" : "v3"
10866 },
10867 {
10868 "duration" : 29.864406779661067,
10869 "fadeIn" : 0,
10870 "fadeOut" : 0,
10871 "id" : "A1E18A2B-C2ED-4F4F-B090-2F04AB1EAB86",
10872 "kind" : "video",
10873 "linkId" : "0F08CC53-E7F9-4A91-9D1E-F73D91D3ECB9",
10874 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
10875 "muted" : false,
10876 "newShot" : false,
10877 "speed" : 1,
10878 "srcIn" : 3101.1821252450513,
10879 "start" : 1066.6101694915253,
10880 "track" : "v4"
10881 },
10882 {
10883 "duration" : 3046.474576440677,
10884 "fadeIn" : 0,
10885 "fadeOut" : 0,
10886 "id" : "77231674-DFD4-469D-8BD3-B0384766ED44",
10887 "kind" : "video",
10888 "linkId" : "06145E01-F314-4D25-9F5A-C1D23472B857",
10889 "mediaId" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
10890 "muted" : false,
10891 "newShot" : false,
10892 "speed" : 1,
10893 "srcIn" : 3175.7288135593208,
10894 "start" : 1096.4745762711864,
10895 "track" : "v0"
10896 },
10897 {
10898 "duration" : 3046.477724982019,
10899 "fadeIn" : 0,
10900 "fadeOut" : 0,
10901 "id" : "46D41482-2761-4F49-8714-E482340928D2",
10902 "kind" : "audio",
10903 "linkId" : "06145E01-F314-4D25-9F5A-C1D23472B857",
10904 "mediaId" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
10905 "muted" : true,
10906 "newShot" : false,
10907 "speed" : 1,
10908 "srcIn" : 3175.6449420179797,
10909 "start" : 1096.4745762711864,
10910 "track" : "v1"
10911 },
10912 {
10913 "duration" : 3046.516381678787,
10914 "fadeIn" : 0,
10915 "fadeOut" : 0,
10916 "id" : "6655D05A-2F89-4E98-A2C0-475D1908D6DB",
10917 "kind" : "audio",
10918 "linkId" : "06145E01-F314-4D25-9F5A-C1D23472B857",
10919 "mediaId" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
10920 "muted" : false,
10921 "newShot" : false,
10922 "speed" : 1,
10923 "srcIn" : 3175.6916183212106,
10924 "start" : 1096.4745762711864,
10925 "track" : "v2"
10926 },
10927 {
10928 "duration" : 3046.5116558150157,
10929 "fadeIn" : 0,
10930 "fadeOut" : 0,
10931 "id" : "0BA9AB3D-B156-466B-A477-DD6CBCC6D5D0",
10932 "kind" : "video",
10933 "linkId" : "06145E01-F314-4D25-9F5A-C1D23472B857",
10934 "mediaId" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
10935 "muted" : false,
10936 "newShot" : false,
10937 "speed" : 1,
10938 "srcIn" : 3175.5550111849825,
10939 "start" : 1096.4745762711864,
10940 "track" : "v3"
10941 },
10942 {
10943 "duration" : 3046.5116603990145,
10944 "fadeIn" : 0,
10945 "fadeOut" : 0,
10946 "id" : "FA5C83A2-F1FC-4E94-B0A3-D12DF73B9D65",
10947 "kind" : "video",
10948 "linkId" : "06145E01-F314-4D25-9F5A-C1D23472B857",
10949 "mediaId" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
10950 "muted" : false,
10951 "newShot" : false,
10952 "speed" : 1,
10953 "srcIn" : 3175.5550066009837,
10954 "start" : 1096.4745762711864,
10955 "track" : "v4"
10956 }
10957 ],
10958 "fps" : 29.5,
10959 "markers" : [
10960
10961 ],
10962 "media" : [
10963 {
10964 "cacheKey" : "cb2b0cb1418eb04f",
10965 "duration" : 812.745763,
10966 "fps" : 29.5,
10967 "hasAudio" : false,
10968 "height" : 720,
10969 "id" : "26CBEF77-6850-4D82-A5BD-A6A37587C040",
10970 "isAudio" : false,
10971 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_13.49\/cam.mov",
10972 "width" : 1280
10973 },
10974 {
10975 "cacheKey" : "49108ecb3168df5a",
10976 "duration" : 812.885333,
10977 "fps" : 30,
10978 "hasAudio" : true,
10979 "height" : 0,
10980 "id" : "F676BA7D-A0E0-4603-AE13-B853D2F9F2BB",
10981 "isAudio" : true,
10982 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_13.49\/desktop.m4a",
10983 "width" : 0
10984 },
10985 {
10986 "cacheKey" : "9f6621781616504d",
10987 "duration" : 813.269333,
10988 "fps" : 30,
10989 "hasAudio" : true,
10990 "height" : 0,
10991 "id" : "6FFA3786-8E51-4E69-A15D-D6A991C0CCAD",
10992 "isAudio" : true,
10993 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_13.49\/mic.m4a",
10994 "width" : 0
10995 },
10996 {
10997 "cacheKey" : "fbe623fb3a0f34ba",
10998 "duration" : 812.833333,
10999 "fps" : 30,
11000 "hasAudio" : false,
11001 "height" : 2160,
11002 "id" : "609E978E-5994-4757-A6C7-EAAE72818A49",
11003 "isAudio" : false,
11004 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_13.49\/screen-1.mov",
11005 "width" : 3840
11006 },
11007 {
11008 "cacheKey" : "1be7aaff98d7b0ff",
11009 "duration" : 812.833333,
11010 "fps" : 30,
11011 "hasAudio" : false,
11012 "height" : 2160,
11013 "id" : "C0E05C14-2D32-4A55-9041-7A752A97F6B1",
11014 "isAudio" : false,
11015 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_13.49\/screen-2.mov",
11016 "width" : 3840
11017 },
11018 {
11019 "cacheKey" : "bc4651748a6b1673",
11020 "duration" : 6222.20339,
11021 "fps" : 29.5,
11022 "hasAudio" : false,
11023 "height" : 720,
11024 "id" : "40BBB768-793D-44DA-BBC9-B22F6591159D",
11025 "isAudio" : false,
11026 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_14.03\/cam.mov",
11027 "width" : 1280
11028 },
11029 {
11030 "cacheKey" : "27d4dedeccdacecc",
11031 "duration" : 6222.122667,
11032 "fps" : 30,
11033 "hasAudio" : true,
11034 "height" : 0,
11035 "id" : "55C13A17-0423-4BC0-93A6-3FEFD02E7FC6",
11036 "isAudio" : true,
11037 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_14.03\/desktop.m4a",
11038 "width" : 0
11039 },
11040 {
11041 "cacheKey" : "103551fc583e9882",
11042 "duration" : 6222.208,
11043 "fps" : 30,
11044 "hasAudio" : true,
11045 "height" : 0,
11046 "id" : "501034F8-09AC-44B1-8826-FCE9B36C68F9",
11047 "isAudio" : true,
11048 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_14.03\/mic.m4a",
11049 "width" : 0
11050 },
11051 {
11052 "cacheKey" : "5bfa7a15f159568d",
11053 "duration" : 6222.066667,
11054 "fps" : 30,
11055 "hasAudio" : false,
11056 "height" : 2160,
11057 "id" : "D866E8BE-A5B1-4B51-B465-D3F753701CCE",
11058 "isAudio" : false,
11059 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_14.03\/screen-1.mov",
11060 "width" : 3840
11061 },
11062 {
11063 "cacheKey" : "8cea14167c6e9778",
11064 "duration" : 6222.066667,
11065 "fps" : 30,
11066 "hasAudio" : false,
11067 "height" : 2160,
11068 "id" : "92E85F7F-CA8B-473D-82F6-C7A7418875B1",
11069 "isAudio" : false,
11070 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_14.03\/screen-2.mov",
11071 "width" : 3840
11072 },
11073 {
11074 "cacheKey" : "b966487289885ec1",
11075 "duration" : 15112.237288,
11076 "fps" : 29.5,
11077 "hasAudio" : false,
11078 "height" : 720,
11079 "id" : "CC300C55-7090-4A6E-81FA-55CB6644FAC9",
11080 "isAudio" : false,
11081 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_16.36\/cam.mov",
11082 "width" : 1280
11083 },
11084 {
11085 "cacheKey" : "9044d1d53bcf480f",
11086 "duration" : 15112.106667,
11087 "fps" : 30,
11088 "hasAudio" : true,
11089 "height" : 0,
11090 "id" : "2613683E-12E6-402E-BF4D-6D9ACD7C1491",
11091 "isAudio" : true,
11092 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_16.36\/desktop.m4a",
11093 "width" : 0
11094 },
11095 {
11096 "cacheKey" : "36fc42c715b64632",
11097 "duration" : 15112.213333,
11098 "fps" : 30,
11099 "hasAudio" : true,
11100 "height" : 0,
11101 "id" : "7889E9EC-F67A-491B-81C0-0CF8338965A3",
11102 "isAudio" : true,
11103 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_16.36\/mic.m4a",
11104 "width" : 0
11105 },
11106 {
11107 "cacheKey" : "0308132cb01a092a",
11108 "duration" : 15112.066667,
11109 "fps" : 30,
11110 "hasAudio" : false,
11111 "height" : 2160,
11112 "id" : "BC4E74C5-4C45-4633-B31A-3CAA58D4B2BF",
11113 "isAudio" : false,
11114 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_16.36\/screen-1.mov",
11115 "width" : 3840
11116 },
11117 {
11118 "cacheKey" : "270afa9f16a6fb41",
11119 "duration" : 15111.366667,
11120 "fps" : 30,
11121 "hasAudio" : false,
11122 "height" : 2160,
11123 "id" : "98882264-A2ED-477B-A7B8-B63E54EDEACE",
11124 "isAudio" : false,
11125 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_16.36\/screen-2.mov",
11126 "width" : 3840
11127 },
11128 {
11129 "cacheKey" : "02a6eb97d64d4630",
11130 "duration" : 10045.254237,
11131 "fps" : 29.5,
11132 "hasAudio" : false,
11133 "height" : 720,
11134 "id" : "2D5EED57-87EB-44B4-9623-2CF48314C5F9",
11135 "isAudio" : false,
11136 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_22.54\/cam.mov",
11137 "width" : 1280
11138 },
11139 {
11140 "cacheKey" : "cc655edd968d5060",
11141 "duration" : 10045.162667,
11142 "fps" : 30,
11143 "hasAudio" : true,
11144 "height" : 0,
11145 "id" : "0ECB1F4F-9065-432F-B0B1-7F47BD31FDCC",
11146 "isAudio" : true,
11147 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_22.54\/desktop.m4a",
11148 "width" : 0
11149 },
11150 {
11151 "cacheKey" : "4a0d44fc07d2db63",
11152 "duration" : 10045.248,
11153 "fps" : 30,
11154 "hasAudio" : true,
11155 "height" : 0,
11156 "id" : "02A5DFB3-9348-4102-BD13-F1856B431A70",
11157 "isAudio" : true,
11158 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_22.54\/mic.m4a",
11159 "width" : 0
11160 },
11161 {
11162 "cacheKey" : "edc26bd451155492",
11163 "duration" : 10045.133333,
11164 "fps" : 30,
11165 "hasAudio" : false,
11166 "height" : 2160,
11167 "id" : "9CA51332-5865-4C3D-9972-E781BDE75ECD",
11168 "isAudio" : false,
11169 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_22.54\/screen-1.mov",
11170 "width" : 3840
11171 },
11172 {
11173 "cacheKey" : "3c45ae09f47beeae",
11174 "duration" : 10045.166667,
11175 "fps" : 30,
11176 "hasAudio" : false,
11177 "height" : 2160,
11178 "id" : "0DEB0D7E-0C1E-49DA-9911-7773FB2158DE",
11179 "isAudio" : false,
11180 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-03_22.54\/screen-2.mov",
11181 "width" : 3840
11182 },
11183 {
11184 "cacheKey" : "81f385d21caafc19",
11185 "duration" : 11597.966102,
11186 "fps" : 29.5,
11187 "hasAudio" : false,
11188 "height" : 720,
11189 "id" : "F06DF483-F96F-4DD1-8602-53609FA888F1",
11190 "isAudio" : false,
11191 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-04_17.30\/cam.mov",
11192 "width" : 1280
11193 },
11194 {
11195 "cacheKey" : "88621a686d385fe8",
11196 "duration" : 11598.101333,
11197 "fps" : 30,
11198 "hasAudio" : true,
11199 "height" : 0,
11200 "id" : "59213540-870A-4795-BD44-B3AFBC9F14E3",
11201 "isAudio" : true,
11202 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-04_17.30\/desktop.m4a",
11203 "width" : 0
11204 },
11205 {
11206 "cacheKey" : "3ee60110dd18110c",
11207 "duration" : 11598.464,
11208 "fps" : 30,
11209 "hasAudio" : true,
11210 "height" : 0,
11211 "id" : "F4DB0AF3-2C74-4501-A58A-A519C046E3D6",
11212 "isAudio" : true,
11213 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-04_17.30\/mic.m4a",
11214 "width" : 0
11215 },
11216 {
11217 "cacheKey" : "290f82433dd61a4c",
11218 "duration" : 11598.033333,
11219 "fps" : 30,
11220 "hasAudio" : false,
11221 "height" : 2160,
11222 "id" : "9F95D76B-EAB2-49AA-897A-C3C9A265E0CC",
11223 "isAudio" : false,
11224 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-04_17.30\/screen-1.mov",
11225 "width" : 3840
11226 },
11227 {
11228 "cacheKey" : "12165002971af6ff",
11229 "duration" : 11598,
11230 "fps" : 30,
11231 "hasAudio" : false,
11232 "height" : 2160,
11233 "id" : "CC497E03-E3E7-4283-A96C-F076D9F154A1",
11234 "isAudio" : false,
11235 "path" : "\/Volumes\/clover\/Archive\/2026\/Journal\/2026-07-04_17.30\/screen-2.mov",
11236 "width" : 3840
11237 }
11238 ],
11239 "preferredTakes" : [
11240
11241 ],
11242 "tracks" : [
11243 {
11244 "hue" : 0
11245 },
11246 {
11247 "hue" : 0.6180339887498949
11248 },
11249 {
11250 "hue" : 0.2360679774997898
11251 },
11252 {
11253 "hue" : 0.8541019662496847
11254 },
11255 {
11256 "hue" : 0.4721359549995796
11257 }
11258 ]
11259 },
11260 "view" : {
11261 "focusedTracks" : [
11262
11263 ],
11264 "fusionFocus" : false,
11265 "fusionHidden" : false,
11266 "hiddenTracks" : [
11267
11268 ],
11269 "laneScale" : 1.5562744140625,
11270 "previewsOnLeft" : false,
11271 "priorityPane" : "v4",
11272 "showFilmstrips" : true,
11273 "snapping" : true,
11274 "trackHeights" : [
11275 {
11276 "factor" : 0.99639892578125,
11277 "track" : "v0"
11278 },
11279 {
11280 "factor" : 1,
11281 "track" : "v4"
11282 }
11283 ]
11284 }
11285}
\ No newline at end of file
sequencer/Sources/Sequencer/AppDelegate.swift+7
...@@ -52,6 +52,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {...@@ -52,6 +52,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
52 /// (Fusion-feeding) workflow.52 /// (Fusion-feeding) workflow.
53 func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true }53 func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true }
5454
55 /// In-flight ffmpeg encodes must die with the app: orphans hold shared
56 /// VideoToolbox decode sessions, and a few accumulated ones make every
57 /// AVPlayer in the NEXT instance render black (see MediaPipeline).
58 func applicationWillTerminate(_ notification: Notification) {
59 MediaPipeline.terminateChildren()
60 }
61
55 /// On launch, reopen the most recent project instead of a blank untitled62 /// On launch, reopen the most recent project instead of a blank untitled
56 /// one; fall back to a fresh untitled document when there's no history.63 /// one; fall back to a fresh untitled document when there's no history.
57 func applicationOpenUntitledFile(_ sender: NSApplication) -> Bool {64 func applicationOpenUntitledFile(_ sender: NSApplication) -> Bool {
sequencer/Sources/Sequencer/Cachetest.swift created+132
...@@ -0,0 +1,132 @@
1import Foundation
2
3/// Headless cache-budget check: `sequencer --cachetest <file.sq> [capGB]`.
4///
5/// Loads the real project into the headless context, jumps the playhead to
6/// several spots spread across the timeline, and lets demand builds +
7/// evictions run at each stop while continuously asserting the invariant
8/// that makes the cap a hard ceiling:
9///
10/// ledger + reserved ≤ cap (checked every second)
11/// bytes on disk ≤ cap + slack (checked every few seconds)
12///
13/// At each stop it also waits for the chunk under the playhead to become
14/// covered — proving the system optimizes around where the user is even when
15/// the whole project can't fit (the "Deltarune problem").
16func runCacheTest(path: String, capGB: Int) {
17 func spin(_ seconds: Double) {
18 RunLoop.main.run(until: Date().addingTimeInterval(seconds))
19 }
20
21 print("== Sequencer cachetest ==")
22 if capGB > 0 { UserDefaults.standard.set(capGB, forKey: "maxCacheGB") }
23 let pipeline = MediaPipeline.shared
24 let cap = pipeline.maxCacheBytes
25 print("cache: \(pipeline.cacheRoot.path)")
26 print(String(format: "cap: %.1f GB", Double(cap) / 1e9))
27
28 // Load the project (package or legacy flat file) into the headless context.
29 let url = URL(fileURLWithPath: path)
30 var jsonURL = url
31 var isDir: ObjCBool = false
32 if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir), isDir.boolValue {
33 jsonURL = url.appendingPathComponent("project.json")
34 }
35 guard let data = try? Data(contentsOf: jsonURL),
36 let doc = try? JSONDecoder().decode(SequencerDocument.self, from: data) else {
37 print("FAIL: cannot read project at \(path)")
38 return
39 }
40 let ctx = DocumentContext.headless
41 ctx.store.adopt(doc.project)
42 let project = ctx.store.project
43 let span = project.clips.map(\.end).max() ?? 0
44 print(String(format: "project: %d clips, %d media, %.0f s timeline",
45 project.clips.count, project.media.count, span))
46
47 // Wait for the launch reconcile so admission is open.
48 let bootDeadline = Date().addingTimeInterval(60)
49 while !pipeline.budgetReady && Date() < bootDeadline { spin(0.1) }
50 guard pipeline.budgetReady else { print("FAIL: ledger reconcile timed out"); return }
51
52 // Playhead stops: spread across the timeline, snapped to the midpoint of a
53 // real video clip so there is always something to cover.
54 var stops: [Double] = []
55 for frac in [0.05, 0.35, 0.65, 0.9] {
56 let t = span * frac
57 if let clip = project.clips
58 .filter({ $0.kind == .video })
59 .min(by: { abs(($0.start + $0.end) / 2 - t) < abs(($1.start + $1.end) / 2 - t) }) {
60 let mid = (clip.start + clip.end) / 2
61 if !stops.contains(where: { abs($0 - mid) < 60 }) { stops.append(mid) }
62 }
63 }
64
65 var failures = 0
66 var walkTick = 0
67 func checkInvariant(_ label: String) {
68 let held = pipeline.ledgerBytes <= cap
69 if !held {
70 failures += 1
71 print(String(format: "FAIL: ledger %.2f GB over cap (%@)",
72 Double(pipeline.ledgerBytes) / 1e9, label))
73 }
74 // The disk walk is the ground truth the ledger approximates. Slack
75 // covers unbudgeted small writes (filmstrips) between reconciles.
76 walkTick += 1
77 if walkTick % 5 == 0 {
78 let disk = MediaPipeline.directorySize(pipeline.cacheRoot)
79 if disk > cap + 500_000_000 {
80 failures += 1
81 print(String(format: "FAIL: %.2f GB on disk exceeds cap (%@)",
82 Double(disk) / 1e9, label))
83 }
84 }
85 }
86
87 for (i, t) in stops.enumerated() {
88 print(String(format: "\n-- stop %d: playhead %.0f s", i + 1, t))
89 ctx.playback.seek(to: t)
90 ctx.chunks.ensure(for: project)
91 ctx.chunks.updateDemand(force: true)
92 let clipsHere = project.clips.filter {
93 $0.kind == .video && $0.start <= t && t < $0.end
94 }
95 let deadline = Date().addingTimeInterval(150)
96 var covered = false
97 var announced = false
98 while Date() < deadline {
99 spin(1.0)
100 checkInvariant("stop \(i + 1)")
101 var missing: [String] = []
102 for clip in clipsHere {
103 guard let media = project.media(clip.mediaId) else { continue }
104 let src = clip.srcIn + (t - clip.start) * clip.speed
105 if !ctx.chunks.isCovered(media: media, sourceTime: src)
106 && !ctx.chunks.buildFailed(media: media, sourceTime: src) {
107 missing.append("\(media.cacheKey.prefix(8))#\(ChunkManager.chunkIndex(forSource: src))")
108 }
109 }
110 covered = missing.isEmpty
111 if covered { break }
112 if !announced {
113 announced = true
114 NSLog("[cachetest] stop %d waiting on: %@", i + 1,
115 missing.joined(separator: " "))
116 }
117 }
118 print(String(format: "coverage at playhead: %@ (ledger %.2f GB)",
119 covered ? "OK" : "FAIL (not covered in 150s)",
120 Double(pipeline.ledgerBytes) / 1e9))
121 if !covered { failures += 1 }
122 }
123
124 // Let any in-flight builds settle, then final ground-truth comparison.
125 spin(5)
126 let disk = MediaPipeline.directorySize(pipeline.cacheRoot)
127 print(String(format: "\nfinal: ledger %.2f GB, disk %.2f GB, cap %.1f GB",
128 Double(pipeline.ledgerBytes) / 1e9, Double(disk) / 1e9,
129 Double(cap) / 1e9))
130 if disk > cap + 500_000_000 { failures += 1; print("FAIL: final disk size over cap") }
131 print(failures == 0 ? "\n== cachetest PASS ==" : "\n== cachetest FAIL (\(failures)) ==")
132}
sequencer/Sources/Sequencer/ChunkedProxy.swift+874-110
...@@ -36,11 +36,18 @@ final class ChunkManager {...@@ -36,11 +36,18 @@ final class ChunkManager {
36 /// fixed 960 until the viewer measures itself.36 /// fixed 960 until the viewer measures itself.
37 private(set) var previewTargetWidth = 96037 private(set) var previewTargetWidth = 960
3838
39 /// Hard upper bound on proxy resolution: 1080p-wide. A proxy only has to be
40 /// sharp enough to edit against, not master from — and a 4K source is ~8×
41 /// the pixels (and cache bytes) of 1080p for detail an editing preview can't
42 /// use. (It also caps content that was cheaply up-scaled to 4K — e.g. 800×600
43 /// gameplay blown up to 4K — back to a size that reflects its real detail.)
44 static let maxProxyWidth = 1920
45
39 /// The width a fresh proxy for this media should reach — the preview target,46 /// The width a fresh proxy for this media should reach — the preview target,
40 /// never upscaled past the source.47 /// never upscaled past the source, and never above the 1080p cap.
41 private func targetWidth(for media: MediaItem) -> Int {48 private func targetWidth(for media: MediaItem) -> Int {
42 let native = media.width > 0 ? media.width : previewTargetWidth49 let native = media.width > 0 ? media.width : previewTargetWidth
43 return min(native, previewTargetWidth)50 return min(native, previewTargetWidth, Self.maxProxyWidth)
44 }51 }
4552
46 /// Effective encode width at `level` for this media (even, ffmpeg-friendly).53 /// Effective encode width at `level` for this media (even, ffmpeg-friendly).
...@@ -50,9 +57,28 @@ final class ChunkManager {...@@ -50,9 +57,28 @@ final class ChunkManager {
50 return max(160, w - (w % 2))57 return max(160, w - (w % 2))
51 }58 }
5259
60 /// Whether a chunk already on disk at width `built` should be re-encoded:
61 /// either it's below the current sharpness target (sharpen up toward it), or
62 /// it's above the hard 1080p cap (a legacy 4K proxy to shrink back down — the
63 /// cap is constant, so this converges and never churns on window resize). The
64 /// `attempted` guard stops a failed re-encode from looping.
65 private func needsReencode(built: Int, attempted: Int?, target: Int) -> Bool {
66 if built < target { return (attempted ?? 0) < target }
67 if built > Self.maxProxyWidth { return (attempted ?? built) > Self.maxProxyWidth }
68 return false
69 }
70
71 /// A short "rescue" slice standing in for a chunk the playhead landed on
72 /// cold: only `dur` seconds starting `offset` into the chunk's grid slot
73 /// exist on disk (as `rNNNNNN.mov`). Session-only — never persisted; stale
74 /// slice files from a crash are purged by the launch reconcile walk.
75 struct Rescue { let offset: Double; let dur: Double; let width: Int }
76
53 private struct MediaState {77 private struct MediaState {
54 var built: [Int: Int] = [:] // chunk index → effective width on disk78 var built: [Int: Int] = [:] // chunk index → effective width on disk
55 var attempted: [Int: Int] = [:] // chunk index → width of the last attempt79 var attempted: [Int: Int] = [:] // chunk index → width of the last attempt
80 var partial: [Int: Rescue] = [:] // chunk index → rescue slice on disk
81 var rescueAttempted: Set<Int> = []
56 var inFlight: Set<Int> = []82 var inFlight: Set<Int> = []
57 var failed: Set<Int> = []83 var failed: Set<Int> = []
58 var urgent: [Int] = []84 var urgent: [Int] = []
...@@ -79,6 +105,18 @@ final class ChunkManager {...@@ -79,6 +105,18 @@ final class ChunkManager {
79 /// queue is retained, resuming where it left off. Main-thread only.105 /// queue is retained, resuming where it left off. Main-thread only.
80 private(set) var isPaused = false106 private(set) var isPaused = false
81107
108 /// Set when the owning document closes. In-flight builds run off-main and
109 /// their completions land back on main; a completion (via `pump`) reaches
110 /// `ctx`, which is `unowned` and may already be gone once the document is
111 /// torn down. `stopped` makes every ctx-touching entry point a no-op, so a
112 /// build finishing after close can't trap on a dangling context.
113 private var stopped = false
114 func stop() {
115 stopped = true
116 geometryPending?.cancel()
117 geometryPending = nil
118 }
119
82 /// Toggle proxy optimization on/off (driven by the status-bar readout).120 /// Toggle proxy optimization on/off (driven by the status-bar readout).
83 func setPaused(_ paused: Bool) {121 func setPaused(_ paused: Bool) {
84 guard paused != isPaused else { return }122 guard paused != isPaused else { return }
...@@ -110,9 +148,7 @@ final class ChunkManager {...@@ -110,9 +148,7 @@ final class ChunkManager {
110 init() {148 init() {
111 NotificationCenter.default.addObserver(149 NotificationCenter.default.addObserver(
112 forName: .projectChanged, object: nil, queue: .main) { [weak self] _ in150 forName: .projectChanged, object: nil, queue: .main) { [weak self] _ in
113 guard let self else { return }151 self?.scheduleRebuild()
114 self.ensure(for: self.ctx.store.project)
115 self.updateDemand(force: true)
116 }152 }
117 // Hiding/focusing a track changes what to optimize first.153 // Hiding/focusing a track changes what to optimize first.
118 NotificationCenter.default.addObserver(154 NotificationCenter.default.addObserver(
...@@ -121,6 +157,26 @@ final class ChunkManager {...@@ -121,6 +157,26 @@ final class ChunkManager {
121 }157 }
122 }158 }
123159
160 private var geometryPending: DispatchWorkItem?
161 /// Coalesce project-geometry rebuilds. During a continuous drag
162 /// `.projectChanged` fires ~60×/s; rebuilding the whole background queue each
163 /// time is pure waste — moving a clip in TIME doesn't change which source
164 /// chunks it needs. Debounce so the queue rebuilds once the edit settles.
165 /// (Initial load calls `ensure` directly via `startServices`, and playback
166 /// refreshes demand every tick, so nothing waits on this.)
167 private func scheduleRebuild() {
168 guard !stopped else { return }
169 geometryPending?.cancel()
170 let w = DispatchWorkItem { [weak self] in
171 guard let self, !self.stopped else { return }
172 self.geometryPending = nil
173 self.ensure(for: self.ctx.store.project)
174 self.updateDemand(force: true)
175 }
176 geometryPending = w
177 DispatchQueue.main.asyncAfter(deadline: .now() + 0.12, execute: w)
178 }
179
124 // MARK: - Paths180 // MARK: - Paths
125181
126 private func chunksDir(_ key: String) -> URL {182 private func chunksDir(_ key: String) -> URL {
...@@ -135,6 +191,9 @@ final class ChunkManager {...@@ -135,6 +191,9 @@ final class ChunkManager {
135 private func chunkURL(key: String, index: Int) -> URL {191 private func chunkURL(key: String, index: Int) -> URL {
136 chunksDir(key).appendingPathComponent(String(format: "c%06d.mov", index))192 chunksDir(key).appendingPathComponent(String(format: "c%06d.mov", index))
137 }193 }
194 private func rescueURL(key: String, index: Int) -> URL {
195 chunksDir(key).appendingPathComponent(String(format: "r%06d.mov", index))
196 }
138 private func manifestURL(_ key: String) -> URL {197 private func manifestURL(_ key: String) -> URL {
139 chunksDir(key).appendingPathComponent("widths.json")198 chunksDir(key).appendingPathComponent("widths.json")
140 }199 }
...@@ -172,6 +231,15 @@ final class ChunkManager {...@@ -172,6 +231,15 @@ final class ChunkManager {
172 var s = states[media.cacheKey] ?? MediaState()231 var s = states[media.cacheKey] ?? MediaState()
173 if !s.scanned {232 if !s.scanned {
174 s.scanned = true233 s.scanned = true
234 // Cold big network media starts the realtime ladder two rungs
235 // down: the first urgent (playhead) build must land in seconds,
236 // and a full-target encode of 4K source rarely does — the
237 // adaptive controller would only learn that AFTER the user
238 // stared at a placeholder. It climbs back once builds measure
239 // comfortably fast; background upgrades restore full quality.
240 if media.width >= 2560, Self.isNetworkPath(media.path) {
241 s.qualityIndex = 2
242 }
175 let widths = loadWidths(media.cacheKey)243 let widths = loadWidths(media.cacheKey)
176 if let names = try? FileManager.default244 if let names = try? FileManager.default
177 .contentsOfDirectory(atPath: chunksDir(media.cacheKey).path) {245 .contentsOfDirectory(atPath: chunksDir(media.cacheKey).path) {
...@@ -205,28 +273,113 @@ final class ChunkManager {...@@ -205,28 +273,113 @@ final class ChunkManager {
205 guard s.urgent != wanted else { return }273 guard s.urgent != wanted else { return }
206 s.urgent = wanted274 s.urgent = wanted
207 states[media.cacheKey] = s275 states[media.cacheKey] = s
276 windowStarved = false // fresh urgency → re-attempt admission
277 pump()
278 }
279
280 /// A live trim/slip drag is exposing this source moment at a clip edge —
281 /// start building its chunk (and the neighbor in the drag direction)
282 /// BEFORE mouse-up, so a newly extended range is covered by the time the
283 /// user plays across it. Cheap and self-deduping; safe to call per drag
284 /// tick.
285 func noteGestureExposure(media: MediaItem, sourceTime: Double, direction: Int) {
286 guard !stopped, media.duration > 0, !media.isAudio, !hasFullProxy(media) else { return }
287 var s = state(for: media)
288 let n = Self.chunkCount(duration: media.duration)
289 let i = min(n - 1, max(0, Self.chunkIndex(forSource: sourceTime)))
290 let wanted = [i, i + (direction < 0 ? -1 : 1)].filter {
291 $0 >= 0 && $0 < n && s.built[$0] == nil
292 && !s.inFlight.contains($0) && !s.failed.contains($0)
293 }
294 guard s.urgent != wanted else { return }
295 s.urgent = wanted
296 states[media.cacheKey] = s
297 windowStarved = false // fresh urgency → re-attempt admission
208 pump()298 pump()
209 }299 }
210300
211 /// Rebuild the background fill queue from the project: every chunk in301 /// Rebuild the background fill queue from the project: every chunk in
212 /// every clip's used source range, in order.302 /// every clip's used source range. Cut heads first (the first chunk of
303 /// every clip is the landing pad for clip-to-clip navigation — a sliver
304 /// of the bytes for most of the "timeline feels instant" effect), then
305 /// everything else; both passes nearest-the-playhead first, so the fill
306 /// grows the working set outward instead of marching from t=0.
213 func ensure(for project: ProjectModel) {307 func ensure(for project: ProjectModel) {
308 guard !stopped else { return }
309 let ph = ctx.playback.playhead
214 for media in project.media {310 for media in project.media {
215 guard media.duration > 0, !media.isAudio, !hasFullProxy(media) else { continue }311 guard media.duration > 0, !media.isAudio, !hasFullProxy(media) else { continue }
216 var s = state(for: media)312 var s = state(for: media)
217 let n = Self.chunkCount(duration: media.duration)313 let n = Self.chunkCount(duration: media.duration)
218 var order: [Int] = []314 var heads: [(i: Int, d: Double)] = []
315 var rest: [(i: Int, d: Double)] = []
316 var seen = Set<Int>()
219 for clip in project.clips where clip.mediaId == media.id {317 for clip in project.clips where clip.mediaId == media.id {
220 let a = min(n - 1, Self.chunkIndex(forSource: clip.srcIn))318 let a = min(n - 1, Self.chunkIndex(forSource: clip.srcIn))
221 let b = min(n - 1, Self.chunkIndex(forSource: clip.srcIn + clip.duration - 0.001))319 let b = min(n - 1, Self.chunkIndex(forSource: clip.srcIn + clip.duration - 0.001))
222 for i in a...max(a, b) where !order.contains(i) { order.append(i) }320 let d = abs(clip.start - ph)
321 for i in a...max(a, b) where seen.insert(i).inserted {
322 if i == a { heads.append((i, d)) } else { rest.append((i, d)) }
323 }
223 }324 }
224 s.background = order325 heads.sort { $0.d < $1.d }
326 rest.sort { $0.d < $1.d }
327 s.background = heads.map(\.i) + rest.map(\.i)
225 states[media.cacheKey] = s328 states[media.cacheKey] = s
226 }329 }
330 // Geometry changed: clips removed from the timeline may have freed
331 // evictable chunks, so a starved fill is worth one more attempt.
332 fillStarved = false
333 // The scan above is also what makes this document's chunks visible as
334 // eviction candidates. If the cache is over cap (launch reconcile ran
335 // before any document had scanned — nothing was evictable then), this
336 // is the moment eviction can actually see the cold chunks: re-check.
337 MediaPipeline.shared.evictIfNeeded()
227 pump()338 pump()
228 }339 }
229340
341 // MARK: - Edit locus
342
343 /// Timeline positions of recent edits, oldest first. Editors scrub and
344 /// re-play around where they're cutting, so chunks near these positions
345 /// build early and evict late. Fed by `Store` after each committed edit;
346 /// entries expire after ~15 minutes.
347 private var editLoci: [(time: Double, at: Date)] = []
348
349 func noteEdits(times: [Double]) {
350 guard !stopped, !times.isEmpty else { return }
351 let now = Date()
352 for t in times {
353 if let i = editLoci.firstIndex(where: { abs($0.time - t) < 30 }) {
354 editLoci[i] = (t, now)
355 } else {
356 editLoci.append((t, now))
357 }
358 }
359 if editLoci.count > 8 { editLoci.removeFirst(editLoci.count - 8) }
360 updateDemand(force: true)
361 }
362
363 /// Loci still fresh enough to matter.
364 private func activeEditLoci() -> [Double] {
365 let cutoff = Date().addingTimeInterval(-15 * 60)
366 editLoci.removeAll { $0.at < cutoff }
367 return editLoci.map(\.time)
368 }
369
370 /// The timeline window the user can currently SEE (when zoomed in enough
371 /// to be meaningful) — scrubbing happens inside it. Set by TimelineView.
372 private func visibleWindow() -> ClosedRange<Double>? {
373 guard let r = ctx.session.visibleTimeRange,
374 r.upperBound - r.lowerBound <= 600 else { return nil }
375 return r
376 }
377
378 /// Distance from a timeline interval to a point (0 when inside).
379 private static func dist(_ t: Double, _ lo: Double, _ hi: Double) -> Double {
380 t < lo ? lo - t : (t > hi ? t - hi : 0)
381 }
382
230 // MARK: - Prefetch demand (what to optimize first)383 // MARK: - Prefetch demand (what to optimize first)
231384
232 private struct DemandKey: Hashable { let key: String; let index: Int }385 private struct DemandKey: Hashable { let key: String; let index: Int }
...@@ -237,6 +390,9 @@ final class ChunkManager {...@@ -237,6 +390,9 @@ final class ChunkManager {
237 /// build at the adaptive realtime quality so they land in time; everything390 /// build at the adaptive realtime quality so they land in time; everything
238 /// else builds at the full preview-quality target.391 /// else builds at the full preview-quality target.
239 private var demandImminent: Set<DemandKey> = []392 private var demandImminent: Set<DemandKey> = []
393 /// Uncovered chunks the playhead is sitting INSIDE right now, mapped to the
394 /// source time being shown — the trigger (and anchor) for rescue slices.
395 private var demandRescue: [DemandKey: Double] = [:]
240 private var lastDemandPlayhead = -1e9396 private var lastDemandPlayhead = -1e9
241 private var lastDemandSign = 0.0397 private var lastDemandSign = 0.0
242398
...@@ -249,14 +405,16 @@ final class ChunkManager {...@@ -249,14 +405,16 @@ final class ChunkManager {
249 private static let imminentAhead = 45.0405 private static let imminentAhead = 45.0
250406
251 /// Recompute the build order — the heart of "optimize the right thing first."407 /// Recompute the build order — the heart of "optimize the right thing first."
252 /// For every video clip near the playhead we score the proxy chunks its408 /// For every video clip near an ANCHOR we score the proxy chunks its
253 /// source range needs and sort them: coverage before sharpening, visible409 /// source range needs and sort them: coverage before sharpening, visible
254 /// (and focused) tracks before hidden, and nearer the playhead in the410 /// (and focused) tracks before hidden, and nearer the anchor before
255 /// playback direction before farther. Chunks outside the window fall through411 /// farther. Anchors, hottest first: the playhead (direction-weighted),
256 /// to the whole-project background queue (`ensure`). Cheap; safe to call as412 /// recent edit sites, and the visible timeline window — the places the
257 /// the playhead moves (self-throttled).413 /// user is most likely to play next. Chunks outside every window fall
414 /// through to the whole-project background queue (`ensure`). Cheap; safe
415 /// to call as the playhead moves (self-throttled).
258 func updateDemand(force: Bool = false) {416 func updateDemand(force: Bool = false) {
259 guard ctx != nil else { return }417 guard !stopped else { return }
260 let ph = ctx.playback.playhead418 let ph = ctx.playback.playhead
261 let sign = ctx.playback.rate < 0 ? -1.0 : 1.0419 let sign = ctx.playback.rate < 0 ? -1.0 : 1.0
262 guard force || abs(ph - lastDemandPlayhead) > 1.5 || sign != lastDemandSign else { return }420 guard force || abs(ph - lastDemandPlayhead) > 1.5 || sign != lastDemandSign else { return }
...@@ -267,48 +425,97 @@ final class ChunkManager {...@@ -267,48 +425,97 @@ final class ChunkManager {
267 let dir = sign425 let dir = sign
268 let anyFocused = !ctx.session.focusedTracks.isEmpty426 let anyFocused = !ctx.session.focusedTracks.isEmpty
269427
428 // Secondary anchors: recent edit sites, then the visible window.
429 // Their bias keeps them strictly behind playhead-window work but far
430 // ahead of the whole-project background fill.
431 struct Window { let lo: Double; let hi: Double; let anchor: Double; let bias: Double }
432 var windows = [Window(lo: ph - (dir > 0 ? Self.prefetchBehind : Self.prefetchAhead),
433 hi: ph + (dir > 0 ? Self.prefetchAhead : Self.prefetchBehind),
434 anchor: ph, bias: 0)]
435 for l in activeEditLoci() {
436 windows.append(Window(lo: l - 45, hi: l + 45, anchor: l, bias: 2_000))
437 }
438 if let vis = visibleWindow() {
439 windows.append(Window(lo: vis.lowerBound, hi: vis.upperBound,
440 anchor: (vis.lowerBound + vis.upperBound) / 2, bias: 6_000))
441 }
442
270 struct Cand { let key: DemandKey; let score: Double; let imminent: Bool }443 struct Cand { let key: DemandKey; let score: Double; let imminent: Bool }
271 var cands: [Cand] = []444 var cands: [Cand] = []
445 var rescue: [DemandKey: Double] = [:]
272 for clip in project.clips where clip.kind == .video {446 for clip in project.clips where clip.kind == .video {
273 guard let media = project.media(clip.mediaId), media.duration > 0,447 guard let media = project.media(clip.mediaId), media.duration > 0,
274 !media.isAudio, !hasFullProxy(media) else { continue }448 !media.isAudio, !hasFullProxy(media) else { continue }
275 let visible = !ctx.session.hiddenTracks.contains(clip.track)449 let visible = !ctx.session.hiddenTracks.contains(clip.track)
276 let focused = ctx.session.focusedTracks.contains(clip.track)450 let focused = ctx.session.focusedTracks.contains(clip.track)
277 let lo = ph - (dir > 0 ? Self.prefetchBehind : Self.prefetchAhead)451 var n = 0, target = 0
278 let hi = ph + (dir > 0 ? Self.prefetchAhead : Self.prefetchBehind)452 var s: MediaState?
279 let a = max(clip.start, lo), b = min(clip.end, hi)453 for w in windows {
280 guard b > a else { continue }454 let a = max(clip.start, w.lo), b = min(clip.end, w.hi)
281 let n = Self.chunkCount(duration: media.duration)455 guard b > a else { continue }
282 let s = state(for: media)456 if s == nil { // lazy: only scan media that some window needs
283 let target = targetWidth(for: media)457 s = state(for: media)
284 let srcA = clip.srcIn + (a - clip.start) * clip.speed458 n = Self.chunkCount(duration: media.duration)
285 let srcB = clip.srcIn + (b - clip.start) * clip.speed459 target = targetWidth(for: media)
286 let ci = min(n - 1, Self.chunkIndex(forSource: min(srcA, srcB)))460 }
287 let cj = min(n - 1, Self.chunkIndex(forSource: max(srcA, srcB) - 1e-6))461 guard let st = s else { continue }
288 for idx in ci...max(ci, cj) {462 let srcA = clip.srcIn + (a - clip.start) * clip.speed
289 if (s.built[idx] ?? 0) >= target { continue } // already good enough463 let srcB = clip.srcIn + (b - clip.start) * clip.speed
290 let covered = s.built[idx] != nil464 let ci = min(n - 1, Self.chunkIndex(forSource: min(srcA, srcB)))
291 // Timeline moment this chunk's content plays inside this clip.465 let cj = min(n - 1, Self.chunkIndex(forSource: max(srcA, srcB) - 1e-6))
292 let tl = clip.start466 for idx in ci...max(ci, cj) {
293 + (Double(idx) * Self.chunkSeconds - clip.srcIn) / max(1e-4, clip.speed)467 if (st.built[idx] ?? 0) >= target { continue } // already good enough
294 let signed = (min(max(tl, a), b) - ph) * dir // >0 = ahead468 let covered = st.built[idx] != nil
295 var score = signed >= 0 ? signed : -signed * 4 // behind costs 4×469 // Timeline interval this chunk's content plays inside this
296 if covered { score += 10_000 } // coverage beats sharpening470 // clip. Scoring by the INTERVAL (not the chunk's start
297 if !visible { score += 100_000 } // hidden tracks last471 // moment) is what puts the chunk UNDER the playhead at
298 else if anyFocused && !focused { score += 1_000 } // the enlarged pane first472 // score 0 — its start is always "behind", and treating it
299 let imminent = visible && !covered && signed >= 0 && signed < Self.imminentAhead473 // that way made the builder prefetch a dozen ahead-chunks
300 cands.append(Cand(key: DemandKey(key: media.cacheKey, index: idx),474 // while the user stared at "Loading Media…" on the frame
301 score: score, imminent: imminent))475 // they'd actually landed on.
476 let t0 = clip.start
477 + (Double(idx) * Self.chunkSeconds - clip.srcIn) / max(1e-4, clip.speed)
478 let t1 = clip.start
479 + (Double(idx + 1) * Self.chunkSeconds - clip.srcIn) / max(1e-4, clip.speed)
480 let cLo = max(min(t0, t1), a), cHi = min(max(t0, t1), b)
481 let isPlayhead = w.bias == 0
482 var score: Double
483 var under = false
484 if isPlayhead {
485 let ahead = dir > 0 ? cLo - ph : ph - cHi // >0 = strictly ahead
486 let behind = dir > 0 ? ph - cHi : cLo - ph // >0 = strictly behind
487 under = ahead <= 0 && behind <= 0 // playing right now
488 score = ahead > 0 ? ahead : (behind > 0 ? behind * 4 : 0)
489 } else {
490 score = w.bias + max(0, max(cLo - w.anchor, w.anchor - cHi))
491 }
492 if covered { score += 10_000 } // coverage beats sharpening
493 if !visible { score += 100_000 } // hidden tracks last
494 else if anyFocused && !focused { score += 1_000 } // the enlarged pane first
495 let ahead = dir > 0 ? cLo - ph : ph - cHi
496 let imminent = isPlayhead && visible && !covered
497 && (under || (ahead >= 0 && ahead < Self.imminentAhead))
498 let dk = DemandKey(key: media.cacheKey, index: idx)
499 if isPlayhead, visible, under, !covered {
500 rescue[dk] = clip.srcIn + (ph - clip.start) * clip.speed
501 }
502 cands.append(Cand(key: dk, score: score, imminent: imminent))
503 }
302 }504 }
303 }505 }
304 cands.sort { $0.score < $1.score }506 cands.sort { $0.score < $1.score }
507 let oldDemand = demand
305 demand.removeAll(keepingCapacity: true)508 demand.removeAll(keepingCapacity: true)
306 demandImminent.removeAll(keepingCapacity: true)509 demandImminent.removeAll(keepingCapacity: true)
510 demandRescue = rescue
307 var seen = Set<DemandKey>()511 var seen = Set<DemandKey>()
308 for c in cands where seen.insert(c.key).inserted {512 for c in cands where seen.insert(c.key).inserted {
309 demand.append(c.key)513 demand.append(c.key)
310 if c.imminent { demandImminent.insert(c.key) }514 if c.imminent { demandImminent.insert(c.key) }
311 }515 }
516 // The window moved: what starved before may fit now (different chunks,
517 // and colder ones may have fallen out of the protected radius).
518 if demand != oldDemand { windowStarved = false }
312 pump()519 pump()
313 }520 }
314521
...@@ -318,7 +525,13 @@ final class ChunkManager {...@@ -318,7 +525,13 @@ final class ChunkManager {
318 if media.isAudio { return true } // audio plays the original directly525 if media.isAudio { return true } // audio plays the original directly
319 if hasFullProxy(media) { return true }526 if hasFullProxy(media) { return true }
320 let s = state(for: media)527 let s = state(for: media)
321 return s.built[Self.chunkIndex(forSource: sourceTime)] != nil528 let i = Self.chunkIndex(forSource: sourceTime)
529 if s.built[i] != nil { return true }
530 if let p = s.partial[i] { // a rescue slice covers only part of the slot
531 let t = sourceTime - Double(i) * Self.chunkSeconds
532 return t >= p.offset - 0.05 && t <= p.offset + p.dur - 0.05
533 }
534 return false
322 }535 }
323536
324 /// Last known answer; unknown kicks the async composition build (which537 /// Last known answer; unknown kicks the async composition build (which
...@@ -330,6 +543,196 @@ final class ChunkManager {...@@ -330,6 +543,196 @@ final class ChunkManager {
330 return false543 return false
331 }544 }
332545
546 // MARK: - Cache budget (admission + eviction support)
547
548 /// Set when a build was skipped because the cache is at its cap and
549 /// nothing colder could be evicted to make room. `window` covers the
550 /// playhead prefetch; `fill` the whole-project background queue. Cleared
551 /// whenever the budget or the demand changes.
552 private var windowStarved = false
553 private var fillStarved = false
554 /// The cache is full and optimization is deliberately not building
555 /// everything — drives the status-bar messaging.
556 var budgetStarved: Bool { windowStarved || fillStarved }
557
558 /// The global cache budget moved (reconcile finished, eviction freed
559 /// space, cap changed): try again from a clean slate.
560 func budgetChanged() {
561 guard !stopped else { return }
562 windowStarved = false
563 fillStarved = false
564 pump()
565 }
566
567 /// `SEQ_BUILDLOG=1`: log every build START and admission failure — the
568 /// queue's decisions, not just its results. For chasing "why isn't chunk
569 /// X building" (success completions are otherwise silent).
570 static let buildLog = ProcessInfo.processInfo.environment["SEQ_BUILDLOG"] != nil
571
572 /// Global correction factor: measured chunk bytes ÷ raw estimate, EMA'd.
573 /// Starts at 1 (the raw model is a ProRes-proxy ballpark) and converges on
574 /// the actual footage within a few chunks.
575 private static var chunkRateEMA = 1.0
576
577 /// Ballpark bytes for a chunk at `width` before correction: ProRes proxy
578 /// ≈ 0.09 bytes per pixel per frame, plus PCM audio when present.
579 /// `seconds` overrides the encoded duration (rescue slices).
580 private func rawChunkEstimate(media: MediaItem, width: Int, index: Int,
581 seconds: Double? = nil) -> Double {
582 let dur = seconds ?? min(Self.chunkSeconds,
583 max(1, media.duration - Double(index) * Self.chunkSeconds))
584 let aspect = (media.width > 0 && media.height > 0)
585 ? Double(media.height) / Double(media.width) : 9.0 / 16
586 let fps = min(60.0, max(10.0, media.fps))
587 var b = 0.09 * Double(width) * (Double(width) * aspect) * fps * dur
588 if media.hasAudio { b += 200_000 * dur }
589 return b
590 }
591
592 /// Corrected + safety-margined estimate the admission gate reserves.
593 private func estimateChunkBytes(media: MediaItem, width: Int, index: Int,
594 seconds: Double? = nil) -> Int64 {
595 Int64(rawChunkEstimate(media: media, width: width, index: index, seconds: seconds)
596 * Self.chunkRateEMA * 1.3)
597 }
598
599 /// A cold, already-built chunk the global cache may delete to make room.
600 struct EvictionCandidate {
601 let key: String
602 let index: Int
603 let url: URL
604 /// Timeline seconds from this document's playhead to the nearest use
605 /// of the chunk; 1e12 when no clip uses it at all (media edited off
606 /// the timeline — the coldest bytes there are).
607 let coldness: Double
608 }
609
610 /// Timeline distance within which built chunks are HARD-protected: the
611 /// frames playback will hit imminently. Everything beyond is merely
612 /// ranked by distance — evictable coldest-first — so when the working set
613 /// alone exceeds the cap, it shrinks to fit instead of wedging eviction.
614 private static let hardProtectRadius = 60.0
615
616 /// Every built chunk of this document that eviction MAY delete, scored by
617 /// coldness (timeline distance from the playhead; off-timeline chunks are
618 /// coldest). Hard-excluded: the demand window, anything mid-build or
619 /// urgent, and chunks within `hardProtectRadius` of the playhead — which
620 /// for a background window is exactly its resume neighborhood.
621 func evictionCandidates() -> [EvictionCandidate] {
622 guard !stopped else { return [] }
623 let project = ctx.store.project
624 let ph = ctx.playback.playhead
625 var protected: Set<DemandKey> = Set(demand)
626 for (key, s) in states {
627 for i in s.inFlight { protected.insert(DemandKey(key: key, index: i)) }
628 for i in s.urgent { protected.insert(DemandKey(key: key, index: i)) }
629 }
630 // Score every chunk any clip uses by its distance from the nearest
631 // heat anchor (playhead, then recent edit sites and the visible
632 // window at a penalty so the playhead wins ties); hard-protect only
633 // the playhead-imminent ones. Cut heads read as 4× closer than they
634 // are, so the "instant timeline" landing pads die last.
635 let loci = activeEditLoci()
636 let vis = visibleWindow()
637 var distance: [DemandKey: Double] = [:]
638 var heads: Set<DemandKey> = []
639 for clip in project.clips where clip.kind == .video {
640 guard let media = project.media(clip.mediaId), media.duration > 0,
641 !media.isAudio else { continue }
642 let n = Self.chunkCount(duration: media.duration)
643 let a = min(n - 1, Self.chunkIndex(forSource: clip.srcIn))
644 let b = min(n - 1, Self.chunkIndex(forSource: clip.srcIn + clip.duration - 0.001))
645 heads.insert(DemandKey(key: media.cacheKey, index: a))
646 for i in a...max(a, b) {
647 let t0 = clip.start + (Double(i) * Self.chunkSeconds - clip.srcIn) / max(1e-4, clip.speed)
648 let t1 = clip.start + (Double(i + 1) * Self.chunkSeconds - clip.srcIn) / max(1e-4, clip.speed)
649 let lo = max(clip.start, min(t0, t1)), hi = min(clip.end, max(t0, t1))
650 var d = Self.dist(ph, lo, hi)
651 for l in loci { d = min(d, Self.dist(l, lo, hi) + 60) }
652 if let vis { // gap between the chunk's interval and the visible range
653 let gap = max(0, max(lo - vis.upperBound, vis.lowerBound - hi))
654 d = min(d, gap + 120)
655 }
656 let dk = DemandKey(key: media.cacheKey, index: i)
657 distance[dk] = min(distance[dk] ?? .infinity, d)
658 if Self.dist(ph, lo, hi) < Self.hardProtectRadius { protected.insert(dk) }
659 }
660 }
661 var out: [EvictionCandidate] = []
662 var builtTotal = 0
663 for (key, s) in states {
664 builtTotal += s.built.count
665 for i in s.built.keys {
666 let dk = DemandKey(key: key, index: i)
667 guard !protected.contains(dk), !s.inFlight.contains(i) else { continue }
668 var coldness = distance[dk] ?? 1e12
669 if heads.contains(dk) { coldness *= 0.25 } // landing pads die last
670 out.append(EvictionCandidate(key: key, index: i,
671 url: chunkURL(key: key, index: i),
672 coldness: coldness))
673 }
674 // Rescue slices are evictable like any chunk (the playhead radius
675 // protects the live one); they just live in an rNNNNNN.mov file.
676 for i in s.partial.keys where s.built[i] == nil {
677 let dk = DemandKey(key: key, index: i)
678 guard !protected.contains(dk), !s.inFlight.contains(i) else { continue }
679 out.append(EvictionCandidate(key: key, index: i,
680 url: rescueURL(key: key, index: i),
681 coldness: distance[dk] ?? 1e12))
682 }
683 }
684 NSLog("[cache] candidates: %d of %d built chunks (%d protected, playhead %.0fs)",
685 out.count, builtTotal, protected.count, ph)
686 return out
687 }
688
689 /// The global cache deleted these chunk files: drop them from state and
690 /// bump the version so compositions rebuild onto the original-file
691 /// fallback instead of pointing at deleted movs.
692 func noteEvicted(key: String, indices: [Int]) {
693 guard var s = states[key] else { return }
694 var changed = false
695 for i in indices {
696 if s.built.removeValue(forKey: i) != nil {
697 s.attempted.removeValue(forKey: i)
698 changed = true
699 }
700 if s.partial.removeValue(forKey: i) != nil { changed = true }
701 }
702 guard changed else { return }
703 s.version += 1
704 states[key] = s
705 persistWidths(key: key)
706 }
707
708 /// Chunk states for the timeline's optimization strip — one cheap value
709 /// snapshot per media per strip rebuild. nil when the media hasn't been
710 /// scanned yet (unknown; the strip paints it as unoptimized until the
711 /// initial `ensure` pass scans it).
712 struct StripSnapshot {
713 let n: Int
714 let target: Int
715 let built: [Int: Int]
716 let inFlight: Set<Int>
717 let partial: Set<Int>
718 let failed: Set<Int>
719 let fullProxy: Bool
720 }
721
722 func stripSnapshot(media: MediaItem) -> StripSnapshot? {
723 guard media.duration > 0, !media.isAudio else { return nil }
724 if hasFullProxy(media) {
725 return StripSnapshot(n: 1, target: 0, built: [:], inFlight: [],
726 partial: [], failed: [], fullProxy: true)
727 }
728 guard let s = states[media.cacheKey], s.scanned else { return nil }
729 return StripSnapshot(n: Self.chunkCount(duration: media.duration),
730 target: targetWidth(for: media),
731 built: s.built, inFlight: s.inFlight,
732 partial: Set(s.partial.keys), failed: s.failed,
733 fullProxy: false)
734 }
735
333 /// (building now, waiting in queue) across all media — for the status bar.736 /// (building now, waiting in queue) across all media — for the status bar.
334 /// A chunk counts as queued while it's either missing OR still below the737 /// A chunk counts as queued while it's either missing OR still below the
335 /// preview-quality target (i.e. an upgrade is pending).738 /// preview-quality target (i.e. an upgrade is pending).
...@@ -348,8 +751,14 @@ final class ChunkManager {...@@ -348,8 +751,14 @@ final class ChunkManager {
348751
349 /// Per-chunk proxy width right now (players record this at item-swap time752 /// Per-chunk proxy width right now (players record this at item-swap time
350 /// to judge whether a later swap upgrades the frame under the playhead).753 /// to judge whether a later swap upgrades the frame under the playhead).
754 /// Rescue slices report as width 1: "something is there", and any full
755 /// build over them reads as a strict upgrade so the player adopts it.
351 func builtWidths(media: MediaItem) -> [Int: Int] {756 func builtWidths(media: MediaItem) -> [Int: Int] {
352 state(for: media).built757 let s = state(for: media)
758 guard !s.partial.isEmpty else { return s.built }
759 var w = s.built
760 for i in s.partial.keys where w[i] == nil { w[i] = 1 }
761 return w
353 }762 }
354763
355 func builtChunkURL(media: MediaItem, index: Int) -> URL? {764 func builtChunkURL(media: MediaItem, index: Int) -> URL? {
...@@ -357,53 +766,181 @@ final class ChunkManager {...@@ -357,53 +766,181 @@ final class ChunkManager {
357 ? chunkURL(key: media.cacheKey, index: index) : nil766 ? chunkURL(key: media.cacheKey, index: index) : nil
358 }767 }
359768
769 /// A playable (file, time-in-file) pair that shows `sourceTime` — the
770 /// sharpest thing on disk right now: legacy full proxy, built chunk,
771 /// covering rescue slice, or the original when it's known playable. nil
772 /// when this frame genuinely can't be decoded yet. Feeds the RAM frame
773 /// cache's stand-in decodes; main-thread.
774 func frameSource(media: MediaItem, sourceTime: Double) -> (url: URL, time: Double)? {
775 guard !media.isAudio else { return nil }
776 if let proxy = MediaPipeline.shared.proxyURL(for: media) {
777 return (proxy, sourceTime)
778 }
779 let s = state(for: media)
780 let i = Self.chunkIndex(forSource: sourceTime)
781 let local = sourceTime - Double(i) * Self.chunkSeconds
782 if s.built[i] != nil {
783 return (chunkURL(key: media.cacheKey, index: i), local)
784 }
785 if let p = s.partial[i], local >= p.offset, local <= p.offset + p.dur - 0.05 {
786 return (rescueURL(key: media.cacheKey, index: i), local - p.offset)
787 }
788 if s.originalPlayable == true { return (media.url, sourceTime) }
789 return nil
790 }
791
792 /// Rough bytes this project needs to be FULLY optimized — every chunk any
793 /// clip uses, at the current preview target width — and how many bytes its
794 /// media already have on disk. Estimates use the same corrected model as
795 /// the admission gate (sans safety margin); `built` is a real disk walk of
796 /// each media's chunk dir, so call this on demand (Settings), not per frame.
797 func optimizeEstimate(for project: ProjectModel) -> (total: Int64, built: Int64) {
798 var total: Int64 = 0, built: Int64 = 0
799 for media in project.media where !media.isAudio && media.duration > 0 {
800 if hasFullProxy(media), let proxy = MediaPipeline.shared.proxyURL(for: media) {
801 let sz = (try? FileManager.default.attributesOfItem(atPath: proxy.path)[.size]
802 as? NSNumber)?.int64Value ?? 0
803 total += sz
804 built += sz
805 continue
806 }
807 let n = Self.chunkCount(duration: media.duration)
808 var used = Set<Int>()
809 for clip in project.clips where clip.mediaId == media.id && clip.kind == .video {
810 let a = min(n - 1, Self.chunkIndex(forSource: clip.srcIn))
811 let b = min(n - 1, Self.chunkIndex(forSource: clip.srcIn + clip.duration - 0.001))
812 for i in a...max(a, b) { used.insert(i) }
813 }
814 guard !used.isEmpty else { continue }
815 let target = targetWidth(for: media)
816 for i in used {
817 total += Int64(rawChunkEstimate(media: media, width: target, index: i)
818 * Self.chunkRateEMA)
819 }
820 built += MediaPipeline.directorySize(chunksDir(media.cacheKey))
821 }
822 return (total, built)
823 }
824
825 /// One-line explanation of why the frame at `sourceTime` might not be
826 /// showing — the [miss] log's payload. Every "Loading Media…" spinner the
827 /// viewer escalates to gets one of these, so a spinner sighting is
828 /// diagnosable from the log instead of a shrug: was the chunk missing
829 /// entirely (and did the demand scorer even know about it?), mid-build,
830 /// built-but-not-yet-stitched, or built with the player just late?
831 func missDiagnosis(media: MediaItem, sourceTime: Double) -> String {
832 guard !media.isAudio else { return "audio" }
833 guard !hasFullProxy(media) else { return "full-proxy player-late" }
834 let s = state(for: media)
835 let i = Self.chunkIndex(forSource: sourceTime)
836 var bits = ["\(media.cacheKey.prefix(8))#\(i)"]
837 if let w = s.built[i] {
838 bits.append("built(w=\(w))")
839 bits.append(s.compositionVersion != s.version ? "composition-lag"
840 : "player-late")
841 } else if let p = s.partial[i] {
842 let t = sourceTime - Double(i) * Self.chunkSeconds
843 bits.append(String(format: "rescue(%.0fs@%.0fs t=%.1f)", p.dur, p.offset, t))
844 bits.append(s.compositionVersion != s.version ? "composition-lag"
845 : "player-late")
846 } else if s.inFlight.contains(i) {
847 bits.append("building")
848 } else if s.failed.contains(i) {
849 bits.append("build-failed")
850 } else {
851 let dk = DemandKey(key: media.cacheKey, index: i)
852 if let rank = demand.firstIndex(of: dk) {
853 bits.append("queued(demand#\(rank)\(demandImminent.contains(dk) ? " imminent" : ""))")
854 } else if s.background.contains(i) {
855 bits.append("queued(background)") // demand window missed it
856 } else {
857 bits.append("NOT-QUEUED") // heuristic gap — the bad one
858 }
859 }
860 if budgetStarved { bits.append("budget-starved") }
861 if isPaused { bits.append("opt-paused") }
862 if s.originalPlayable == nil { bits.append("orig-unknown") }
863 else if s.originalPlayable == false { bits.append("orig-unplayable") }
864 return bits.joined(separator: " ")
865 }
866
867 /// The proxy chunk covering `sourceTime` tried to build and hard-failed
868 /// (both encoders) with nothing usable — the viewer surfaces this instead of
869 /// spinning "processing…" forever.
870 func buildFailed(media: MediaItem, sourceTime: Double) -> Bool {
871 if media.isAudio || hasFullProxy(media) { return false }
872 return state(for: media).failed.contains(Self.chunkIndex(forSource: sourceTime))
873 }
874
360 // MARK: - Build queue875 // MARK: - Build queue
361876
877 /// Which budget tier a job builds for. `window` jobs (playhead prefetch,
878 /// urgent coverage) may evict cold chunks of open documents to make room;
879 /// `fill` jobs (whole-project background) may only consume free budget or
880 /// space freed from closed projects — never evict another chunk. That
881 /// asymmetry is what makes the cache converge instead of thrash: a build
882 /// can only displace bytes strictly colder than itself.
883 private enum JobTier { case window, fill }
884
362 /// Next chunk to build, in priority order:885 /// Next chunk to build, in priority order:
363 /// 0. urgent — coverage the playhead needs NOW, at any quality;886 /// 0. urgent — coverage the playhead needs NOW, at any quality;
364 /// 1. missing — background chunks not yet built at all (coverage first);887 /// 1. missing — background chunks not yet built at all (coverage first);
365 /// 2. upgrade — background chunks built below the preview-quality target.888 /// 2. upgrade — background chunks built below the preview-quality target.
366 /// Coverage always beats sharpening, so playback never stalls waiting on a889 /// Coverage always beats sharpening, so playback never stalls waiting on a
367 /// quality upgrade of a frame that's already visible.890 /// quality upgrade of a frame that's already visible.
368 private func nextJob() -> (media: MediaItem, index: Int, urgent: Bool)? {891 private func nextJob(frontDoc: Bool) -> (media: MediaItem, index: Int, urgent: Bool, tier: JobTier)? {
892 // Only the frontmost document builds proxies. macOS state restoration
893 // reopens every previously-open project on launch; if each one built its
894 // proxies, they'd transcode their full ProRes sets in parallel. Every open
895 // project's media counts as "in use", so eviction can't reclaim any of it —
896 // the shared cache blows past its cap and fills the disk (the reported bug).
897 // A background project builds nothing until you switch to it (its window
898 // becoming main re-pumps it — see windowDidBecomeMain); the document under
899 // the playhead is always the front one, so playback is unaffected.
900 guard frontDoc else { return nil }
369 // 1. Prefetch demand — already ordered best-first (visible/focused, near,901 // 1. Prefetch demand — already ordered best-first (visible/focused, near,
370 // coverage before sharpening). Coverage rides the adaptive realtime902 // coverage before sharpening). Coverage rides the adaptive realtime
371 // quality when imminent; an upgrade goes for the full target.903 // quality when imminent; an upgrade goes for the full target.
372 for dk in demand {904 if !windowStarved {
373 guard let media = mediaByKey[dk.key] else { continue }905 for dk in demand {
374 let s = states[dk.key] ?? state(for: media)906 guard let media = mediaByKey[dk.key] else { continue }
375 guard !s.inFlight.contains(dk.index), !s.failed.contains(dk.index) else { continue }907 let s = states[dk.key] ?? state(for: media)
376 let target = targetWidth(for: media)908 guard !s.inFlight.contains(dk.index), !s.failed.contains(dk.index) else { continue }
377 if let have = s.built[dk.index] {909 let target = targetWidth(for: media)
378 if have < target, (s.attempted[dk.index] ?? 0) < target {910 if let have = s.built[dk.index] {
379 return (media, dk.index, false) // upgrade → target911 if needsReencode(built: have, attempted: s.attempted[dk.index], target: target) {
912 return (media, dk.index, false, .window) // sharpen or shrink → target
913 }
914 } else {
915 return (media, dk.index, demandImminent.contains(dk), .window) // coverage
380 }916 }
381 } else {
382 return (media, dk.index, demandImminent.contains(dk)) // coverage
383 }917 }
384 }918 // 2. Legacy urgent (headless `want`).
385 // 2. Legacy urgent (headless `want`) then the whole-project background919 for (key, s) in states {
386 // fill for chunks off-screen of the prefetch window.920 guard let media = mediaByKey[key] else { continue }
387 for (key, s) in states {921 for i in s.urgent where s.built[i] == nil
388 guard let media = mediaByKey[key] else { continue }922 && !s.inFlight.contains(i) && !s.failed.contains(i) {
389 for i in s.urgent where s.built[i] == nil923 return (media, i, true, .window)
390 && !s.inFlight.contains(i) && !s.failed.contains(i) {924 }
391 return (media, i, true)
392 }925 }
393 }926 }
927 // 3. Whole-project background fill for chunks off-screen of the
928 // prefetch window — the tier that stops when the cache is full.
929 guard !fillStarved else { return nil }
394 for (key, s) in states {930 for (key, s) in states {
395 guard let media = mediaByKey[key] else { continue }931 guard let media = mediaByKey[key] else { continue }
396 for i in s.background where s.built[i] == nil932 for i in s.background where s.built[i] == nil
397 && !s.inFlight.contains(i) && !s.failed.contains(i) {933 && !s.inFlight.contains(i) && !s.failed.contains(i) {
398 return (media, i, false)934 return (media, i, false, .fill)
399 }935 }
400 }936 }
401 for (key, s) in states {937 for (key, s) in states {
402 guard let media = mediaByKey[key] else { continue }938 guard let media = mediaByKey[key] else { continue }
403 let target = targetWidth(for: media)939 let target = targetWidth(for: media)
404 for i in s.background where !s.inFlight.contains(i) {940 for i in s.background where !s.inFlight.contains(i) {
405 if let have = s.built[i], have < target, (s.attempted[i] ?? 0) < target {941 if let have = s.built[i],
406 return (media, i, false)942 needsReencode(built: have, attempted: s.attempted[i], target: target) {
943 return (media, i, false, .fill)
407 }944 }
408 }945 }
409 }946 }
...@@ -411,40 +948,161 @@ final class ChunkManager {...@@ -411,40 +948,161 @@ final class ChunkManager {
411 }948 }
412949
413 private func pump() {950 private func pump() {
951 guard !stopped else { return } // document closing — don't touch ctx
414 // Paused stops idle background fill, but playback still optimizes the952 // Paused stops idle background fill, but playback still optimizes the
415 // chunks it's about to need.953 // chunks it's about to need.
954 // Only the front document runs its whole-project background fill (see
955 // nextJob) — this is the guard that stops N restored projects transcoding
956 // their full proxy sets in parallel and overflowing the cache.
957 let frontDoc = DocumentContext.current === ctx
416 while (!isPaused || ctx.playback.isPlaying),958 while (!isPaused || ctx.playback.isPlaying),
417 activeBuilds < maxBuilds, let job = nextJob() {959 activeBuilds < maxBuilds + 1, let job = nextJob(frontDoc: frontDoc) {
418 // During playback keep one slot free for urgent coverage so a slow960 // URGENT coverage (the playhead just landed on/near this chunk) may
419 // background quality-upgrade can't stall the frames being played.961 // take one OVERFLOW slot: a couple of minutes-long 4K background
420 if !job.urgent, ctx.playback.isPlaying, activeBuilds >= maxBuilds - 1 { break }962 // encodes must never wall off the frame the user is looking at —
421 let (media, index, urgent) = job963 // that wait was the last reproducible "Loading Media…" spinner.
964 // Everything else respects maxBuilds, and during playback keeps one
965 // slot free for urgent coverage on top.
966 if !job.urgent {
967 if activeBuilds >= maxBuilds { break }
968 if ctx.playback.isPlaying, activeBuilds >= maxBuilds - 1 { break }
969 }
970 let (media, index, urgent, tier) = job
971 // RESCUE: the playhead is sitting on this uncovered chunk right
972 // now. A full 30-second encode makes the user wait for content
973 // they're already staring at — so first land a short slice
974 // starting AT the playhead (read-bound NAS sources scale with
975 // encoded seconds, so this is fast even when quality drops
976 // aren't), then immediately re-queue the full chunk behind it.
977 var slice: (offset: Double, dur: Double)? = nil
978 let dk = DemandKey(key: media.cacheKey, index: index)
979 if urgent, let st = states[media.cacheKey], st.built[index] == nil,
980 st.partial[index] == nil, !st.rescueAttempted.contains(index),
981 let srcT = demandRescue[dk] {
982 let chunkStart = Double(index) * Self.chunkSeconds
983 let content = min(Self.chunkSeconds, media.duration - chunkStart)
984 let offset = min(max(0, (srcT - chunkStart - 1).rounded(.down)),
985 max(0, content - 2))
986 let dur = min(Self.rescueSeconds, content - offset)
987 // Only worth two encodes when the slice is a real shortcut.
988 if dur >= 4, dur <= content - offset, dur < content * 0.7 {
989 slice = (offset, dur)
990 }
991 }
422 // Urgent builds ride the adaptive realtime level; background builds992 // Urgent builds ride the adaptive realtime level; background builds
423 // go for the full preview-quality target.993 // go for the full preview-quality target. A rescue slice drops one
424 let level = urgent ? (states[media.cacheKey]?.qualityIndex ?? 0) : 0994 // more rung — landing NOW is its whole purpose.
995 var level = urgent ? (states[media.cacheKey]?.qualityIndex ?? 0) : 0
996 if slice != nil { level = min(Self.qualities.count - 1, level + 1) }
425 let width = buildWidth(level: level, media: media)997 let width = buildWidth(level: level, media: media)
426 let fpsDiv = Self.qualities[min(max(0, level), Self.qualities.count - 1)].fpsDivisor998 let fpsDiv = Self.qualities[min(max(0, level), Self.qualities.count - 1)].fpsDivisor
427 let fps = max(1, Int((media.fps / Double(fpsDiv)).rounded()))999 let fps = max(1, Int((media.fps / Double(fpsDiv)).rounded()))
1000 // Admission gate: the cap is enforced BEFORE bytes hit the disk.
1001 // No reservation, no build — first try to make room by evicting
1002 // strictly-colder bytes (closed projects for any tier; open
1003 // documents' cold chunks only for window builds), and if nothing
1004 // colder exists, this tier starves until the budget changes.
1005 let est = estimateChunkBytes(media: media, width: width, index: index,
1006 seconds: slice?.dur)
1007 let ceiling = tier == .window ? MediaPipeline.shared.maxCacheBytes
1008 : MediaPipeline.shared.lowWatermarkBytes
1009 if !MediaPipeline.shared.tryReserve(bytes: est, upTo: ceiling) {
1010 if Self.buildLog {
1011 SeqLog.log("[cache] admission blocked %@#%d est=%.0fMB tier=%@",
1012 String(media.cacheKey.prefix(8)), index,
1013 Double(est) / 1e6, tier == .window ? "window" : "fill")
1014 }
1015 // Try to make room by evicting strictly-colder bytes: closed
1016 // projects' dirs for any tier; open documents' cold chunks
1017 // only for window builds (fill must never displace a chunk —
1018 // it stops at the watermark instead, which is what keeps
1019 // fill and eviction from fighting over the same bytes).
1020 MediaPipeline.shared.evictToFit(need: est, openDocChunks: tier == .window,
1021 upTo: ceiling) { [weak self] ok in
1022 guard let self, !self.stopped, !ok else { return }
1023 // Eviction couldn't free enough (or one is already running
1024 // — resolved via budgetChanged when it lands): starve the
1025 // tier so pump stops retrying until the budget moves.
1026 if tier == .window { self.windowStarved = true }
1027 else { self.fillStarved = true }
1028 NotificationCenter.default.post(name: .mediaStatusChanged, object: nil)
1029 }
1030 break
1031 }
428 states[media.cacheKey]?.inFlight.insert(index)1032 states[media.cacheKey]?.inFlight.insert(index)
429 states[media.cacheKey]?.attempted[index] = width1033 if slice == nil {
1034 states[media.cacheKey]?.attempted[index] = width
1035 } else {
1036 states[media.cacheKey]?.rescueAttempted.insert(index)
1037 }
430 activeBuilds += 11038 activeBuilds += 1
1039 if Self.buildLog {
1040 SeqLog.log("[cache] start %@#%d w=%d urgent=%d tier=%@%@",
1041 String(media.cacheKey.prefix(8)), index, width, urgent ? 1 : 0,
1042 tier == .window ? "window" : "fill",
1043 slice != nil ? " rescue" : "")
1044 }
431 DispatchQueue.global(qos: .userInitiated).async { [self] in1045 DispatchQueue.global(qos: .userInitiated).async { [self] in
432 let r = buildChunk(media: media, index: index, width: width, fps: fps)1046 let r = buildChunk(media: media, index: index, width: width, fps: fps,
1047 slice: slice)
433 DispatchQueue.main.async {1048 DispatchQueue.main.async {
1049 if !r.ok {
1050 SeqLog.log("[cache] build FAILED %@#%d w=%d%@",
1051 String(media.cacheKey.prefix(8)), index, width,
1052 slice != nil ? " (rescue)" : "")
1053 }
1054 MediaPipeline.shared.commitBuild(
1055 reserved: est, delta: r.ok ? r.newBytes - r.oldBytes : 0)
1056 if r.ok, r.newBytes > 0, slice == nil {
1057 // Fold the measured size into the estimator (clamped so
1058 // one weird chunk can't poison admissions).
1059 let raw = self.rawChunkEstimate(media: media, width: width, index: index)
1060 if raw > 0 {
1061 let ratio = Double(r.newBytes) / raw
1062 Self.chunkRateEMA = min(10, max(0.1,
1063 Self.chunkRateEMA * 0.7 + ratio * 0.3))
1064 }
1065 }
434 self.activeBuilds -= 11066 self.activeBuilds -= 1
435 var s = self.states[media.cacheKey] ?? MediaState()1067 var s = self.states[media.cacheKey] ?? MediaState()
436 s.inFlight.remove(index)1068 s.inFlight.remove(index)
437 if r.ok {1069 if r.ok, let slice {
1070 // Slice landed: cover the playhead NOW; the chunk still
1071 // reads as unbuilt so the full encode queues right behind.
1072 s.partial[index] = Rescue(offset: slice.offset, dur: r.dur,
1073 width: width)
1074 s.failed.remove(index)
1075 s.version += 1
1076 SeqLog.log("[cache] rescue %@#%d %.0fs@%.0fs w=%d in %.1fs",
1077 String(media.cacheKey.prefix(8)), index, r.dur,
1078 slice.offset, width, r.wall)
1079 } else if r.ok {
438 s.built[index] = width1080 s.built[index] = width
439 s.failed.remove(index)1081 s.failed.remove(index)
440 s.version += 11082 s.version += 1
441 } else if s.built[index] == nil {1083 // The full chunk supersedes any rescue slice under it.
1084 if s.partial.removeValue(forKey: index) != nil {
1085 let rURL = self.rescueURL(key: media.cacheKey, index: index)
1086 DispatchQueue.global(qos: .utility).async {
1087 let sz = (try? FileManager.default.attributesOfItem(
1088 atPath: rURL.path)[.size] as? NSNumber)?.int64Value ?? 0
1089 try? FileManager.default.removeItem(at: rURL)
1090 if sz > 0 {
1091 DispatchQueue.main.async {
1092 MediaPipeline.shared.noteBytesAdded(-sz)
1093 }
1094 }
1095 }
1096 }
1097 } else if s.built[index] == nil, slice == nil {
442 // Only a hard failure when we have NOTHING; a failed1098 // Only a hard failure when we have NOTHING; a failed
443 // upgrade just keeps the existing lower-quality chunk.1099 // upgrade just keeps the existing lower-quality chunk.
1100 // (A failed rescue is not a failure — the full build
1101 // is still queued and gets its own attempt.)
444 s.failed.insert(index)1102 s.failed.insert(index)
445 }1103 }
446 self.states[media.cacheKey] = s1104 self.states[media.cacheKey] = s
447 if r.ok {1105 if r.ok, slice == nil {
448 self.persistWidths(key: media.cacheKey)1106 self.persistWidths(key: media.cacheKey)
449 // Only realtime (urgent) builds inform the realtime1107 // Only realtime (urgent) builds inform the realtime
450 // controller — a slow, quality-first background build1108 // controller — a slow, quality-first background build
...@@ -462,9 +1120,19 @@ final class ChunkManager {...@@ -462,9 +1120,19 @@ final class ChunkManager {
462 }1120 }
463 }1121 }
4641122
465 struct BuildResult { var ok: Bool; var wall: Double; var dur: Double; var isNetwork: Bool }1123 struct BuildResult {
1124 var ok: Bool; var wall: Double; var dur: Double; var isNetwork: Bool
1125 /// Bytes of the finished chunk file / of the file it replaced (an
1126 /// upgrade re-encode) — the ledger records the difference.
1127 var newBytes: Int64 = 0; var oldBytes: Int64 = 0
1128 }
1129
1130 /// Seconds of content a rescue slice encodes — enough to watch while the
1131 /// full chunk builds behind it, small enough to land in a few seconds.
1132 private static let rescueSeconds = 10.0
4661133
467 private func buildChunk(media: MediaItem, index: Int, width: Int, fps: Int) -> BuildResult {1134 private func buildChunk(media: MediaItem, index: Int, width: Int, fps: Int,
1135 slice: (offset: Double, dur: Double)? = nil) -> BuildResult {
468 let isNet = Self.isNetworkPath(media.path)1136 let isNet = Self.isNetworkPath(media.path)
469 func fail(_ wall: Double = 0, _ dur: Double = 0) -> BuildResult {1137 func fail(_ wall: Double = 0, _ dur: Double = 0) -> BuildResult {
470 BuildResult(ok: false, wall: wall, dur: dur, isNetwork: isNet)1138 BuildResult(ok: false, wall: wall, dur: dur, isNetwork: isNet)
...@@ -472,11 +1140,14 @@ final class ChunkManager {...@@ -472,11 +1140,14 @@ final class ChunkManager {
472 guard let ffmpeg = MediaPipeline.findExecutable("ffmpeg") else { return fail() }1140 guard let ffmpeg = MediaPipeline.findExecutable("ffmpeg") else { return fail() }
473 let dir = chunksDir(media.cacheKey)1141 let dir = chunksDir(media.cacheKey)
474 try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)1142 try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
475 let final = chunkURL(key: media.cacheKey, index: index)1143 let final = slice == nil ? chunkURL(key: media.cacheKey, index: index)
476 let tmp = dir.appendingPathComponent(String(format: ".c%06d.partial.mov", index))1144 : rescueURL(key: media.cacheKey, index: index)
1145 let tmp = dir.appendingPathComponent(String(
1146 format: slice == nil ? ".c%06d.partial.mov" : ".r%06d.partial.mov", index))
477 try? FileManager.default.removeItem(at: tmp)1147 try? FileManager.default.removeItem(at: tmp)
478 let start = Double(index) * Self.chunkSeconds1148 let start = Double(index) * Self.chunkSeconds + (slice?.offset ?? 0)
479 let dur = min(Self.chunkSeconds, media.duration - start)1149 var dur = min(Self.chunkSeconds - (slice?.offset ?? 0), media.duration - start)
1150 if let slice { dur = min(dur, slice.dur) }
480 guard dur > 0.01 else { return fail() }1151 guard dur > 0.01 else { return fail() }
4811152
482 func args(encoder: String) -> [String] {1153 func args(encoder: String) -> [String] {
...@@ -485,7 +1156,12 @@ final class ChunkManager {...@@ -485,7 +1156,12 @@ final class ChunkManager {
485 "-i", media.path,1156 "-i", media.path,
486 "-t", String(format: "%.3f", dur),1157 "-t", String(format: "%.3f", dur),
487 "-map", "0:v:0",1158 "-map", "0:v:0",
488 "-vf", "scale='min(\(width),iw)':-2,fps=\(fps)",1159 // Lanczos downscale: swscale's default (bicubic) softens the
1160 // hard edges of up-scaled/pixel-art content into an annoying
1161 // blur; lanczos keeps the downscaled proxy crisp (not
1162 // nearest-neighbour "pixely", just sharp) — which matters more
1163 // than resolution for editing legibility.
1164 "-vf", "scale=w='min(\(width),iw)':h=-2:flags=lanczos,fps=\(fps)",
489 "-c:v", encoder, "-profile:v", "proxy"]1165 "-c:v", encoder, "-profile:v", "proxy"]
490 if media.hasAudio { a += ["-map", "0:a:0", "-c:a", "pcm_s16le"] }1166 if media.hasAudio { a += ["-map", "0:a:0", "-c:a", "pcm_s16le"] }
491 a.append(tmp.path)1167 a.append(tmp.path)
...@@ -498,10 +1174,16 @@ final class ChunkManager {...@@ -498,10 +1174,16 @@ final class ChunkManager {
498 }1174 }
499 let wall = Date().timeIntervalSince(t0)1175 let wall = Date().timeIntervalSince(t0)
500 if res.exitCode == 0 {1176 if res.exitCode == 0 {
1177 func size(_ url: URL) -> Int64 {
1178 (try? FileManager.default.attributesOfItem(atPath: url.path)[.size]
1179 as? NSNumber)?.int64Value ?? 0
1180 }
1181 let newBytes = size(tmp), oldBytes = size(final)
501 try? FileManager.default.removeItem(at: final)1182 try? FileManager.default.removeItem(at: final)
502 do { try FileManager.default.moveItem(at: tmp, to: final) }1183 do { try FileManager.default.moveItem(at: tmp, to: final) }
503 catch { return fail(wall, dur) }1184 catch { return fail(wall, dur) }
504 return BuildResult(ok: true, wall: wall, dur: dur, isNetwork: isNet)1185 return BuildResult(ok: true, wall: wall, dur: dur, isNetwork: isNet,
1186 newBytes: newBytes, oldBytes: oldBytes)
505 }1187 }
506 try? FileManager.default.removeItem(at: tmp)1188 try? FileManager.default.removeItem(at: tmp)
507 return fail(wall, dur)1189 return fail(wall, dur)
...@@ -611,16 +1293,30 @@ final class ChunkManager {...@@ -611,16 +1293,30 @@ final class ChunkManager {
611 return (AVMutableComposition(), -2)1293 return (AVMutableComposition(), -2)
612 }1294 }
6131295
1296 /// One stitched piece of a media's composition: a full chunk (offset 0,
1297 /// dur nil) or a rescue slice sitting `offset` seconds into its grid slot.
1298 private struct CompPart {
1299 let url: URL
1300 let offset: Double
1301 let dur: Double?
1302 }
1303
614 private func kickCompositionBuild(media: MediaItem) {1304 private func kickCompositionBuild(media: MediaItem) {
615 let key = media.cacheKey1305 let key = media.cacheKey
616 guard !compBuilding.contains(key) else { return }1306 guard !compBuilding.contains(key) else { return }
617 compBuilding.insert(key)1307 compBuilding.insert(key)
618 let s = state(for: media)1308 let s = state(for: media)
619 let version = s.version1309 let version = s.version
620 let chunkURLs = Dictionary(uniqueKeysWithValues:1310 var parts: [Int: CompPart] = [:]
621 s.built.keys.map { ($0, chunkURL(key: key, index: $0)) })1311 for i in s.built.keys {
1312 parts[i] = CompPart(url: chunkURL(key: key, index: i), offset: 0, dur: nil)
1313 }
1314 for (i, p) in s.partial where parts[i] == nil {
1315 parts[i] = CompPart(url: rescueURL(key: key, index: i),
1316 offset: p.offset, dur: p.dur)
1317 }
622 Task.detached(priority: .userInitiated) {1318 Task.detached(priority: .userInitiated) {
623 let (comp, playable) = await Self.assemble(media: media, chunkURLs: chunkURLs)1319 let (comp, playable) = await Self.assemble(media: media, parts: parts)
624 await MainActor.run { [self] in1320 await MainActor.run { [self] in
625 self.compBuilding.remove(key)1321 self.compBuilding.remove(key)
626 var s = self.states[key] ?? MediaState()1322 var s = self.states[key] ?? MediaState()
...@@ -634,8 +1330,24 @@ final class ChunkManager {...@@ -634,8 +1330,24 @@ final class ChunkManager {
634 }1330 }
635 }1331 }
6361332
1333 /// A chunk asset's tracks, loaded and ready to insert. `asset` is what
1334 /// keeps the tracks alive: an AVAssetTrack does NOT retain its asset, and
1335 /// inserting a track whose asset has been deallocated fails with
1336 /// -11800/-12780 — silently under `try?`, leaving a black GAP in the
1337 /// composition for a chunk that's perfectly healthy on disk. (Bit us for
1338 /// real: the task-group refactor returned bare tracks, and whether a slot
1339 /// went black depended on autorelease timing.)
1340 private struct LoadedPart {
1341 let index: Int
1342 let asset: AVURLAsset
1343 let v: AVAssetTrack
1344 let a: AVAssetTrack?
1345 let duration: CMTime
1346 let offset: Double
1347 }
1348
637 private static func assemble(media: MediaItem,1349 private static func assemble(media: MediaItem,
638 chunkURLs: [Int: URL]) async -> (AVComposition, Bool) {1350 parts: [Int: CompPart]) async -> (AVComposition, Bool) {
639 let comp = AVMutableComposition()1351 let comp = AVMutableComposition()
640 guard let vTrack = comp.addMutableTrack(1352 guard let vTrack = comp.addMutableTrack(
641 withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)1353 withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
...@@ -647,6 +1359,51 @@ final class ChunkManager {...@@ -647,6 +1359,51 @@ final class ChunkManager {
647 let origV = try? await original.loadTracks(withMediaType: .video).first1359 let origV = try? await original.loadTracks(withMediaType: .video).first
648 let origA = try? await original.loadTracks(withMediaType: .audio).first1360 let origA = try? await original.loadTracks(withMediaType: .audio).first
6491361
1362 // Load every chunk asset's tracks CONCURRENTLY (bounded), then insert
1363 // in order. The old one-await-per-chunk loop made a media with
1364 // hundreds of built chunks take seconds to reassemble — and a
1365 // reassembly runs every time a chunk lands, right when the user is
1366 // waiting to see it.
1367 let wantAudio = aTrack != nil
1368 var loaded: [Int: LoadedPart] = [:]
1369 await withTaskGroup(of: LoadedPart?.self) { group in
1370 var pending = Array(parts).sorted { $0.key < $1.key }[...]
1371 var inFlight = 0
1372 func addNext() {
1373 guard let (i, part) = pending.first else { return }
1374 pending = pending.dropFirst()
1375 inFlight += 1
1376 group.addTask {
1377 let chunk = AVURLAsset(url: part.url)
1378 guard let v = try? await chunk.loadTracks(withMediaType: .video).first
1379 else { return nil }
1380 let d = (try? await chunk.load(.duration)) ?? .zero
1381 let a = wantAudio
1382 ? try? await chunk.loadTracks(withMediaType: .audio).first : nil
1383 return LoadedPart(index: i, asset: chunk, v: v, a: a, duration: d,
1384 offset: part.offset)
1385 }
1386 }
1387 for _ in 0..<8 { addNext() }
1388 while inFlight > 0 {
1389 guard let r = await group.next() else { break }
1390 inFlight -= 1
1391 if let r { loaded[r.index] = r }
1392 addNext()
1393 }
1394 }
1395
1396 func fillFromOriginal(_ range: CMTimeRange) {
1397 if let origV {
1398 try? vTrack.insertTimeRange(range, of: origV, at: range.start)
1399 if let aTrack, let origA {
1400 try? aTrack.insertTimeRange(range, of: origA, at: range.start)
1401 }
1402 } else {
1403 vTrack.insertEmptyTimeRange(range)
1404 }
1405 }
1406
650 let n = Self.chunkCount(duration: media.duration)1407 let n = Self.chunkCount(duration: media.duration)
651 for i in 0..<n {1408 for i in 0..<n {
652 let startSec = Double(i) * Self.chunkSeconds1409 let startSec = Double(i) * Self.chunkSeconds
...@@ -654,29 +1411,36 @@ final class ChunkManager {...@@ -654,29 +1411,36 @@ final class ChunkManager {
654 guard durSec > 0.001 else { break }1411 guard durSec > 0.001 else { break }
655 let at = CMTime(seconds: startSec, preferredTimescale: 600)1412 let at = CMTime(seconds: startSec, preferredTimescale: 600)
656 let dur = CMTime(seconds: durSec, preferredTimescale: 600)1413 let dur = CMTime(seconds: durSec, preferredTimescale: 600)
657 var inserted = false1414 guard let part = loaded[i] else {
658 if let url = chunkURLs[i] {1415 fillFromOriginal(CMTimeRange(start: at, duration: dur))
659 let chunk = AVURLAsset(url: url)1416 continue
660 if let v = try? await chunk.loadTracks(withMediaType: .video).first {
661 let chunkDuration = (try? await chunk.load(.duration)) ?? .zero
662 let r = CMTimeRange(start: .zero, duration: min(dur, chunkDuration))
663 try? vTrack.insertTimeRange(r, of: v, at: at)
664 if let aTrack, let a = try? await chunk.loadTracks(withMediaType: .audio).first {
665 try? aTrack.insertTimeRange(r, of: a, at: at)
666 }
667 inserted = true
668 }
669 }1417 }
670 if !inserted {1418 let sliceAt = CMTime(seconds: startSec + part.offset, preferredTimescale: 600)
671 if let origV {1419 let sliceDur = min(part.duration,
672 let r = CMTimeRange(start: at, duration: dur)1420 CMTime(seconds: durSec - part.offset, preferredTimescale: 600))
673 try? vTrack.insertTimeRange(r, of: origV, at: at)1421 // Rescue slice: original (or empty) leads in, the slice covers the
674 if let aTrack, let origA {1422 // playhead's neighborhood, original (or empty) fills the tail.
675 try? aTrack.insertTimeRange(r, of: origA, at: at)1423 if part.offset > 0.001 {
676 }1424 fillFromOriginal(CMTimeRange(start: at, end: sliceAt))
677 } else {1425 }
678 vTrack.insertEmptyTimeRange(CMTimeRange(start: at, duration: dur))1426 let r = CMTimeRange(start: .zero, duration: sliceDur)
679 }1427 do {
1428 try vTrack.insertTimeRange(r, of: part.v, at: sliceAt)
1429 } catch {
1430 // A healthy chunk that fails to stitch plays back as a BLACK
1431 // gap — never let that be silent again (a dropped asset
1432 // reference made every insert fail exactly this way once).
1433 SeqLog.log("[cache] comp insert FAILED %@#%d dur=%.2f: %@",
1434 String(media.cacheKey.prefix(8)), i, sliceDur.seconds,
1435 String(describing: error))
1436 }
1437 if let aTrack, let a = part.a {
1438 try? aTrack.insertTimeRange(r, of: a, at: sliceAt)
1439 }
1440 let sliceEnd = sliceAt + sliceDur
1441 let slotEnd = at + dur
1442 if sliceEnd + CMTime(seconds: 0.001, preferredTimescale: 600) < slotEnd {
1443 fillFromOriginal(CMTimeRange(start: sliceEnd, end: slotEnd))
680 }1444 }
681 }1445 }
682 return (comp, origV != nil)1446 return (comp, origV != nil)
sequencer/Sources/Sequencer/Document.swift+31-1
...@@ -12,7 +12,7 @@ import AppKit...@@ -12,7 +12,7 @@ import AppKit
12/// and force the document type for any `.sq` URL — regardless of what12/// and force the document type for any `.sq` URL — regardless of what
13/// LaunchServices believes — so opening always resolves to `ProjectDocument`.13/// LaunchServices believes — so opening always resolves to `ProjectDocument`.
14final class ProjectDocumentController: NSDocumentController {14final class ProjectDocumentController: NSDocumentController {
15 private static let projectType = "com.clover.sequencer.project"15 private static let projectType = "net.paperclover.sequencer.project"
16 private let sqPanelDelegate = SQOpenPanelDelegate()16 private let sqPanelDelegate = SQOpenPanelDelegate()
1717
18 /// Pin the document type for `.sq` URLs so it never resolves to a folder,18 /// Pin the document type for `.sq` URLs so it never resolves to a folder,
...@@ -41,6 +41,21 @@ final class ProjectDocumentController: NSDocumentController {...@@ -41,6 +41,21 @@ final class ProjectDocumentController: NSDocumentController {
41 }41 }
42 }42 }
43 }43 }
44
45 /// Opening a project from a pristine untitled window replaces that window
46 /// instead of leaving an empty one behind.
47 override func openDocument(withContentsOf url: URL, display displayDocument: Bool,
48 completionHandler: @escaping (NSDocument?, Bool, Error?) -> Void) {
49 let blanks = documents.compactMap { $0 as? ProjectDocument }.filter(\.isPristineUntitled)
50 super.openDocument(withContentsOf: url, display: displayDocument) { doc, alreadyOpen, error in
51 if doc != nil, error == nil {
52 for blank in blanks where blank.isPristineUntitled && blank !== doc {
53 blank.close()
54 }
55 }
56 completionHandler(doc, alreadyOpen, error)
57 }
58 }
44}59}
4560
46/// Enables only `.sq` items (package directories or legacy flat files) in the61/// Enables only `.sq` items (package directories or legacy flat files) in the
...@@ -73,6 +88,21 @@ final class ProjectDocument: NSDocument {...@@ -73,6 +88,21 @@ final class ProjectDocument: NSDocument {
73 /// first explicit save.88 /// first explicit save.
74 override class var autosavesInPlace: Bool { true }89 override class var autosavesInPlace: Bool { true }
7590
91 /// Never saved, never edited, and nothing on the timeline — safe to close
92 /// when a real project opens over it.
93 var isPristineUntitled: Bool {
94 fileURL == nil && !isDocumentEdited
95 && ctx.store.project.media.isEmpty && ctx.store.project.clips.isEmpty
96 }
97
98 /// Stop per-document services before AppKit tears the document down, so a
99 /// proxy build or the playback clock finishing after close can't touch the
100 /// now-dangling `unowned` context.
101 override func close() {
102 ctx.shutdown()
103 super.close()
104 }
105
76 override func makeWindowControllers() {106 override func makeWindowControllers() {
77 let wc = SequencerWindowController(ctx: ctx)107 let wc = SequencerWindowController(ctx: ctx)
78 addWindowController(wc)108 addWindowController(wc)
sequencer/Sources/Sequencer/DocumentContext.swift+40-3
...@@ -47,6 +47,25 @@ final class DocumentContext {...@@ -47,6 +47,25 @@ final class DocumentContext {
47 }47 }
48 }48 }
4949
50 private var didShutdown = false
51
52 /// Tear the document's services down while `self` is still alive — called
53 /// from `ProjectDocument.close()`. Services hold `unowned var ctx`; an
54 /// in-flight chunk build or the 60 Hz clock landing a callback AFTER the
55 /// context deallocs would trap on that dangling reference. Stopping them here
56 /// (before dealloc) makes every such late callback a no-op.
57 func shutdown() {
58 guard !didShutdown else { return }
59 didShutdown = true
60 playback.stop()
61 chunks.stop()
62 comps.stopWatching()
63 if let reconcileObserver {
64 NotificationCenter.default.removeObserver(reconcileObserver)
65 self.reconcileObserver = nil
66 }
67 }
68
50 deinit {69 deinit {
51 if let reconcileObserver { NotificationCenter.default.removeObserver(reconcileObserver) }70 if let reconcileObserver { NotificationCenter.default.removeObserver(reconcileObserver) }
52 comps.stopWatching()71 comps.stopWatching()
...@@ -55,8 +74,22 @@ final class DocumentContext {...@@ -55,8 +74,22 @@ final class DocumentContext {
55 /// Start the per-document services (playback clock, comps folder watch,74 /// Start the per-document services (playback clock, comps folder watch,
56 /// derived-asset warmup). Called once by the window controller after load.75 /// derived-asset warmup). Called once by the window controller after load.
57 func startServices() {76 func startServices() {
77 // Trim the shared media cache under its byte cap now that this project's
78 // media is registered (so its own cache is protected). Opening a
79 // document is also the natural moment to re-measure the cache from
80 // disk (reconcile) so the incremental ledger can't drift for long.
81 MediaPipeline.shared.evictIfNeeded(reconcile: true)
58 MediaPipeline.shared.ensureDerivedAssets(for: store.project)82 MediaPipeline.shared.ensureDerivedAssets(for: store.project)
59 chunks.ensure(for: store.project)83 // NOTE: the whole-project proxy pre-build (`chunks.ensure`) is NOT kicked
84 // here. macOS state restoration reopens *every* previously-open project on
85 // launch, and each one running `ensure` would transcode its full ProRes
86 // proxy set in parallel — every open project's media counts as "in use", so
87 // eviction can't trim any of it and the shared cache blows past its cap and
88 // fills the disk. Instead the fill is triggered when a document's window
89 // becomes main (SequencerWindowController.windowDidBecomeMain): the
90 // frontmost project fills immediately, a background/restored project fills
91 // only once you switch to it. On-demand playhead builds (`want`) still run
92 // for whatever is actually playing.
60 comps.rescan()93 comps.rescan()
61 comps.startWatching()94 comps.startWatching()
62 playback.start()95 playback.start()
...@@ -69,10 +102,14 @@ final class DocumentContext {...@@ -69,10 +102,14 @@ final class DocumentContext {
69 /// (Settings, Export, cache eviction) that operate on whichever project is102 /// (Settings, Export, cache eviction) that operate on whichever project is
70 /// frontmost. Falls back to the headless context when nothing is open.103 /// frontmost. Falls back to the headless context when nothing is open.
71 static var current: DocumentContext {104 static var current: DocumentContext {
72 if let wc = NSApp.keyWindow?.windowController as? SequencerWindowController {105 // `NSApp` is nil in the headless `--selftest` harness (no NSApplication);
106 // guard it so callers on the build path can ask "am I frontmost?" without
107 // crashing — with no app, the headless context is by definition current.
108 guard let app = NSApp else { return headless }
109 if let wc = app.keyWindow?.windowController as? SequencerWindowController {
73 return wc.ctx110 return wc.ctx
74 }111 }
75 if let wc = NSApp.mainWindow?.windowController as? SequencerWindowController {112 if let wc = app.mainWindow?.windowController as? SequencerWindowController {
76 return wc.ctx113 return wc.ctx
77 }114 }
78 if let doc = NSDocumentController.shared.currentDocument as? ProjectDocument {115 if let doc = NSDocumentController.shared.currentDocument as? ProjectDocument {
sequencer/Sources/Sequencer/Export.swift+30-4
...@@ -186,6 +186,7 @@ enum ExportError: Error, LocalizedError {...@@ -186,6 +186,7 @@ enum ExportError: Error, LocalizedError {
186 case fusion(String)186 case fusion(String)
187 case intermediateFailed187 case intermediateFailed
188 case encodeFailed(String)188 case encodeFailed(String)
189 case missingMedia([String])
189190
190 var errorDescription: String? {191 var errorDescription: String? {
191 switch self {192 switch self {
...@@ -194,6 +195,11 @@ enum ExportError: Error, LocalizedError {...@@ -194,6 +195,11 @@ enum ExportError: Error, LocalizedError {
194 case .fusion(let s): return s195 case .fusion(let s): return s
195 case .intermediateFailed: return "Could not render the timeline composition."196 case .intermediateFailed: return "Could not render the timeline composition."
196 case .encodeFailed(let s): return "ffmpeg failed to encode the output.\n\n\(s)"197 case .encodeFailed(let s): return "ffmpeg failed to encode the output.\n\n\(s)"
198 case .missingMedia(let names):
199 let list = names.map { " • \($0)" }.joined(separator: "\n")
200 return "Export was stopped because this media could not be read — the "
201 + "output would silently drop those clips. Reconnect the drive or "
202 + "relink the files and try again:\n\n\(list)"
197 }203 }
198 }204 }
199}205}
...@@ -244,24 +250,34 @@ enum Exporter {...@@ -244,24 +250,34 @@ enum Exporter {
244 let comp = AVMutableComposition()250 let comp = AVMutableComposition()
245 let wantVideo = job.format.isVideo && !segments.isEmpty251 let wantVideo = job.format.isVideo && !segments.isEmpty
246252
253 // Media that couldn't be read while building the composition. A silent
254 // gap here means the render quietly drops content, so we collect every
255 // offending source and abort (below) rather than hand ffmpeg a truncated
256 // intermediate that looks like a successful export.
257 var missing = Set<String>()
258
247 // Video: one track, segments appended left-to-right with empty gaps.259 // Video: one track, segments appended left-to-right with empty gaps.
248 if wantVideo, let vTrack = comp.addMutableTrack(260 if wantVideo, let vTrack = comp.addMutableTrack(
249 withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) {261 withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) {
250 var cursor = 0.0262 var cursor = 0.0
251 for seg in segments {263 for seg in segments {
252 guard let media = project.media(seg.mediaId) else { continue }264 guard let media = project.media(seg.mediaId) else {
265 missing.insert("(missing media reference)"); continue
266 }
253 if seg.start > cursor + 1e-6 {267 if seg.start > cursor + 1e-6 {
254 vTrack.insertEmptyTimeRange(cmRange(cursor, seg.start - cursor))268 vTrack.insertEmptyTimeRange(cmRange(cursor, seg.start - cursor))
255 cursor = seg.start269 cursor = seg.start
256 }270 }
257 let asset = AVURLAsset(url: media.url)271 let asset = AVURLAsset(url: media.url)
258 guard let src = loadTracksSync(asset, mediaType: .video).first else {272 guard let src = loadTracksSync(asset, mediaType: .video).first else {
273 missing.insert(media.displayName)
259 vTrack.insertEmptyTimeRange(cmRange(cursor, seg.duration)); cursor += seg.duration; continue274 vTrack.insertEmptyTimeRange(cmRange(cursor, seg.duration)); cursor += seg.duration; continue
260 }275 }
261 let srcDur = seg.duration * seg.speed276 let srcDur = seg.duration * seg.speed
262 let range = cmRange(seg.srcIn, srcDur)277 let range = cmRange(seg.srcIn, srcDur)
263 let at = cm(cursor)278 let at = cm(cursor)
264 try? vTrack.insertTimeRange(range, of: src, at: at)279 do { try vTrack.insertTimeRange(range, of: src, at: at) }
280 catch { missing.insert(media.displayName) }
265 if abs(seg.speed - 1) > 1e-6 {281 if abs(seg.speed - 1) > 1e-6 {
266 // Nothing has been appended after `at` yet, so scaling this282 // Nothing has been appended after `at` yet, so scaling this
267 // range back to timeline duration is safe.283 // range back to timeline duration is safe.
...@@ -280,9 +296,12 @@ enum Exporter {...@@ -280,9 +296,12 @@ enum Exporter {
280 withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)296 withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
281 else { continue }297 else { continue }
282 let asset = AVURLAsset(url: media.url)298 let asset = AVURLAsset(url: media.url)
283 guard let src = loadTracksSync(asset, mediaType: .audio).first else { continue }299 guard let src = loadTracksSync(asset, mediaType: .audio).first else {
300 missing.insert(media.displayName); continue
301 }
284 let srcDur = clip.duration * clip.speed302 let srcDur = clip.duration * clip.speed
285 try? aTrack.insertTimeRange(cmRange(clip.srcIn, srcDur), of: src, at: cm(clip.start))303 do { try aTrack.insertTimeRange(cmRange(clip.srcIn, srcDur), of: src, at: cm(clip.start)) }
304 catch { missing.insert(media.displayName) }
286 if abs(clip.speed - 1) > 1e-6 {305 if abs(clip.speed - 1) > 1e-6 {
287 aTrack.scaleTimeRange(cmRange(clip.start, srcDur), toDuration: cm(clip.duration))306 aTrack.scaleTimeRange(cmRange(clip.start, srcDur), toDuration: cm(clip.duration))
288 }307 }
...@@ -298,6 +317,13 @@ enum Exporter {...@@ -298,6 +317,13 @@ enum Exporter {
298 mixParams.append(p)317 mixParams.append(p)
299 }318 }
300319
320 // Any unreadable source means the render would be silently truncated —
321 // stop and tell the user exactly which files, rather than produce a
322 // partial export that looks complete.
323 if !missing.isEmpty {
324 throw ExportError.missingMedia(missing.sorted())
325 }
326
301 // Render the composition to an intermediate the encoder can read.327 // Render the composition to an intermediate the encoder can read.
302 let tmp = tempDir()328 let tmp = tempDir()
303 defer { try? FileManager.default.removeItem(at: tmp) }329 defer { try? FileManager.default.removeItem(at: tmp) }
sequencer/Sources/Sequencer/FrameCache.swift created+106
...@@ -0,0 +1,106 @@
1import Foundation
2import AVFoundation
3import QuartzCore
4
5/// RAM cache of decoded, full-quality stand-in frames.
6///
7/// The players keep only ~1s of decoded video buffered, so the instant of a
8/// clip boundary (an item swap, or a long seek inside a stitched composition)
9/// has nothing exact to show for a beat — the viewer fell back to a 240px
10/// filmstrip thumb, or the "Loading Media…" spinner when even that missed.
11/// This cache holds the *exact* frames those moments need: cut heads and
12/// tails near the playhead are warmed before the boundary arrives, and any
13/// frame the viewer is currently unable to show gets decoded on demand from
14/// the sharpest thing on disk (chunk, rescue slice, or playable original).
15///
16/// Budget comes from the `maxRAMGB` default (Settings → Global, 2 GB when
17/// unset); a 1080p frame is ~8 MB, so even the default holds a couple of
18/// hundred boundaries. Frames are keyed on 0.25s buckets — a stand-in a
19/// fraction of a second off is indistinguishable during the sub-second hold
20/// it covers.
21final class FrameCache {
22 static let shared = FrameCache()
23 private static let bucket = 0.25
24
25 static var ramGB: Int {
26 let gb = UserDefaults.standard.integer(forKey: "maxRAMGB")
27 return gb > 0 ? gb : 2
28 }
29
30 private let cache = NSCache<NSString, CGImage>()
31 /// Decodes in flight / recently failed (source vanished mid-decode, or a
32 /// truncated file) — failures back off so a hopeless frame isn't retried
33 /// every tick. Main-thread only.
34 private var inFlight = Set<String>()
35 private var failedAt: [String: Double] = [:]
36 private let genQueue = DispatchQueue(label: "sequencer.framecache",
37 qos: .userInitiated)
38
39 init() { refreshBudget() }
40
41 /// Re-read the budget after the Settings field changes.
42 func refreshBudget() {
43 cache.totalCostLimit = Self.ramGB * 1_000_000_000
44 }
45
46 private func key(_ mediaKey: String, bucket: Int) -> NSString {
47 "\(mediaKey)@\(bucket)" as NSString
48 }
49 private func bucketIndex(_ src: Double) -> Int {
50 Int((src / Self.bucket).rounded())
51 }
52
53 /// The cached frame nearest `src`, if any (this bucket or a neighbor).
54 func image(media: MediaItem, at src: Double) -> CGImage? {
55 let b = bucketIndex(src)
56 for d in [0, -1, 1] {
57 if let img = cache.object(forKey: key(media.cacheKey, bucket: b + d)) {
58 return img
59 }
60 }
61 return nil
62 }
63
64 /// Decode the frame at `src` into the cache if it isn't there already.
65 /// `source` is a playable (file, time-in-file) pair for this moment,
66 /// resolved on the main thread by `ChunkManager.frameSource` — nil (no
67 /// chunk, no playable original) is a silent no-op. Posts
68 /// `.viewerNeedsRefresh` when the frame lands so a waiting cell picks it
69 /// up. Cheap and self-deduping; safe to call every tick.
70 /// `SEQ_NOWARM=1` disables decodes (diagnostic kill switch).
71 private static let disabled = ProcessInfo.processInfo.environment["SEQ_NOWARM"] != nil
72
73 func warm(mediaKey: String, at src: Double, source: (url: URL, time: Double)?) {
74 guard !Self.disabled, let source else { return }
75 let k = key(mediaKey, bucket: bucketIndex(src))
76 let ks = k as String
77 guard cache.object(forKey: k) == nil, !inFlight.contains(ks) else { return }
78 let now = CACurrentMediaTime()
79 if let failed = failedAt[ks], now - failed < 5 { return }
80 inFlight.insert(ks)
81 genQueue.async { [self] in
82 let asset = AVURLAsset(url: source.url)
83 let gen = AVAssetImageGenerator(asset: asset)
84 gen.appliesPreferredTrackTransform = true
85 // Exact going in, a hair of slack after: the frame ON the cut is
86 // what a boundary needs, but a keyframe-snap decode is far cheaper
87 // than a precise reverse walk.
88 gen.requestedTimeToleranceBefore = .zero
89 gen.requestedTimeToleranceAfter = CMTime(seconds: 0.2, preferredTimescale: 600)
90 let t = CMTime(seconds: max(0, source.time), preferredTimescale: 60000)
91 let img = try? gen.copyCGImage(at: t, actualTime: nil)
92 DispatchQueue.main.async {
93 self.inFlight.remove(ks)
94 if let img {
95 self.failedAt.removeValue(forKey: ks)
96 self.cache.setObject(img, forKey: k,
97 cost: img.bytesPerRow * img.height)
98 NotificationCenter.default.post(name: .viewerNeedsRefresh,
99 object: nil)
100 } else {
101 self.failedAt[ks] = CACurrentMediaTime()
102 }
103 }
104 }
105 }
106}
sequencer/Sources/Sequencer/HangMonitor.swift created+190
...@@ -0,0 +1,190 @@
1import Foundation
2import Darwin
3
4/// Main-thread hang watchdog. A background thread pings the main queue every
5/// 20ms; when the pong stops coming back for >100ms, the main thread is
6/// stalled — exactly the "microhang" that makes playback video stutter while
7/// audio (decoded off-main by CoreAudio) keeps going. While the stall lasts,
8/// the watchdog suspends the main thread for microseconds at a time, walks its
9/// frame pointers, and symbolicates — so the log names the culprit, not just
10/// the duration. Lines go to ~/Library/Logs/Sequencer.log via SeqLog:
11///
12/// [hang] main thread 0.34s during playback (rate 1.0) — 3 samples,
13/// top: ViewerGridView.update() ← CA::Transaction::commit ← …
14///
15/// Cost when healthy: one trivial main-queue block per 20ms. Disable with
16/// SEQ_NOHANGWATCH=1.
17/// SEQ_HANGTEST target: a recognizable frame that should appear in the
18/// sampled stack. Spins (not sleeps) so the pc sits in our own code.
19@inline(never)
20func hangTestStall() {
21 let until = CFAbsoluteTimeGetCurrent() + 0.4
22 var sink = 0.0
23 while CFAbsoluteTimeGetCurrent() < until { sink += sin(sink) + 1 }
24 _ = sink
25}
26
27enum HangMonitor {
28 /// Written from PlaybackController.setRate (main), read by the watchdog
29 /// thread. Benign race — it only annotates log lines.
30 nonisolated(unsafe) static var playbackRate: Double = 0
31
32 private nonisolated(unsafe) static var mainThread: thread_t = 0
33 private nonisolated(unsafe) static var lastPong = CFAbsoluteTimeGetCurrent()
34 private nonisolated(unsafe) static var pingInFlight = false
35 private static let lock = NSLock()
36 private static let threshold = 0.1 // report stalls longer than this
37
38 /// Call once from the main thread at startup.
39 static func start() {
40 guard ProcessInfo.processInfo.environment["SEQ_NOHANGWATCH"] == nil else { return }
41 mainThread = pthread_mach_thread_np(pthread_self())
42 let t = Thread { watch() }
43 t.name = "sequencer.hangwatch"
44 t.qualityOfService = .userInitiated
45 t.start()
46 }
47
48 private static func watch() {
49 while true {
50 usleep(20_000)
51 lock.lock()
52 let age = CFAbsoluteTimeGetCurrent() - lastPong
53 let busy = pingInFlight
54 if !busy {
55 pingInFlight = true
56 lock.unlock()
57 DispatchQueue.main.async {
58 lock.lock()
59 lastPong = CFAbsoluteTimeGetCurrent()
60 pingInFlight = false
61 lock.unlock()
62 }
63 } else {
64 lock.unlock()
65 }
66 if busy, age > threshold { observeStall(begunAge: age) }
67 }
68 }
69
70 /// Main thread has been unresponsive for `begunAge` already. Sample its
71 /// stack periodically until it recovers, then log one line.
72 private static func observeStall(begunAge: Double) {
73 let start = CFAbsoluteTimeGetCurrent() - begunAge
74 var samples: [[String]] = []
75 while true {
76 if samples.count < 5 {
77 let frames = sampleMainStack()
78 if !frames.isEmpty { samples.append(frames) }
79 }
80 usleep(100_000)
81 lock.lock()
82 let stillStalled = pingInFlight && lastPong < start
83 lock.unlock()
84 if !stillStalled { break }
85 }
86 // Wait for the pong to actually land so the duration is honest.
87 var duration = CFAbsoluteTimeGetCurrent() - start
88 for _ in 0..<200 { // give the queued pong up to 2s to run
89 lock.lock(); let pong = lastPong; lock.unlock()
90 if pong >= start { duration = pong - start; break }
91 usleep(10_000)
92 }
93 guard duration > threshold else { return }
94 let rate = playbackRate
95 let during = rate != 0 ? String(format: " during playback (rate %.1f)", rate) : ""
96 let top = samples.first?.prefix(8).joined(separator: " ← ") ?? "no stack (sampling failed)"
97 SeqLog.log("[hang] main thread %.2fs%@ — %d sample%@, top: %@",
98 duration, during, samples.count, samples.count == 1 ? "" : "s", top)
99 for extra in samples.dropFirst() where extra.first != samples.first?.first {
100 SeqLog.log("[hang] also seen: %@", extra.prefix(5).joined(separator: " ← "))
101 }
102 }
103
104 // MARK: stack sampling (arm64 frame-pointer walk)
105
106 /// Fixed buffers, touched only by the watchdog thread. They exist so the
107 /// suspend window below performs ZERO allocations: if the main thread is
108 /// suspended while holding the malloc lock, any malloc here deadlocks the
109 /// whole app (watchdog waits on the lock, suspended main can never release
110 /// it, thread_resume never runs). This happened — main frozen mid free()
111 /// in drawRuler, watchdog frozen in Array.append → permanent freeze.
112 private static let maxFrames = 50
113 private nonisolated(unsafe) static var pcBuf = [UInt64](repeating: 0, count: maxFrames)
114 private nonisolated(unsafe) static var pairBuf = [UInt64](repeating: 0, count: 2)
115
116 private static func sampleMainStack() -> [String] {
117 guard mainThread != 0, thread_suspend(mainThread) == KERN_SUCCESS else { return [] }
118 // ---- suspend window: no allocation, no locks, no ObjC/Swift runtime
119 // calls that might take either. Only mach syscalls and raw stores. ----
120 var n = 0
121 var state = arm_thread_state64_t()
122 var count = mach_msg_type_number_t(MemoryLayout<arm_thread_state64_t>.size
123 / MemoryLayout<natural_t>.size)
124 let kr = withUnsafeMutablePointer(to: &state) { ptr in
125 ptr.withMemoryRebound(to: natural_t.self, capacity: Int(count)) {
126 thread_get_state(mainThread, ARM_THREAD_STATE64, $0, &count)
127 }
128 }
129 if kr == KERN_SUCCESS {
130 pcBuf[n] = arm64PC(state); n += 1
131 let lr = arm64LR(state)
132 var fp = arm64FP(state)
133 if lr != 0 { pcBuf[n] = lr; n += 1 }
134 // Frame layout: [fp] = caller fp, [fp+8] = return address.
135 while n < maxFrames {
136 guard fp != 0, fp & 0x7 == 0 else { break }
137 var outSize: mach_vm_size_t = 16
138 let r = pairBuf.withUnsafeMutableBytes { buf in
139 mach_vm_read_overwrite(mach_task_self_, mach_vm_address_t(fp), 16,
140 mach_vm_address_t(UInt(bitPattern: buf.baseAddress)),
141 &outSize)
142 }
143 guard r == KERN_SUCCESS, pairBuf[1] != 0 else { break }
144 pcBuf[n] = pairBuf[1]; n += 1
145 guard pairBuf[0] > fp else { break } // stacks grow down; fp chain grows up
146 fp = pairBuf[0]
147 }
148 }
149 thread_resume(mainThread)
150 // ---- end suspend window; symbolication may allocate freely. ----
151 return (0..<n).compactMap { symbolicate(pcBuf[$0]) }
152 }
153
154 private static func arm64PC(_ s: arm_thread_state64_t) -> UInt64 { s.__pc }
155 private static func arm64LR(_ s: arm_thread_state64_t) -> UInt64 {
156 s.__lr & 0x0000_7FFF_FFFF_FFFF // strip ptrauth bits
157 }
158 private static func arm64FP(_ s: arm_thread_state64_t) -> UInt64 { s.__fp }
159
160 private typealias DemangleFn = @convention(c) (
161 UnsafePointer<CChar>?, Int, UnsafeMutablePointer<CChar>?,
162 UnsafeMutablePointer<Int>?, UInt32) -> UnsafeMutablePointer<CChar>?
163 private static let demangleFn: DemangleFn? = {
164 guard let sym = dlsym(dlopen(nil, RTLD_NOW), "swift_demangle") else { return nil }
165 return unsafeBitCast(sym, to: DemangleFn.self)
166 }()
167
168 private static func symbolicate(_ pc: UInt64) -> String? {
169 let stripped = pc & 0x0000_7FFF_FFFF_FFFF
170 var info = Dl_info()
171 guard dladdr(UnsafeRawPointer(bitPattern: UInt(stripped)), &info) != 0 else { return nil }
172 var name: String
173 if let sname = info.dli_sname {
174 name = String(cString: sname)
175 if name.hasPrefix("$s") || name.hasPrefix("_$s"), let fn = demangleFn,
176 let d = fn(name, name.utf8.count, nil, nil, 0) {
177 name = String(cString: d)
178 free(d)
179 // Demangled Swift names are long; keep the signature-free head.
180 if let paren = name.firstIndex(of: "(") { name = String(name[..<paren]) + "()" }
181 }
182 } else if let fname = info.dli_fname {
183 name = (String(cString: fname) as NSString).lastPathComponent
184 + String(format: "+0x%llx", stripped - UInt64(UInt(bitPattern: info.dli_fbase)))
185 } else {
186 return nil
187 }
188 return name
189 }
190}
sequencer/Sources/Sequencer/LayerTest.swift created+77
...@@ -0,0 +1,77 @@
1import AppKit
2import AVFoundation
3
4/// Diagnostic (temporary): `sequencer --layertest <key:orig> [<key:orig> …]`.
5/// Builds the same chunk+original stitched compositions the app plays and
6/// shows each in a bare AVPlayerLayer tile — isolating "do N of these render
7/// concurrently" from every other moving part of the app. Seeks all tiles to
8/// SEQ_LAYERTEST_T (default 16.6).
9@MainActor
10func runLayerTest(specs: [String]) {
11 let t = Double(ProcessInfo.processInfo.environment["SEQ_LAYERTEST_T"] ?? "") ?? 16.6
12 let cols = specs.count
13 let tileW = 420.0, tileH = 260.0
14 let win = NSWindow(contentRect: NSRect(x: 100, y: 300,
15 width: tileW * Double(cols), height: tileH),
16 styleMask: [.titled], backing: .buffered, defer: false)
17 win.title = "LayerTest t=\(t)"
18 win.contentView!.wantsLayer = true
19 win.makeKeyAndOrderFront(nil)
20 NSApp.activate(ignoringOtherApps: true)
21
22 for (col, spec) in specs.enumerated() {
23 let bits = spec.split(separator: ":", maxSplits: 1).map(String.init)
24 guard bits.count == 2 else { continue }
25 let key = bits[0], orig = bits[1]
26 let player = AVPlayer()
27 player.automaticallyWaitsToMinimizeStalling = false
28 let layer = AVPlayerLayer(player: player)
29 layer.frame = NSRect(x: Double(col) * tileW, y: 0, width: tileW, height: tileH)
30 layer.videoGravity = .resizeAspect
31 layer.backgroundColor = NSColor.purple.cgColor // un-rendered = purple
32 win.contentView!.layer!.addSublayer(layer)
33 let chunkDir = MediaPipeline.shared.cacheRoot
34 .appendingPathComponent(key).appendingPathComponent("chunks")
35 Task { @MainActor in
36 let comp = AVMutableComposition()
37 let vTrack = comp.addMutableTrack(withMediaType: .video,
38 preferredTrackID: kCMPersistentTrackID_Invalid)!
39 let original = AVURLAsset(url: URL(fileURLWithPath: orig))
40 let origV = try? await original.loadTracks(withMediaType: .video).first
41 let origDur = (try? await original.load(.duration))?.seconds ?? 0
42 for i in 0..<Int(ceil(origDur / 30)) {
43 let startSec = Double(i) * 30
44 let at = CMTime(seconds: startSec, preferredTimescale: 600)
45 let dur = CMTime(seconds: min(30, origDur - startSec),
46 preferredTimescale: 600)
47 let url = chunkDir.appendingPathComponent(String(format: "c%06d.mov", i))
48 if FileManager.default.fileExists(atPath: url.path) {
49 let chunk = AVURLAsset(url: url)
50 if let v = try? await chunk.loadTracks(withMediaType: .video).first {
51 let d = (try? await chunk.load(.duration)) ?? .zero
52 try? vTrack.insertTimeRange(
53 CMTimeRange(start: .zero, duration: min(d, dur)), of: v, at: at)
54 continue
55 }
56 }
57 if let origV {
58 try? vTrack.insertTimeRange(CMTimeRange(start: at, duration: dur),
59 of: origV, at: at)
60 }
61 }
62 let item = AVPlayerItem(asset: comp)
63 item.preferredForwardBufferDuration = 1
64 player.replaceCurrentItem(with: item)
65 while item.status != .readyToPlay {
66 try? await Task.sleep(nanoseconds: 100_000_000)
67 }
68 let t0 = CACurrentMediaTime()
69 nonisolated(unsafe) let layerRef = layer // diagnostic: read on main at completion
70 player.seek(to: CMTime(seconds: t, preferredTimescale: 60000),
71 toleranceBefore: .zero, toleranceAfter: .zero) { _ in
72 NSLog("[layertest] %@ seek landed in %.1fs ready=%d", key,
73 CACurrentMediaTime() - t0, layerRef.isReadyForDisplay ? 1 : 0)
74 }
75 }
76 }
77}
sequencer/Sources/Sequencer/Log.swift created+44
...@@ -0,0 +1,44 @@
1import Foundation
2
3/// Diagnostics that must survive without a console attached. NSLog only
4/// reaches the unified log (invisible for `open`-launched apps unless you go
5/// digging in Console.app), so the lines worth keeping — every [miss] spinner
6/// event, rescue/build failures, admission blocks — are ALSO appended to
7/// `~/Library/Logs/Sequencer.log`. Plain text, timestamped, rotated at 5 MB
8/// (one `.old` generation kept).
9enum SeqLog {
10 private static let queue = DispatchQueue(label: "sequencer.log", qos: .utility)
11 private static let url: URL = {
12 let dir = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
13 .appendingPathComponent("Logs", isDirectory: true)
14 try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
15 return dir.appendingPathComponent("Sequencer.log")
16 }()
17 private static let stamp: DateFormatter = {
18 let f = DateFormatter()
19 f.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
20 return f
21 }()
22
23 /// Log to the unified log (NSLog) AND the plain-text file.
24 static func log(_ format: String, _ args: CVarArg...) {
25 let line = String(format: format, arguments: args)
26 NSLog("%@", line)
27 let dated = "\(stamp.string(from: Date())) \(line)\n"
28 queue.async {
29 if let size = (try? FileManager.default.attributesOfItem(atPath: url.path)[.size]
30 as? NSNumber)?.int64Value, size > 5_000_000 {
31 let old = url.deletingPathExtension().appendingPathExtension("old.log")
32 try? FileManager.default.removeItem(at: old)
33 try? FileManager.default.moveItem(at: url, to: old)
34 }
35 if let h = FileHandle(forWritingAtPath: url.path) {
36 defer { try? h.close() }
37 _ = try? h.seekToEnd()
38 try? h.write(contentsOf: Data(dated.utf8))
39 } else {
40 try? Data(dated.utf8).write(to: url)
41 }
42 }
43 }
44}
sequencer/Sources/Sequencer/MediaPipeline.swift+350-55
...@@ -17,8 +17,8 @@ final class MediaPipeline {...@@ -17,8 +17,8 @@ final class MediaPipeline {
17 static let shared = MediaPipeline()17 static let shared = MediaPipeline()
1818
19 let cacheRoot: URL19 let cacheRoot: URL
20 /// LRU cap in bytes (default 50 GB). Override with `defaults write20 /// LRU cap in bytes (default 50 GB). Override in Settings, or `defaults
21 /// com.sequencer maxCacheGB -int 100`.21 /// write net.paperclover.Sequencer maxCacheGB -int 100`.
22 var maxCacheBytes: Int64 {22 var maxCacheBytes: Int64 {
23 let gb = UserDefaults.standard.integer(forKey: "maxCacheGB")23 let gb = UserDefaults.standard.integer(forKey: "maxCacheGB")
24 return Int64(gb > 0 ? gb : 50) * 1_000_000_00024 return Int64(gb > 0 ? gb : 50) * 1_000_000_000
...@@ -28,7 +28,7 @@ final class MediaPipeline {...@@ -28,7 +28,7 @@ final class MediaPipeline {
28 private let ffprobe: String?28 private let ffprobe: String?
29 private let workQueue = OperationQueue()29 private let workQueue = OperationQueue()
30 private var statuses: [UUID: MediaStatus] = [:] // main-thread only30 private var statuses: [UUID: MediaStatus] = [:] // main-thread only
31 private let thumbCache = NSCache<NSString, NSImage>()31 private let thumbCache = NSCache<NSString, CGImage>()
32 private var stripInfoCache: [String: (interval: Double, count: Int)] = [:]32 private var stripInfoCache: [String: (interval: Double, count: Int)] = [:]
33 private var lruTouched: [String: Date] = [:]33 private var lruTouched: [String: Date] = [:]
3434
...@@ -44,6 +44,12 @@ final class MediaPipeline {...@@ -44,6 +44,12 @@ final class MediaPipeline {
44 ffprobe = Self.findExecutable("ffprobe")44 ffprobe = Self.findExecutable("ffprobe")
45 workQueue.maxConcurrentOperationCount = 245 workQueue.maxConcurrentOperationCount = 2
46 thumbCache.countLimit = 200046 thumbCache.countLimit = 2000
47 // A previous instance killed mid-build (rebuild relaunch, force quit)
48 // leaves orphaned ffmpeg encoders holding shared VideoToolbox decode
49 // sessions — enough of them and every AVPlayer here renders black.
50 Self.reapOrphans(cacheRoot: cacheRoot)
51 // Measure the cache once at launch; chunk builds wait on `budgetReady`.
52 DispatchQueue.main.async { self.reconcileLedger() }
47 }53 }
4854
49 static func findExecutable(_ name: String) -> String? {55 static func findExecutable(_ name: String) -> String? {
...@@ -68,6 +74,18 @@ final class MediaPipeline {...@@ -68,6 +74,18 @@ final class MediaPipeline {
68 return s74 return s
69 }75 }
7076
77 private var offlineCache: [String: (offline: Bool, until: Date)] = [:] // main-thread only
78 /// Whether the media's ORIGINAL file is currently unreachable (moved, or on
79 /// an unmounted NAS). Cached briefly so the viewer can call it every frame
80 /// without a `stat` each time, and so it auto-recovers when the drive returns.
81 func isOffline(_ media: MediaItem) -> Bool {
82 let now = Date()
83 if let c = offlineCache[media.cacheKey], c.until > now { return c.offline }
84 let off = !FileManager.default.fileExists(atPath: media.path)
85 offlineCache[media.cacheKey] = (off, now.addingTimeInterval(2))
86 return off
87 }
88
71 // MARK: - Cache paths89 // MARK: - Cache paths
7290
73 /// The shape `cacheKey(for:)` produces: exactly 16 lowercase hex chars. A91 /// The shape `cacheKey(for:)` produces: exactly 16 lowercase hex chars. A
...@@ -236,25 +254,71 @@ final class MediaPipeline {...@@ -236,25 +254,71 @@ final class MediaPipeline {
236 "aformat=channel_layouts=mono,showwavespic=s=2048x200:colors=white",254 "aformat=channel_layouts=mono,showwavespic=s=2048x200:colors=white",
237 "-frames:v", "1", waveformURL(media.cacheKey).path,255 "-frames:v", "1", waveformURL(media.cacheKey).path,
238 ])256 ])
257 let bytes = (try? FileManager.default.attributesOfItem(
258 atPath: waveformURL(media.cacheKey).path)[.size] as? NSNumber)?.int64Value ?? 0
239 DispatchQueue.main.async {259 DispatchQueue.main.async {
240 self.touchLRU(media.cacheKey)260 self.touchLRU(media.cacheKey)
241 if res.exitCode == 0 {261 if res.exitCode == 0 {
242 NotificationCenter.default.post(name: .mediaStatusChanged, object: nil)262 self.noteBytesAdded(bytes)
263 self.imgStateLock.lock()
264 self.waveformMissing.remove(media.cacheKey)
265 self.imgStateLock.unlock()
266 // `scene: true` — a waveform image changes timeline pixels
267 // (the tile cache flushes on it; plain chunk churn must not).
268 NotificationCenter.default.post(name: .mediaStatusChanged, object: nil,
269 userInfo: ["scene": true])
243 }270 }
244 }271 }
245 }272 }
246273
247 private let waveformCache = NSCache<NSString, NSImage>()274 private let waveformCache = NSCache<NSString, CGImage>()
275
276 /// Decode an image file straight into the display's raster format (BGRA
277 /// premultiplied, screen colorspace). A plain NSImage/CGImageSource image
278 /// keeps the file's own format (RGB JPEG, generic colorspace), and Quartz
279 /// then converts it on EVERY draw — per thumbnail, per frame. Converting
280 /// once at load makes the timeline's image blits plain memory copies.
281 static func displayImage(contentsOf url: URL) -> CGImage? {
282 guard let src = CGImageSourceCreateWithURL(url as CFURL, nil),
283 let raw = CGImageSourceCreateImageAtIndex(
284 src, 0, [kCGImageSourceShouldCache: false] as CFDictionary)
285 else { return nil }
286 let space = NSScreen.main?.colorSpace?.cgColorSpace
287 ?? CGColorSpace(name: CGColorSpace.sRGB)!
288 guard let ctx = CGContext(
289 data: nil, width: raw.width, height: raw.height,
290 bitsPerComponent: 8, bytesPerRow: 0, space: space,
291 bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue
292 | CGBitmapInfo.byteOrder32Little.rawValue) else { return raw }
293 ctx.draw(raw, in: CGRect(x: 0, y: 0, width: raw.width, height: raw.height))
294 return ctx.makeImage() ?? raw
295 }
248296
249 func waveformImage(for media: MediaItem) -> NSImage? {297 /// Same negative/in-flight bookkeeping as thumbnails: a missing waveform
250 let key = media.cacheKey as NSString298 /// must not re-dispatch a load per audio clip per frame.
251 if let img = waveformCache.object(forKey: key) { return img }299 private var waveformMissing = Set<String>()
252 let url = waveformURL(media.cacheKey)300 private var waveformLoading = Set<String>()
301
302 func waveformImage(for media: MediaItem) -> CGImage? {
303 let key = media.cacheKey
304 if let img = waveformCache.object(forKey: key as NSString) { return img }
305 imgStateLock.lock()
306 let skip = waveformMissing.contains(key) || waveformLoading.contains(key)
307 if !skip { waveformLoading.insert(key) }
308 imgStateLock.unlock()
309 guard !skip else { return nil }
310 let url = waveformURL(key)
253 DispatchQueue.global(qos: .utility).async {311 DispatchQueue.global(qos: .utility).async {
254 guard let img = NSImage(contentsOf: url) else { return }312 let img = Self.displayImage(contentsOf: url)
255 DispatchQueue.main.async {313 DispatchQueue.main.async {
256 self.waveformCache.setObject(img, forKey: key)314 self.imgStateLock.lock()
257 self.notifyThumbsCoalesced()315 self.waveformLoading.remove(key)
316 if img == nil { self.waveformMissing.insert(key) }
317 self.imgStateLock.unlock()
318 if let img {
319 self.waveformCache.setObject(img, forKey: key as NSString)
320 self.notifyThumbsCoalesced()
321 }
258 }322 }
259 }323 }
260 return nil324 return nil
...@@ -279,41 +343,72 @@ final class MediaPipeline {...@@ -279,41 +343,72 @@ final class MediaPipeline {
279 try? d.write(to: stripInfoURL(media.cacheKey))343 try? d.write(to: stripInfoURL(media.cacheKey))
280 }344 }
281 }345 }
346 let bytes = Self.directorySize(dir)
282 DispatchQueue.main.async {347 DispatchQueue.main.async {
283 var s = self.status(for: media)348 var s = self.status(for: media)
284 s.filmstripReady = res.exitCode == 0 && count > 0349 s.filmstripReady = res.exitCode == 0 && count > 0
285 self.statuses[media.id] = s350 self.statuses[media.id] = s
286 self.touchLRU(media.cacheKey)351 self.touchLRU(media.cacheKey)
287 NotificationCenter.default.post(name: .mediaStatusChanged, object: nil)352 self.noteBytesAdded(bytes)
353 self.imgStateLock.lock()
354 self.thumbMissing.removeAll() // a fresh strip supersedes misses
355 self.imgStateLock.unlock()
356 NotificationCenter.default.post(name: .mediaStatusChanged, object: nil,
357 userInfo: ["scene": true])
288 }358 }
289 }359 }
290360
291 // MARK: - Filmstrip access361 // MARK: - Filmstrip access
292362
293 func filmstripInfo(_ key: String) -> (interval: Double, count: Int)? {363 func filmstripInfo(_ key: String) -> (interval: Double, count: Int)? {
294 if let c = stripInfoCache[key] { return c }364 imgStateLock.lock()
365 let cached = stripInfoCache[key]
366 imgStateLock.unlock()
367 if let cached { return cached }
295 guard let data = try? Data(contentsOf: stripInfoURL(key)),368 guard let data = try? Data(contentsOf: stripInfoURL(key)),
296 let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],369 let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
297 let interval = json["interval"] as? Double,370 let interval = json["interval"] as? Double,
298 let count = json["count"] as? Int else { return nil }371 let count = json["count"] as? Int else { return nil }
372 imgStateLock.lock()
299 stripInfoCache[key] = (interval, count)373 stripInfoCache[key] = (interval, count)
374 imgStateLock.unlock()
300 return (interval, count)375 return (interval, count)
301 }376 }
302377
303 /// Cached thumbnail nearest to `seconds`; loads async and posts a single378 /// Cached thumbnail nearest to `seconds`; loads async and posts a single
304 /// coalesced .mediaStatusChanged when thumbs land (a post per thumb379 /// coalesced .mediaStatusChanged when thumbs land (a post per thumb
305 /// cascades into an app-wide refresh storm while a filmstrip streams in).380 /// cascades into an app-wide refresh storm while a filmstrip streams in).
306 func filmstripImage(for media: MediaItem, at seconds: Double) -> NSImage? {381 /// Thumbs that failed to load (file absent/evicted) or are mid-load.
382 /// Without these a dense timeline re-dispatches a load per missing thumb
383 /// per FRAME — hundreds of no-op queue hops every draw. Misses are
384 /// forgotten whenever fresh thumbs land (strips may have regenerated).
385 /// Lock-guarded: the timeline rasterizes lanes on parallel threads.
386 private var thumbMissing = Set<String>()
387 private var thumbLoading = Set<String>()
388 private let imgStateLock = NSLock()
389
390 func filmstripImage(for media: MediaItem, at seconds: Double) -> CGImage? {
307 guard let info = filmstripInfo(media.cacheKey) else { return nil }391 guard let info = filmstripInfo(media.cacheKey) else { return nil }
308 let index = min(info.count, max(1, Int(seconds / info.interval) + 1))392 let index = min(info.count, max(1, Int(seconds / info.interval) + 1))
309 let cacheId = "\(media.cacheKey)/\(index)" as NSString393 let cacheId = "\(media.cacheKey)/\(index)"
310 if let img = thumbCache.object(forKey: cacheId) { return img }394 if let img = thumbCache.object(forKey: cacheId as NSString) { return img }
395 imgStateLock.lock()
396 let skip = thumbMissing.contains(cacheId) || thumbLoading.contains(cacheId)
397 if !skip { thumbLoading.insert(cacheId) }
398 imgStateLock.unlock()
399 guard !skip else { return nil }
311 let url = stripDir(media.cacheKey).appendingPathComponent(String(format: "%06d.jpg", index))400 let url = stripDir(media.cacheKey).appendingPathComponent(String(format: "%06d.jpg", index))
312 DispatchQueue.global(qos: .utility).async {401 DispatchQueue.global(qos: .utility).async {
313 guard let img = NSImage(contentsOf: url) else { return }402 let img = Self.displayImage(contentsOf: url)
314 DispatchQueue.main.async {403 DispatchQueue.main.async {
315 self.thumbCache.setObject(img, forKey: cacheId)404 self.imgStateLock.lock()
316 self.notifyThumbsCoalesced()405 self.thumbLoading.remove(cacheId)
406 if img == nil { self.thumbMissing.insert(cacheId) }
407 self.imgStateLock.unlock()
408 if let img {
409 self.thumbCache.setObject(img, forKey: cacheId as NSString)
410 self.notifyThumbsCoalesced()
411 }
317 }412 }
318 }413 }
319 return nil414 return nil
...@@ -325,7 +420,11 @@ final class MediaPipeline {...@@ -325,7 +420,11 @@ final class MediaPipeline {
325 thumbNotifyPending = true420 thumbNotifyPending = true
326 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {421 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
327 self.thumbNotifyPending = false422 self.thumbNotifyPending = false
328 NotificationCenter.default.post(name: .mediaStatusChanged, object: nil)423 self.imgStateLock.lock()
424 self.thumbMissing.removeAll() // strips may have (re)generated
425 self.imgStateLock.unlock()
426 NotificationCenter.default.post(name: .mediaStatusChanged, object: nil,
427 userInfo: ["scene": true])
329 }428 }
330 }429 }
331430
...@@ -341,52 +440,202 @@ final class MediaPipeline {...@@ -341,52 +440,202 @@ final class MediaPipeline {
341440
342 private var evicting = false441 private var evicting = false
343442
344 /// LRU eviction under the byte cap. Size scan and deletion run off-main443 // MARK: - Cache budget ledger
345 /// (the cache walk is I/O); never touches the current project's media.444
346 func evictIfNeeded() {445 /// Approximate cache size in bytes, maintained incrementally (builds add,
347 guard !evicting else { return }446 /// evictions subtract) and reconciled against a real walk at launch and on
447 /// document open. With the ledger, staying under the cap is enforced BEFORE
448 /// bytes hit the disk (`tryReserve`), and the per-build full-cache walk the
449 /// old eviction did is gone. Main-thread only.
450 private(set) var ledgerBytes: Int64 = 0
451 /// Bytes promised to in-flight chunk builds; released on completion.
452 private var reservedBytes: Int64 = 0
453 /// False until the first reconcile walk finishes. Admission stays closed
454 /// while false, so an unmeasured cache can never be built past the cap.
455 private(set) var budgetReady = false
456 private var reconciling = false
457
458 /// When eviction runs at all it frees down to here (not just under the
459 /// cap), so it works in batches instead of one chunk per build.
460 var lowWatermarkBytes: Int64 { Int64(Double(maxCacheBytes) * 0.92) }
461
462 /// Re-measure the cache and set the ledger to ground truth. Called at
463 /// launch, on document open, and when the cap changes; incremental updates
464 /// keep it honest in between.
465 /// Anything on disk with an mtime before this was left by a previous
466 /// session — the test for purging session-transient files (rescue slices,
467 /// crashed builds' .partial temps) without touching live ones.
468 private static let processStart = Date()
469
470 func reconcileLedger() {
471 guard !reconciling else { return }
472 reconciling = true
473 let root = cacheRoot
474 DispatchQueue.global(qos: .utility).async {
475 Self.purgeSessionTransients(root: root)
476 let total = Self.directorySize(root)
477 DispatchQueue.main.async {
478 NSLog("[cache] reconcile: %.2f GB on disk (cap %.0f GB)",
479 Double(total) / 1e9, Double(self.maxCacheBytes) / 1e9)
480 self.ledgerBytes = total
481 self.budgetReady = true
482 self.reconciling = false
483 if self.ledgerBytes + self.reservedBytes > self.maxCacheBytes {
484 self.evictToFit(need: 0) { _ in }
485 }
486 for c in DocumentContext.allLive { c.chunks.budgetChanged() }
487 }
488 }
489 }
490
491 /// Reserve room for a build about to start. The reservation counts against
492 /// the ceiling alongside bytes already on disk, so two concurrent builds
493 /// can't both squeeze into the same headroom. Window builds reserve up to
494 /// the cap; background fill only up to the low watermark — the band in
495 /// between is slack for playhead work, so fill can never trigger (or
496 /// refight) an eviction. Main thread.
497 func tryReserve(bytes: Int64, upTo ceiling: Int64) -> Bool {
498 guard budgetReady, ledgerBytes + reservedBytes + bytes <= ceiling else { return false }
499 reservedBytes += bytes
500 return true
501 }
502
503 /// A reserved build finished: release its reservation and record what
504 /// actually landed on disk (new file minus any replaced one; 0 on failure).
505 func commitBuild(reserved: Int64, delta: Int64) {
506 reservedBytes = max(0, reservedBytes - reserved)
507 ledgerBytes = max(0, ledgerBytes + delta)
508 }
509
510 /// Bytes written outside the reservation flow (filmstrips, waveforms —
511 /// small, but the ledger should still see them between reconciles).
512 func noteBytesAdded(_ bytes: Int64) { ledgerBytes += bytes }
513
514 /// Cheap budget check for the old trigger sites (build completions, doc
515 /// open, manual). `reconcile: true` re-walks the disk first — use it when
516 /// ground truth matters (doc open, cap change, menu action).
517 func evictIfNeeded(reconcile: Bool = false) {
518 if reconcile || !budgetReady { reconcileLedger(); return }
519 if ledgerBytes + reservedBytes > maxCacheBytes {
520 evictToFit(need: 0) { _ in }
521 }
522 }
523
524 /// Free cache space so `need` more bytes fit under `ceiling` (the cap for
525 /// playhead work, the low watermark for fill), evicting down to the low
526 /// watermark once it runs at all. Coldness order: whole cache dirs of
527 /// projects no window has open (LRU) first, then — when `openDocChunks` —
528 /// individual cold proxy chunks of open documents, never touching any
529 /// document's working set (see ChunkManager.evictionCandidates).
530 /// `completion(true)` on main once the space exists.
531 func evictToFit(need: Int64, openDocChunks: Bool = true,
532 upTo ceiling: Int64? = nil,
533 completion: @escaping (Bool) -> Void) {
534 let ceiling = ceiling ?? maxCacheBytes
535 guard budgetReady, !evicting else { completion(false); return }
536 let deficit = (ledgerBytes + reservedBytes + need) - lowWatermarkBytes
537 guard deficit > 0 else { completion(true); return }
348 evicting = true538 evicting = true
349 // Protect the media of every open document (not just the front one) so539 // Whole-dir eviction must not touch any open document's media — those
350 // eviction can't drop cache another window is still using.540 // dirs also hold filmstrips/waveforms other windows are showing. Their
541 // cold CHUNKS are reclaimed individually via the candidates instead.
351 let inUse = Set(DocumentContext.allLive.flatMap { $0.store.project.media.map(\.cacheKey) })542 let inUse = Set(DocumentContext.allLive.flatMap { $0.store.project.media.map(\.cacheKey) })
543 var chunkCands: [ChunkManager.EvictionCandidate] = []
544 if openDocChunks {
545 for c in DocumentContext.allLive { chunkCands += c.chunks.evictionCandidates() }
546 chunkCands.sort { $0.coldness > $1.coldness }
547 }
352 let root = cacheRoot548 let root = cacheRoot
353 let cap = maxCacheBytes
354 DispatchQueue.global(qos: .utility).async {549 DispatchQueue.global(qos: .utility).async {
355 let fm = FileManager.default550 let fm = FileManager.default
356 var evicted: [String] = []551 var freed: Int64 = 0
357 defer {552 var evictedDirs: [String] = []
358 DispatchQueue.main.async {553 var evictedChunks: [String: [Int]] = [:]
359 for k in evicted {554 // 1. Closed projects' whole cache dirs, least recently used first.
360 self.stripInfoCache[k] = nil555 if let keys = try? fm.contentsOfDirectory(atPath: root.path) {
361 self.enqueued.remove(k)556 var entries: [(key: String, lastUsed: Double)] = []
362 }557 for key in keys where !inUse.contains(key) {
363 for c in DocumentContext.allLive { c.chunks.forget(keys: evicted) }558 let dir = root.appendingPathComponent(key, isDirectory: true)
364 self.evicting = false559 var isDir: ObjCBool = false
560 guard fm.fileExists(atPath: dir.path, isDirectory: &isDir), isDir.boolValue else { continue }
561 let lastUsed = Double((try? String(contentsOf: dir.appendingPathComponent("lastUsed"), encoding: .utf8)) ?? "") ?? 0
562 entries.append((key, lastUsed))
563 }
564 for e in entries.sorted(by: { $0.lastUsed < $1.lastUsed }) {
565 guard freed < deficit else { break }
566 let dir = root.appendingPathComponent(e.key, isDirectory: true)
567 let bytes = Self.directorySize(dir)
568 try? fm.removeItem(at: dir)
569 evictedDirs.append(e.key)
570 freed += bytes
365 }571 }
366 }572 }
367 guard let keys = try? fm.contentsOfDirectory(atPath: root.path) else { return }573 // 2. Cold chunks of open documents, coldest (farthest from any
368 var entries: [(key: String, bytes: Int64, lastUsed: Double)] = []574 // playhead / off-timeline) first.
369 var total: Int64 = 0575 for c in chunkCands {
370 for key in keys {576 guard freed < deficit else { break }
371 let dir = root.appendingPathComponent(key, isDirectory: true)577 let bytes = (try? fm.attributesOfItem(atPath: c.url.path)[.size] as? NSNumber)?.int64Value ?? 0
372 var isDir: ObjCBool = false578 guard bytes > 0 else { continue }
373 guard fm.fileExists(atPath: dir.path, isDirectory: &isDir), isDir.boolValue else { continue }579 try? fm.removeItem(at: c.url)
374 let bytes = Self.directorySize(dir)580 evictedChunks[c.key, default: []].append(c.index)
375 let lastUsed = Double((try? String(contentsOf: dir.appendingPathComponent("lastUsed"), encoding: .utf8)) ?? "") ?? 0581 freed += bytes
376 total += bytes
377 entries.append((key, bytes, lastUsed))
378 }582 }
379 guard total > cap else { return }583 DispatchQueue.main.async {
380 for e in entries.sorted(by: { $0.lastUsed < $1.lastUsed }) where !inUse.contains(e.key) {584 self.ledgerBytes = max(0, self.ledgerBytes - freed)
381 try? fm.removeItem(at: root.appendingPathComponent(e.key, isDirectory: true))585 NSLog("[cache] evict: freed %.2f GB (%d dirs, %d chunks) → ledger %.2f GB",
382 evicted.append(e.key)586 Double(freed) / 1e9, evictedDirs.count,
383 total -= e.bytes587 evictedChunks.values.map(\.count).reduce(0, +),
384 if total <= cap { break }588 Double(self.ledgerBytes) / 1e9)
589 self.imgStateLock.lock()
590 for k in evictedDirs {
591 self.stripInfoCache[k] = nil
592 }
593 self.imgStateLock.unlock()
594 for k in evictedDirs {
595 self.enqueued.remove(k)
596 }
597 for c in DocumentContext.allLive {
598 c.chunks.forget(keys: evictedDirs)
599 for (key, idxs) in evictedChunks {
600 c.chunks.noteEvicted(key: key, indices: idxs)
601 }
602 }
603 self.evicting = false
604 let ok = self.ledgerBytes + self.reservedBytes + need <= ceiling
605 if !evictedDirs.isEmpty || !evictedChunks.isEmpty {
606 NotificationCenter.default.post(name: .mediaStatusChanged, object: nil)
607 // Only re-pump when something was actually freed — a no-op
608 // eviction re-pumping would loop pump → evict → pump forever.
609 for c in DocumentContext.allLive { c.chunks.budgetChanged() }
610 }
611 completion(ok)
385 }612 }
386 }613 }
387 }614 }
388615
389 private static func directorySize(_ url: URL) -> Int64 {616 /// Delete leftovers no session references anymore: rescue-slice files
617 /// (`rNNNNNN.mov` — their state is memory-only, so a relaunch can't know
618 /// which part of the chunk they cover) and `.partial.mov` temps from
619 /// builds a crash interrupted. Only files from BEFORE this process
620 /// started — a slice the current session just built stays.
621 private static func purgeSessionTransients(root: URL) {
622 let fm = FileManager.default
623 guard let en = fm.enumerator(at: root,
624 includingPropertiesForKeys: [.contentModificationDateKey])
625 else { return }
626 for case let f as URL in en {
627 let name = f.lastPathComponent
628 let isRescue = name.hasPrefix("r") && name.hasSuffix(".mov") && name.count == 11
629 && f.deletingLastPathComponent().lastPathComponent == "chunks"
630 let isTemp = name.hasSuffix(".partial.mov")
631 guard isRescue || isTemp else { continue }
632 let m = (try? f.resourceValues(forKeys: [.contentModificationDateKey])
633 .contentModificationDate) ?? .distantPast
634 if m < processStart { try? fm.removeItem(at: f) }
635 }
636 }
637
638 static func directorySize(_ url: URL) -> Int64 {
390 var total: Int64 = 0639 var total: Int64 = 0
391 if let en = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.fileSizeKey]) {640 if let en = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.fileSizeKey]) {
392 for case let f as URL in en {641 for case let f as URL in en {
...@@ -400,6 +649,46 @@ final class MediaPipeline {...@@ -400,6 +649,46 @@ final class MediaPipeline {
400649
401 struct RunResult { var exitCode: Int32; var stdout: String }650 struct RunResult { var exitCode: Int32; var stdout: String }
402651
652 /// Every child process currently running, so app teardown can take them
653 /// down too. A killed Sequencer otherwise leaves its in-flight ffmpeg
654 /// encodes running as orphans — and each one holds VideoToolbox decode
655 /// sessions from a SHARED machine-wide pool. Enough accumulated orphans
656 /// (a few rebuild-relaunch cycles' worth) and every AVPlayer in the next
657 /// app instance silently renders BLACK: items park, seeks land,
658 /// isReadyForDisplay says true, no error anywhere. Diagnosed 2026-07-11
659 /// after the viewer went black with all state reporting healthy.
660 private static var liveChildren: [Int32: Process] = [:]
661 private static let childLock = NSLock()
662
663 /// Terminate every live child (normal quit AND SIGTERM — run.sh pkills
664 /// the app on every rebuild, which is exactly how orphans were minted).
665 static func terminateChildren() {
666 childLock.lock()
667 let children = Array(liveChildren.values)
668 childLock.unlock()
669 for p in children where p.isRunning { p.terminate() }
670 }
671
672 /// Kill orphaned ffmpeg processes from a PREVIOUS Sequencer instance
673 /// (crash, force-quit, kill -9 — anything terminateChildren couldn't
674 /// catch). Identified by their command line referencing our cache root,
675 /// so nothing else on the machine can match. Runs once at launch.
676 static func reapOrphans(cacheRoot: URL) {
677 let marker = cacheRoot.path
678 DispatchQueue.global(qos: .utility).async {
679 let r = run("/usr/bin/pgrep", ["-fl", "ffmpeg"])
680 var killed = 0
681 for line in r.stdout.split(separator: "\n") where line.contains(marker) {
682 guard let pid = Int32(line.prefix(while: \.isNumber)) else { continue }
683 kill(pid, SIGKILL)
684 killed += 1
685 }
686 if killed > 0 {
687 SeqLog.log("[cache] reaped %d orphaned ffmpeg encoder(s) from a previous instance", killed)
688 }
689 }
690 }
691
403 @discardableResult692 @discardableResult
404 static func run(_ path: String, _ args: [String],693 static func run(_ path: String, _ args: [String],
405 duration: Double? = nil,694 duration: Double? = nil,
...@@ -426,7 +715,13 @@ final class MediaPipeline {...@@ -426,7 +715,13 @@ final class MediaPipeline {
426 }715 }
427 do {716 do {
428 try p.run()717 try p.run()
718 childLock.lock()
719 liveChildren[p.processIdentifier] = p
720 childLock.unlock()
429 p.waitUntilExit()721 p.waitUntilExit()
722 childLock.lock()
723 liveChildren.removeValue(forKey: p.processIdentifier)
724 childLock.unlock()
430 } catch {725 } catch {
431 return RunResult(exitCode: -1, stdout: "")726 return RunResult(exitCode: -1, stdout: "")
432 }727 }
sequencer/Sources/Sequencer/Model.swift+36-9
...@@ -142,16 +142,22 @@ struct ViewState: Codable, Equatable {...@@ -142,16 +142,22 @@ struct ViewState: Codable, Equatable {
142 var laneScale: Double = 1142 var laneScale: Double = 1
143 var snapping = true143 var snapping = true
144 var showFilmstrips = true144 var showFilmstrips = true
145 var subtitles = true // clover-transcript captions overlay
145 var previewsOnLeft = false146 var previewsOnLeft = false
146 var priorityPane: TrackRef? = nil147 var priorityPane: TrackRef? = nil
147 var fusionHidden = false148 var fusionHidden = false
148 var fusionFocus = false149 var fusionFocus = false
150 /// Where the playhead was parked at save. Reopening resumes here, which
151 /// also tells the proxy cache which neighbourhood to keep warm/protected
152 /// for this project while its window is in the background.
153 var playhead: Double = 0
149154
150 init() {}155 init() {}
151156
152 enum CodingKeys: String, CodingKey {157 enum CodingKeys: String, CodingKey {
153 case hiddenTracks, focusedTracks, trackHeights, laneScale, snapping,158 case hiddenTracks, focusedTracks, trackHeights, laneScale, snapping,
154 showFilmstrips, previewsOnLeft, priorityPane, fusionHidden, fusionFocus159 showFilmstrips, subtitles, previewsOnLeft, priorityPane,
160 fusionHidden, fusionFocus, playhead
155 }161 }
156 init(from decoder: Decoder) throws {162 init(from decoder: Decoder) throws {
157 let c = try decoder.container(keyedBy: CodingKeys.self)163 let c = try decoder.container(keyedBy: CodingKeys.self)
...@@ -161,10 +167,12 @@ struct ViewState: Codable, Equatable {...@@ -161,10 +167,12 @@ struct ViewState: Codable, Equatable {
161 laneScale = try c.decodeIfPresent(Double.self, forKey: .laneScale) ?? 1167 laneScale = try c.decodeIfPresent(Double.self, forKey: .laneScale) ?? 1
162 snapping = try c.decodeIfPresent(Bool.self, forKey: .snapping) ?? true168 snapping = try c.decodeIfPresent(Bool.self, forKey: .snapping) ?? true
163 showFilmstrips = try c.decodeIfPresent(Bool.self, forKey: .showFilmstrips) ?? true169 showFilmstrips = try c.decodeIfPresent(Bool.self, forKey: .showFilmstrips) ?? true
170 subtitles = try c.decodeIfPresent(Bool.self, forKey: .subtitles) ?? true
164 previewsOnLeft = try c.decodeIfPresent(Bool.self, forKey: .previewsOnLeft) ?? false171 previewsOnLeft = try c.decodeIfPresent(Bool.self, forKey: .previewsOnLeft) ?? false
165 priorityPane = try c.decodeIfPresent(TrackRef.self, forKey: .priorityPane)172 priorityPane = try c.decodeIfPresent(TrackRef.self, forKey: .priorityPane)
166 fusionHidden = try c.decodeIfPresent(Bool.self, forKey: .fusionHidden) ?? false173 fusionHidden = try c.decodeIfPresent(Bool.self, forKey: .fusionHidden) ?? false
167 fusionFocus = try c.decodeIfPresent(Bool.self, forKey: .fusionFocus) ?? false174 fusionFocus = try c.decodeIfPresent(Bool.self, forKey: .fusionFocus) ?? false
175 playhead = try c.decodeIfPresent(Double.self, forKey: .playhead) ?? 0
168 }176 }
169}177}
170178
...@@ -341,8 +349,10 @@ struct MediaItem: Codable, Equatable, Identifiable {...@@ -341,8 +349,10 @@ struct MediaItem: Codable, Equatable, Identifiable {
341 var isAudio: Bool = false // audio-only file (no video stream)349 var isAudio: Bool = false // audio-only file (no video stream)
342 var cacheKey: String = ""350 var cacheKey: String = ""
343351
344 var url: URL { URL(fileURLWithPath: path) }352 // isDirectory:false skips URL's hidden stat() — path lives on the NAS,
345 var displayName: String { url.lastPathComponent }353 // and per-draw stats of a cold network volume freeze the whole UI.
354 var url: URL { URL(fileURLWithPath: path, isDirectory: false) }
355 var displayName: String { (path as NSString).lastPathComponent }
346356
347 init(path: String) { self.path = path }357 init(path: String) { self.path = path }
348358
...@@ -418,6 +428,14 @@ struct Clip: Codable, Equatable, Identifiable {...@@ -418,6 +428,14 @@ struct Clip: Codable, Equatable, Identifiable {
418 /// Source time for a timeline moment inside the clip.428 /// Source time for a timeline moment inside the clip.
419 func sourceTime(at t: Double) -> Double { srcIn + (t - start) * speed }429 func sourceTime(at t: Double) -> Double { srcIn + (t - start) * speed }
420430
431 /// Inverse of `sourceTime(at:)`: the timeline moment inside the clip that
432 /// shows source second `s`. Clamped to the clip's own span so it can't jump
433 /// the playhead onto a neighbour.
434 func timelineTime(forSource s: Double) -> Double {
435 let t = speed != 0 ? start + (s - srcIn) / speed : start
436 return min(max(t, start), end)
437 }
438
421 init(mediaId: UUID?, track: TrackRef, start: Double, srcIn: Double,439 init(mediaId: UUID?, track: TrackRef, start: Double, srcIn: Double,
422 duration: Double, kind: ClipKind = .video, linkId: UUID? = nil,440 duration: Double, kind: ClipKind = .video, linkId: UUID? = nil,
423 board: Board? = nil) {441 board: Board? = nil) {
...@@ -572,14 +590,23 @@ extension ProjectModel {...@@ -572,14 +590,23 @@ extension ProjectModel {
572 by: \.track)590 by: \.track)
573 for (tref, arr) in grouped {591 for (tref, arr) in grouped {
574 if let ref, tref != ref { continue }592 if let ref, tref != ref { continue }
575 let sorted = arr.sorted { ($0.start, $0.id.uuidString) < ($1.start, $1.id.uuidString) }593 // Tiebreak on the id only when starts collide — building a
594 // uuidString per comparison made sorting a dense lane allocate
595 // thousands of strings.
596 let sorted = arr.sorted {
597 $0.start != $1.start ? $0.start < $1.start
598 : $0.id.uuidString < $1.id.uuidString
599 }
576 for i in 0..<sorted.count {600 for i in 0..<sorted.count {
601 let a = sorted[i]
577 for j in (i + 1)..<sorted.count {602 for j in (i + 1)..<sorted.count {
578 let a = sorted[i], b = sorted[j]603 let b = sorted[j]
579 if b.start < a.end - 1e-9 {604 // Sorted by start: once b starts at/after a's end, every
580 out.append(ClipOverlap(a: a, b: b, track: tref,605 // later clip does too. Without this break the scan is
581 start: b.start, end: min(a.end, b.end)))606 // O(n²) per lane — and it runs on every gesture update.
582 }607 if b.start >= a.end - 1e-9 { break }
608 out.append(ClipOverlap(a: a, b: b, track: tref,
609 start: b.start, end: min(a.end, b.end)))
583 }610 }
584 }611 }
585 }612 }
sequencer/Sources/Sequencer/PerfTest.swift+78-15
...@@ -44,7 +44,6 @@ func runPerfTest(path: String) {...@@ -44,7 +44,6 @@ func runPerfTest(path: String) {
44 // runloop until the loads land, so we then measure real thumbnail blits.44 // runloop until the loads land, so we then measure real thumbnail blits.
45 func warmThumbnails() {45 func warmThumbnails() {
46 ctx.session.showFilmstrips = true46 ctx.session.showFilmstrips = true
47 timeline.testSetScrolling(false)
48 NSGraphicsContext.saveGraphicsState()47 NSGraphicsContext.saveGraphicsState()
49 NSGraphicsContext.current = gctx48 NSGraphicsContext.current = gctx
50 for _ in 0..<5 {49 for _ in 0..<5 {
...@@ -57,20 +56,34 @@ func runPerfTest(path: String) {...@@ -57,20 +56,34 @@ func runPerfTest(path: String) {
57 NSGraphicsContext.restoreGraphicsState()56 NSGraphicsContext.restoreGraphicsState()
58 }57 }
5958
60 // Pan across the first half of the timeline over `frames` steps, timing each59 // Pan the timeline over `frames` steps, timing each direct draw. Returns
61 // direct draw. Returns mean ms/frame.60 // (median, p90) ms/frame — the median resists the I/O-contention spikes
62 func measure(scrolling: Bool, frames: Int = 120) -> Double {61 // that async thumbnail loads inject into a mean.
63 timeline.testSetScrolling(scrolling)62 //
63 // Two pan patterns: `panPxPerFrame` nil teleports across half the project
64 // (cache-hostile — approximates fresh zooms and scrubber jumps); a value
65 // pans that many px per frame like a real scroll gesture.
66 func measure(frames: Int = 120, panPxPerFrame: Double? = nil)
67 -> (median: Double, p90: Double) {
64 NSGraphicsContext.saveGraphicsState()68 NSGraphicsContext.saveGraphicsState()
65 NSGraphicsContext.current = gctx69 NSGraphicsContext.current = gctx
66 defer { NSGraphicsContext.restoreGraphicsState() }70 defer { NSGraphicsContext.restoreGraphicsState() }
67 for _ in 0..<3 { timeline.testRedraw() } // warm up71 for _ in 0..<3 { timeline.testRedraw() } // warm up
68 let t0 = Date()72 var times: [Double] = []
73 times.reserveCapacity(frames)
69 for i in 0..<frames {74 for i in 0..<frames {
70 timeline.testSetOrigin(panSpan * Double(i) / Double(frames))75 if let px = panPxPerFrame {
76 timeline.testSetOrigin(panSpan * 0.25
77 + Double(i) * px / timeline.testPxPerSecond)
78 } else {
79 timeline.testSetOrigin(panSpan * Double(i) / Double(frames))
80 }
81 let t0 = DispatchTime.now().uptimeNanoseconds
71 timeline.testRedraw()82 timeline.testRedraw()
83 times.append(Double(DispatchTime.now().uptimeNanoseconds - t0) / 1e6)
72 }84 }
73 return Date().timeIntervalSince(t0) / Double(frames) * 100085 times.sort()
86 return (times[frames / 2], times[Int(Double(frames) * 0.9)])
74 }87 }
7588
76 // pxPerSecond values: fit-all (everything on screen at once) up through89 // pxPerSecond values: fit-all (everything on screen at once) up through
...@@ -84,7 +97,7 @@ func runPerfTest(path: String) {...@@ -84,7 +97,7 @@ func runPerfTest(path: String) {
84 ("empty", 4000),97 ("empty", 4000),
85 ]98 ]
86 ctx.session.showFilmstrips = true99 ctx.session.showFilmstrips = true
87 print("mode zoom px/s visible ms/frame (fps) thumbs/frame")100 print("mode zoom px/s visible med ms p90 ms (fps) thumbs/frame")
88 for (name, pps) in zooms {101 for (name, pps) in zooms {
89 timeline.testSetPxPerSecond(pps)102 timeline.testSetPxPerSecond(pps)
90 timeline.testSetOrigin(0)103 timeline.testSetOrigin(0)
...@@ -93,17 +106,67 @@ func runPerfTest(path: String) {...@@ -93,17 +106,67 @@ func runPerfTest(path: String) {
93 // Count thumbnails actually blitted over one profiled pan pass.106 // Count thumbnails actually blitted over one profiled pan pass.
94 DrawProf.on = true; DrawProf.thumbHits = 0107 DrawProf.on = true; DrawProf.thumbHits = 0
95 NSGraphicsContext.saveGraphicsState(); NSGraphicsContext.current = gctx108 NSGraphicsContext.saveGraphicsState(); NSGraphicsContext.current = gctx
96 timeline.testSetScrolling(false)
97 for i in 0..<60 { timeline.testSetOrigin(panSpan * Double(i) / 60); timeline.testRedraw() }109 for i in 0..<60 { timeline.testSetOrigin(panSpan * Double(i) / 60); timeline.testRedraw() }
98 NSGraphicsContext.restoreGraphicsState()110 NSGraphicsContext.restoreGraphicsState()
99 let thumbsPerFrame = DrawProf.thumbHits / 60111 let thumbsPerFrame = DrawProf.thumbHits / 60
100 DrawProf.on = false112 DrawProf.on = false
101 for (mode, scrolling) in [("detail ", false), ("scrolling", true)] {113 // A/B inside one process: the direct (Stage-1) path vs the tile cache,
102 let ms = measure(scrolling: scrolling)114 // measured seconds apart so thermal drift can't skew the comparison.
103 print(String(format: "%@ %-9@ %7.2f %7d %8.2f %5.0f %6d",115 // "scroll" pans 8 px/frame like a real gesture; "jump" teleports
104 mode, name as NSString, pps, visible, ms, 1000 / max(0.001, ms),116 // half-project strides (cache-hostile worst case).
105 scrolling ? 0 : thumbsPerFrame))117 let px8 = 8.0
118 for (mode, disabled, pan) in [
119 ("direct-scroll", true, px8 as Double?),
120 ("tiled·-scroll", false, px8),
121 ("direct-jump··", true, nil),
122 ("tiled·-jump··", false, nil),
123 ] {
124 TimelineView.tilesDisabled = disabled
125 _ = measure(frames: 30, panPxPerFrame: pan) // warm/fill cache
126 let ms = measure(panPxPerFrame: pan)
127 print(String(format: "%@ %-9@ %7.2f %7d %8.2f %8.2f %5.0f %6d",
128 mode, name as NSString, pps, visible, ms.median, ms.p90,
129 1000 / max(0.001, ms.median), thumbsPerFrame))
106 }130 }
131 // Zoom simulation: a continuous pinch around this zoom level (±1.5×,
132 // ~1.5%/frame like a real magnify stream). Every frame changes
133 // pxPerSecond, so the tile cache can never hit — this is the pure
134 // cold-render rate that 60 Hz zooming needs.
135 TimelineView.tilesDisabled = false
136 do {
137 let anchor = panSpan * 0.25
138 NSGraphicsContext.saveGraphicsState()
139 NSGraphicsContext.current = gctx
140 for _ in 0..<3 { timeline.testRedraw() }
141 var times: [Double] = []
142 DrawProf.on = true; DrawProf.reset()
143 for i in 0..<120 {
144 let phase = Double(i) / 120 * 4 * .pi
145 let f = exp(sin(phase) * 0.4) // pps swings ×0.67…×1.5
146 timeline.testSetPxPerSecond(pps * f)
147 timeline.testSetOrigin(anchor)
148 let t0 = DispatchTime.now().uptimeNanoseconds
149 timeline.testRedraw()
150 times.append(Double(DispatchTime.now().uptimeNanoseconds - t0) / 1e6)
151 }
152 DrawProf.on = false
153 NSGraphicsContext.restoreGraphicsState()
154 times.sort()
155 print(String(format: "zoom·-pinch·· %-9@ %7.2f %7d %8.2f %8.2f %5.0f",
156 name as NSString, pps, visible, times[60], times[108],
157 1000 / max(0.001, times[60])))
158 DrawProf.report(frames: 120) // per-phase cost of a cold zoom frame
159 timeline.testSetPxPerSecond(pps)
160 }
161 // Per-section breakdown of a tiled pan pass at this zoom.
162 DrawProf.on = true; DrawProf.reset()
163 DrawProf.tileRenders = 0; DrawProf.tileBlits = 0
164 _ = measure(frames: 60)
165 DrawProf.on = false
166 DrawProf.report(frames: 60)
167 print(String(format: " tiles: %.1f blits, %.2f renders /frame",
168 Double(DrawProf.tileBlits) / 60, Double(DrawProf.tileRenders) / 60))
169 TimelineView.tilesDisabled = false
107 }170 }
108 exit(0)171 exit(0)
109}172}
sequencer/Sources/Sequencer/PlaybackController.swift+81-10
...@@ -51,6 +51,15 @@ final class PlaybackController {...@@ -51,6 +51,15 @@ final class PlaybackController {
51 /// 60 Hz timer keeps running for the app's lifetime.51 /// 60 Hz timer keeps running for the app's lifetime.
52 deinit { timer?.invalidate() }52 deinit { timer?.invalidate() }
5353
54 /// Called from `DocumentContext.shutdown()` on document close: kill the timer
55 /// now (while `ctx` is still alive) so no `tick()` fires against a context
56 /// that's mid-teardown.
57 func stop() {
58 rate = 0
59 timer?.invalidate()
60 timer = nil
61 }
62
54 private func tick() {63 private func tick() {
55 // While paused nothing moves; edits and proxy completions push their64 // While paused nothing moves; edits and proxy completions push their
56 // own syncs, so idle costs nothing.65 // own syncs, so idle costs nothing.
...@@ -81,6 +90,7 @@ final class PlaybackController {...@@ -81,6 +90,7 @@ final class PlaybackController {
81 let now = playhead90 let now = playhead
82 let wasStopped = rate == 091 let wasStopped = rate == 0
83 rate = newRate92 rate = newRate
93 HangMonitor.playbackRate = newRate
84 if newRate == 0 {94 if newRate == 0 {
85 pausedPlayhead = now95 pausedPlayhead = now
86 playbackStartPlayhead = nil96 playbackStartPlayhead = nil
...@@ -313,12 +323,16 @@ final class TrackPlayer {...@@ -313,12 +323,16 @@ final class TrackPlayer {
313 // PAUSED: a swap is a pure visual flash with no playback323 // PAUSED: a swap is a pure visual flash with no playback
314 // benefit, so swap only to REVEAL a frame we otherwise324 // benefit, so swap only to REVEAL a frame we otherwise
315 // can't show at all — an unplayable original (DNx) whose325 // can't show at all — an unplayable original (DNx) whose
316 // covering chunk has just landed. Never swap to merely326 // covering chunk has just landed, or a playable original
317 // sharpen a still, and never trade a playable original327 // that still hasn't managed to park on the frame (a slow
318 // (crisper than any proxy for a still) down to a proxy.328 // NAS seek — the cell is showing a stand-in or spinner
329 // while the proxy could show the frame now). Never swap to
330 // merely sharpen a still that's already on screen.
319 // Pending quality upgrades take effect on the next play/seek.331 // Pending quality upgrades take effect on the next play/seek.
332 let cur = player.currentTime().seconds
333 let parkedOff = !cur.isFinite || abs(cur - sourceTime) > 0.5
320 swap = sharperProxy && had == nil334 swap = sharperProxy && had == nil
321 && !ctx.chunks.originalPlayable(media: media)335 && (!ctx.chunks.originalPlayable(media: media) || parkedOff)
322 } else {336 } else {
323 // PLAYING: adopt the proxy the moment it covers the playhead337 // PLAYING: adopt the proxy the moment it covers the playhead
324 // (a NAS original may stutter), throttled so rapid338 // (a NAS original may stutter), throttled so rapid
...@@ -352,7 +366,11 @@ final class TrackPlayer {...@@ -352,7 +366,11 @@ final class TrackPlayer {
352 /// pass) seeks it to the live position and resumes. Without the hold it366 /// pass) seeks it to the live position and resumes. Without the hold it
353 /// blips wrong content from the file's head — audibly so on the audio track.367 /// blips wrong content from the file's head — audibly so on the audio track.
354 private func installItem(_ item: AVPlayerItem) {368 private func installItem(_ item: AVPlayerItem) {
355 item.preferredForwardBufferDuration = lenientSync ? 8 : 1369 // Video buffer depth scales with the RAM budget (Settings → Global):
370 // the default 2 GB keeps the old 1s; a workstation-sized budget buys
371 // deeper read-ahead, which is what smooths NAS originals.
372 item.preferredForwardBufferDuration = lenientSync ? 8
373 : Double(min(8, max(1, FrameCache.ramGB / 2)))
356 let wasPlaying = player.rate != 0374 let wasPlaying = player.rate != 0
357 player.replaceCurrentItem(with: item)375 player.replaceCurrentItem(with: item)
358 if wasPlaying { player.rate = 0 }376 if wasPlaying { player.rate = 0 }
...@@ -438,8 +456,13 @@ final class VideoTrackPlayer {...@@ -438,8 +456,13 @@ final class VideoTrackPlayer {
438 var currentClipId: UUID? { front.currentClipId }456 var currentClipId: UUID? { front.currentClipId }
439457
440 /// Spin up the next clip's buffer this many seconds before its cut — enough458 /// Spin up the next clip's buffer this many seconds before its cut — enough
441 /// to load + decode the first frame even off a NAS original.459 /// to load + decode the first frame even off a NAS original or a stitched
442 private static let preroll = 1.5460 /// composition whose seek has to open a fresh chunk file.
461 private static let preroll = 4.0
462 /// Warm exact cut frames (heads AND tails) into the RAM frame cache when
463 /// their boundary is within this many seconds of the playhead, so even a
464 /// swap that outruns the preroll has the right frame to stand in.
465 private static let warmRadius = 10.0
443466
444 func sync(ref: TrackRef, playhead: Double, rate: Double,467 func sync(ref: TrackRef, playhead: Double, rate: Double,
445 playheadMoved: Bool, force: Bool) {468 playheadMoved: Bool, force: Bool) {
...@@ -462,9 +485,11 @@ final class VideoTrackPlayer {...@@ -462,9 +485,11 @@ final class VideoTrackPlayer {
462 }485 }
463 }486 }
464487
465 // BACK: preroll the next different-media clip when its cut is imminent,488 // BACK: preroll the boundary clip when its cut is imminent, parked
466 // parked (rate 0) and muted at its first frame. Forward playback only —489 // (rate 0) and muted at the frame the cut lands on: forward playback
467 // a reverse/scrub cut falls back to the plain (tiny-gap) swap.490 // prerolls the NEXT different-media clip at its first frame; reverse
491 // prerolls the PREVIOUS one at its last, so a backwards cut is just as
492 // gapless.
468 var buffering = false493 var buffering = false
469 if rate >= 0, let next = nextDifferentClip(on: ref, after: playhead, current: cur),494 if rate >= 0, let next = nextDifferentClip(on: ref, after: playhead, current: cur),
470 next.start - playhead <= Self.preroll,495 next.start - playhead <= Self.preroll,
...@@ -475,10 +500,46 @@ final class VideoTrackPlayer {...@@ -475,10 +500,46 @@ final class VideoTrackPlayer {
475 back.player.isMuted = true500 back.player.isMuted = true
476 back.syncTime(expected: next.srcIn, rate: 0, force: false)501 back.syncTime(expected: next.srcIn, rate: 0, force: false)
477 buffering = true502 buffering = true
503 } else if rate < 0, let prev = prevDifferentClip(on: ref, before: playhead, current: cur),
504 playhead - prev.end <= Self.preroll,
505 let prevMedia = project.media(prev.mediaId) {
506 let tail = max(prev.srcIn, prev.sourceTime(at: prev.end) - 0.05)
507 if back.currentClipId != prev.id {
508 back.setClip(prev, media: prevMedia, sourceTime: tail)
509 }
510 back.player.isMuted = true
511 back.syncTime(expected: tail, rate: 0, force: false)
512 buffering = true
478 }513 }
479 if !buffering, back.currentClipId != nil {514 if !buffering, back.currentClipId != nil {
480 back.setClip(nil, media: nil) // release the idle buffer515 back.setClip(nil, media: nil) // release the idle buffer
481 }516 }
517
518 warmBoundaryFrames(ref: ref, playhead: playhead, project: project)
519 }
520
521 /// Warm the exact frames every nearby cut will need into the RAM frame
522 /// cache — clip heads for forward crossings, clip tails for reverse — so
523 /// the beat between "boundary crossed" and "player ready" shows the real
524 /// frame instead of a filmstrip thumb or a spinner. Self-deduping (the
525 /// cache remembers, decodes are backgrounded), so per-tick calls are cheap.
526 private func warmBoundaryFrames(ref: TrackRef, playhead: Double, project: ProjectModel) {
527 for clip in project.clips where clip.track == ref && clip.kind == .video {
528 guard abs(clip.start - playhead) <= Self.warmRadius
529 || abs(clip.end - playhead) <= Self.warmRadius,
530 let media = project.media(clip.mediaId), !media.isAudio else { continue }
531 if abs(clip.start - playhead) <= Self.warmRadius {
532 FrameCache.shared.warm(
533 mediaKey: media.cacheKey, at: clip.srcIn,
534 source: ctx.chunks.frameSource(media: media, sourceTime: clip.srcIn))
535 }
536 if abs(clip.end - playhead) <= Self.warmRadius {
537 let tail = max(clip.srcIn, clip.sourceTime(at: clip.end) - 0.05)
538 FrameCache.shared.warm(
539 mediaKey: media.cacheKey, at: tail,
540 source: ctx.chunks.frameSource(media: media, sourceTime: tail))
541 }
542 }
482 }543 }
483544
484 /// The next clip on this track (in time) whose media differs from `current`545 /// The next clip on this track (in time) whose media differs from `current`
...@@ -492,6 +553,16 @@ final class VideoTrackPlayer {...@@ -492,6 +553,16 @@ final class VideoTrackPlayer {
492 .min { $0.start < $1.start }553 .min { $0.start < $1.start }
493 }554 }
494555
556 /// The mirror for reverse playback: the previous clip (in time) whose media
557 /// differs — the cut a rewinding playhead will cross next.
558 private func prevDifferentClip(on ref: TrackRef, before t: Double,
559 current: Clip?) -> Clip? {
560 ctx.store.project.clips
561 .filter { $0.track == ref && $0.kind == .video && $0.end < t + 1e-6
562 && $0.mediaId != current?.mediaId }
563 .max { $0.end < $1.end }
564 }
565
495 func clear() {566 func clear() {
496 a.player.replaceCurrentItem(with: nil)567 a.player.replaceCurrentItem(with: nil)
497 b.player.replaceCurrentItem(with: nil)568 b.player.replaceCurrentItem(with: nil)
sequencer/Sources/Sequencer/SessionState.swift+13
...@@ -12,6 +12,10 @@ final class SessionState {...@@ -12,6 +12,10 @@ final class SessionState {
1212
13 var snapping = true { didSet { postViewOptions() } }13 var snapping = true { didSet { postViewOptions() } }
14 var showFilmstrips = true { didSet { postViewOptions() } }14 var showFilmstrips = true { didSet { postViewOptions() } }
15 /// Show the clover-recording transcript as synchronized captions over the
16 /// viewer (only ever visible when a `cam.mov` with a sibling `transcript.json`
17 /// is under the playhead).
18 var subtitlesEnabled = true { didSet { postViewOptions() } }
15 /// Vertical zoom: multiplies the base lane height for all tracks.19 /// Vertical zoom: multiplies the base lane height for all tracks.
16 var laneScale: CGFloat = 1 {20 var laneScale: CGFloat = 1 {
17 didSet {21 didSet {
...@@ -24,6 +28,11 @@ final class SessionState {...@@ -24,6 +28,11 @@ final class SessionState {
2428
25 var hiddenTracks: Set<TrackRef> = [] { didSet { postViewOptions() } }29 var hiddenTracks: Set<TrackRef> = [] { didSet { postViewOptions() } }
26 var focusedTracks: Set<TrackRef> = [] { didSet { postViewOptions() } }30 var focusedTracks: Set<TrackRef> = [] { didSet { postViewOptions() } }
31 /// The timeline span currently on screen (set by TimelineView as it
32 /// scrolls/zooms). The proxy builder treats it as a heat anchor — the
33 /// user scrubs inside what they can see. Not persisted; no notification
34 /// (read lazily by the chunk scheduler).
35 var visibleTimeRange: ClosedRange<Double>?
27 /// The Fusion comps band gets its own hide/focus.36 /// The Fusion comps band gets its own hide/focus.
28 var fusionHidden = false { didSet { postViewOptions() } }37 var fusionHidden = false { didSet { postViewOptions() } }
29 var fusionFocus = false { didSet { postViewOptions() } }38 var fusionFocus = false { didSet { postViewOptions() } }
...@@ -101,10 +110,12 @@ final class SessionState {...@@ -101,10 +110,12 @@ final class SessionState {
101 v.laneScale = Double(laneScale)110 v.laneScale = Double(laneScale)
102 v.snapping = snapping111 v.snapping = snapping
103 v.showFilmstrips = showFilmstrips112 v.showFilmstrips = showFilmstrips
113 v.subtitles = subtitlesEnabled
104 v.previewsOnLeft = previewsOnLeft114 v.previewsOnLeft = previewsOnLeft
105 v.priorityPane = priorityPane115 v.priorityPane = priorityPane
106 v.fusionHidden = fusionHidden116 v.fusionHidden = fusionHidden
107 v.fusionFocus = fusionFocus117 v.fusionFocus = fusionFocus
118 v.playhead = ctx.playback.playhead
108 return v119 return v
109 }120 }
110121
...@@ -117,10 +128,12 @@ final class SessionState {...@@ -117,10 +128,12 @@ final class SessionState {
117 laneScale = CGFloat(v.laneScale)128 laneScale = CGFloat(v.laneScale)
118 snapping = v.snapping129 snapping = v.snapping
119 showFilmstrips = v.showFilmstrips130 showFilmstrips = v.showFilmstrips
131 subtitlesEnabled = v.subtitles
120 previewsOnLeft = v.previewsOnLeft132 previewsOnLeft = v.previewsOnLeft
121 priorityPane = v.priorityPane133 priorityPane = v.priorityPane
122 fusionHidden = v.fusionHidden134 fusionHidden = v.fusionHidden
123 fusionFocus = v.fusionFocus135 fusionFocus = v.fusionFocus
136 if v.playhead > 0 { ctx.playback.seek(to: v.playhead) }
124 }137 }
125138
126 private func postViewOptions() {139 private func postViewOptions() {
sequencer/Sources/Sequencer/Store.swift+85-14
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import Foundation1import AppKit
22
3extension Notification.Name {3extension Notification.Name {
4 static let projectChanged = Notification.Name("projectChanged")4 static let projectChanged = Notification.Name("projectChanged")
...@@ -36,8 +36,18 @@ final class Store {...@@ -36,8 +36,18 @@ final class Store {
36 var playhead: Double36 var playhead: Double
37 }37 }
3838
39 private var undoStack: [Snapshot] = []39 /// One step on the undo timeline. Model edits and storyboard DRAWING edits
40 private var redoStack: [Snapshot] = []40 /// share this single stack so ⌘Z / ⌘⇧Z step through them in the exact order
41 /// they happened — regardless of which window is focused — and drawing is
42 /// redoable like everything else. (Raster pixels live in `BoardStore`, out of
43 /// the value-type model; the entry just carries the image to restore.)
44 private enum UndoEntry {
45 case model(Snapshot)
46 case raster(boardId: UUID, image: NSImage?) // nil image = board was blank
47 }
48
49 private var undoStack: [UndoEntry] = []
50 private var redoStack: [UndoEntry] = []
41 private var gestureBase: Snapshot?51 private var gestureBase: Snapshot?
4252
43 var canUndo: Bool { !undoStack.isEmpty || gestureBase != nil }53 var canUndo: Bool { !undoStack.isEmpty || gestureBase != nil }
...@@ -70,6 +80,7 @@ final class Store {...@@ -70,6 +80,7 @@ final class Store {
70 project = copy80 project = copy
71 pruneSelection()81 pruneSelection()
72 changed()82 changed()
83 noteEditLocus(before: before.model, after: copy)
73 }84 }
7485
75 /// Continuous-gesture mutations (drags): one undo entry for the whole86 /// Continuous-gesture mutations (drags): one undo entry for the whole
...@@ -97,24 +108,58 @@ final class Store {...@@ -97,24 +108,58 @@ final class Store {
97 /// base snapshot and would discard the gesture's changes.108 /// base snapshot and would discard the gesture's changes.
98 func endGesture(finalize: ((inout ProjectModel) -> Void)? = nil) {109 func endGesture(finalize: ((inout ProjectModel) -> Void)? = nil) {
99 guard let base = gestureBase else { return }110 guard let base = gestureBase else { return }
111 // Did the drag move anything on screen (mid-gesture posts drew it)?
112 let diverged = project != base.model
100 var copy = project113 var copy = project
101 finalize?(&copy)114 finalize?(&copy)
102 copy.normalizeStoryboards()115 copy.normalizeStoryboards()
103 // A drag that vacated a bottom lane leaves it as an ephemeral drop116 // A drag that vacated a bottom lane leaves it as an ephemeral drop
104 // target — collapse it (interior lanes and the clip's own lane stay).117 // target — collapse it (interior lanes and the clip's own lane stay).
105 copy.pruneTrailingEmptyTracks()118 copy.pruneTrailingEmptyTracks()
106 if copy != project {119 project = copy
107 project = copy
108 post(.projectChanged)
109 }
110 gestureBase = nil120 gestureBase = nil
121 // Post `.projectChanged` exactly once. A net change goes through
122 // `changed()` (undo + dirty + the post). A net-ZERO drag that still moved
123 // things mid-gesture (dragged out and back, ephemeral lane pruned) has no
124 // undo entry, but the view is drawing the mid-gesture state — refresh it.
125 // A no-op gesture (a click that never dragged) posts nothing.
111 if project != base.model {126 if project != base.model {
112 pushUndo(base)127 pushUndo(base)
128 noteEditLocus(before: base.model, after: project)
113 pruneSelection()129 pruneSelection()
114 changed()130 changed()
131 } else if diverged {
132 post(.projectChanged)
115 }133 }
116 }134 }
117135
136 /// Report where an edit landed on the timeline — the proxy builder keeps
137 /// chunks around recent edit sites warm (built early, evicted late), since
138 /// editors scrub and re-play around where they're cutting.
139 private func noteEditLocus(before: ProjectModel, after: ProjectModel) {
140 var old: [UUID: Clip] = [:]
141 for c in before.clips { old[c.id] = c }
142 var times: [Double] = []
143 func add(_ t: Double) {
144 guard times.count < 4,
145 !times.contains(where: { abs($0 - t) < 30 }) else { return }
146 times.append(t)
147 }
148 for c in after.clips where c.kind == .video && old[c.id] != c {
149 if times.count >= 4 { break }
150 guard let o = old[c.id] else { add(c.start); continue } // new clip
151 // Warm the EDGE that moved: a right-trim on a long clip should
152 // anchor at its out point, not the (possibly minutes-away) head.
153 let durChanged = abs(o.duration - c.duration) > 1e-9
154 if durChanged, abs(o.end - c.end) > 1e-9 { add(c.end) } // tail edit
155 if abs(o.srcIn - c.srcIn) > 1e-9 || abs(o.start - c.start) > 1e-9
156 || !durChanged {
157 add(c.start) // head edit, slip, move, or non-geometry change
158 }
159 }
160 ctx.chunks.noteEdits(times: times)
161 }
162
118 /// Live, non-undoable edit for a floating control that has no discrete163 /// Live, non-undoable edit for a floating control that has no discrete
119 /// start/end (the colour picker). Unlike a gesture it holds no open state,164 /// start/end (the colour picker). Unlike a gesture it holds no open state,
120 /// so timeline edits mid-preview can't trip the gesture precondition.165 /// so timeline edits mid-preview can't trip the gesture precondition.
...@@ -143,19 +188,45 @@ final class Store {...@@ -143,19 +188,45 @@ final class Store {
143188
144 func undo() {189 func undo() {
145 if gestureBase != nil { cancelGesture(); return }190 if gestureBase != nil { cancelGesture(); return }
146 guard let prev = undoStack.popLast() else { return }191 guard let entry = undoStack.popLast() else { return }
147 redoStack.append(snapshot())192 switch entry {
148 restore(prev)193 case .model(let snap):
194 redoStack.append(.model(snapshot()))
195 restore(snap)
196 case .raster(let boardId, let image):
197 // Swap: what's on screen now becomes the redo target; restore the
198 // stored (pre-edit) drawing.
199 redoStack.append(.raster(boardId: boardId, image: ctx.boards.rasterSnapshot(boardId)))
200 ctx.boards.applyRaster(image, boardId: boardId)
201 }
149 }202 }
150203
151 func redo() {204 func redo() {
152 guard let next = redoStack.popLast() else { return }205 guard let entry = redoStack.popLast() else { return }
153 undoStack.append(snapshot())206 switch entry {
154 restore(next)207 case .model(let snap):
208 undoStack.append(.model(snapshot()))
209 restore(snap)
210 case .raster(let boardId, let image):
211 undoStack.append(.raster(boardId: boardId, image: ctx.boards.rasterSnapshot(boardId)))
212 ctx.boards.applyRaster(image, boardId: boardId)
213 }
214 }
215
216 /// Register a storyboard drawing edit on the shared undo timeline — called by
217 /// `BoardStore.endStroke` after it commits a stroke. `before` is the drawing
218 /// as it stood BEFORE the stroke, so undo restores it.
219 func recordRasterEdit(boardId: UUID, before: NSImage?) {
220 undoStack.append(.raster(boardId: boardId, image: before))
221 if undoStack.count > 500 { undoStack.removeFirst() }
222 redoStack.removeAll()
223 // The stroke already marked the document dirty (via noteRasterChanged);
224 // this refreshes the Undo menu's enabled state.
225 post(.documentStateChanged)
155 }226 }
156227
157 private func pushUndo(_ snapshot: Snapshot) {228 private func pushUndo(_ snapshot: Snapshot) {
158 undoStack.append(snapshot)229 undoStack.append(.model(snapshot))
159 if undoStack.count > 500 { undoStack.removeFirst() }230 if undoStack.count > 500 { undoStack.removeFirst() }
160 redoStack.removeAll()231 redoStack.removeAll()
161 }232 }
sequencer/Sources/Sequencer/Storyboard.swift+18-30
...@@ -134,9 +134,9 @@ final class BoardStore {...@@ -134,9 +134,9 @@ final class BoardStore {
134 // Board coordinates everywhere: top-left origin, y down. The engine owns134 // Board coordinates everywhere: top-left origin, y down. The engine owns
135 // the y-flip into image space so callers never think about it.135 // the y-flip into image space so callers never think about it.
136 private var workingRasters: [UUID: NSImage] = [:]136 private var workingRasters: [UUID: NSImage] = [:]
137 private var strokeUndo: [UUID: [NSImage?]] = [:]137 /// Pre-stroke drawing captured at `beginStroke`, used by `endStroke` to push
138 /// Boards in the order strokes were committed (global ⌘Z routing).138 /// one undo entry onto the shared `Store` timeline.
139 private(set) var strokeHistory: [UUID] = []139 private var pendingStrokeBefore: [UUID: NSImage?] = [:]
140140
141 /// Raster to DISPLAY: the in-progress stroke image when one is active.141 /// Raster to DISPLAY: the in-progress stroke image when one is active.
142 func displayRaster(_ boardId: UUID) -> NSImage? {142 func displayRaster(_ boardId: UUID) -> NSImage? {
...@@ -153,10 +153,9 @@ final class BoardStore {...@@ -153,10 +153,9 @@ final class BoardStore {
153 }153 }
154154
155 func beginStroke(board: Board) {155 func beginStroke(board: Board) {
156 var stack = strokeUndo[board.id] ?? []156 // Remember the pre-stroke drawing so the commit can register one undo
157 stack.append(rasterImage(board.id)?.copy() as? NSImage)157 // step on the shared timeline.
158 if stack.count > 24 { stack.removeFirst() }158 pendingStrokeBefore[board.id] = rasterImage(board.id)?.copy() as? NSImage
159 strokeUndo[board.id] = stack
160 workingRasters[board.id] = (rasterImage(board.id)?.copy() as? NSImage)159 workingRasters[board.id] = (rasterImage(board.id)?.copy() as? NSImage)
161 ?? blankRaster(size: board.size)160 ?? blankRaster(size: board.size)
162 }161 }
...@@ -192,34 +191,23 @@ final class BoardStore {...@@ -192,34 +191,23 @@ final class BoardStore {
192 func endStroke(board: Board) {191 func endStroke(board: Board) {
193 guard let img = workingRasters.removeValue(forKey: board.id) else { return }192 guard let img = workingRasters.removeValue(forKey: board.id) else { return }
194 saveRaster(img, boardId: board.id)193 saveRaster(img, boardId: board.id)
195 strokeHistory.append(board.id)194 // Register the completed stroke on the shared undo timeline.
196 if strokeHistory.count > 48 { strokeHistory.removeFirst() }195 let before = pendingStrokeBefore.removeValue(forKey: board.id) ?? nil
197 NotificationCenter.default.post(name: .mediaStatusChanged, object: nil)196 ctx.store.recordRasterEdit(boardId: board.id, before: before)
197 NotificationCenter.default.post(name: .mediaStatusChanged, object: nil, userInfo: ["scene": true])
198 }198 }
199199
200 func canUndoStroke(_ boardId: UUID) -> Bool {200 /// A copy of the current drawing for the undo timeline (nil = blank board).
201 !(strokeUndo[boardId] ?? []).isEmpty201 func rasterSnapshot(_ boardId: UUID) -> NSImage? {
202 }202 rasterImage(boardId)?.copy() as? NSImage
203 var canUndoAnyStroke: Bool {
204 strokeHistory.last.map(canUndoStroke) ?? false
205 }203 }
206204
207 @discardableResult205 /// Restore a drawing captured by `rasterSnapshot` (undo/redo of a stroke),
208 func undoStroke(_ boardId: UUID) -> Bool {206 /// dropping any in-progress stroke and refreshing the editor + viewer.
209 guard var stack = strokeUndo[boardId], let prev = stack.popLast() else { return false }207 func applyRaster(_ image: NSImage?, boardId: UUID) {
210 strokeUndo[boardId] = stack
211 workingRasters.removeValue(forKey: boardId)208 workingRasters.removeValue(forKey: boardId)
212 saveRaster(prev, boardId: boardId)209 saveRaster(image, boardId: boardId)
213 if let i = strokeHistory.lastIndex(of: boardId) { strokeHistory.remove(at: i) }210 NotificationCenter.default.post(name: .projectChanged, object: nil)
214 NotificationCenter.default.post(name: .mediaStatusChanged, object: nil)
215 return true
216 }
217
218 /// Undo the most recent stroke on any board.
219 @discardableResult
220 func undoLastStroke() -> Bool {
221 guard let boardId = strokeHistory.last else { return false }
222 return undoStroke(boardId)
223 }211 }
224212
225 /// Wipe the drawing layer (undoable as one stroke).213 /// Wipe the drawing layer (undoable as one stroke).
sequencer/Sources/Sequencer/StoryboardEditor.swift-18
...@@ -132,15 +132,6 @@ final class StoryboardEditor: NSObject, NSWindowDelegate {...@@ -132,15 +132,6 @@ final class StoryboardEditor: NSObject, NSWindowDelegate {
132 private var colorWell: NSColorWell?132 private var colorWell: NSColorWell?
133133
134 var isKeyEditor: Bool { window != nil && NSApp.keyWindow === window }134 var isKeyEditor: Bool { window != nil && NSApp.keyWindow === window }
135 var canUndoRaster: Bool { isKeyEditor && (canvas?.canUndoRaster ?? false) }
136
137 /// Raster strokes undo in their own lane while the editor is key; shape
138 /// edits ride the global Store undo like everything else.
139 func undoRasterIfKey() -> Bool {
140 guard canUndoRaster, let canvas else { return false }
141 canvas.undoRaster()
142 return true
143 }
144135
145 /// Open on a panel belonging to `ctx`'s document. The single editor window136 /// Open on a panel belonging to `ctx`'s document. The single editor window
146 /// re-targets to whichever document asked for it.137 /// re-targets to whichever document asked for it.
...@@ -381,9 +372,6 @@ final class BoardCanvas: NSView {...@@ -381,9 +372,6 @@ final class BoardCanvas: NSView {
381 // Raster stroke state (pixels live in BoardStore's shared stroke engine)372 // Raster stroke state (pixels live in BoardStore's shared stroke engine)
382 private var strokeActive = false373 private var strokeActive = false
383 private var lastStrokePoint: CGPoint?374 private var lastStrokePoint: CGPoint?
384 var canUndoRaster: Bool {
385 board.map { boards.canUndoStroke($0.id) } ?? false
386 }
387375
388 // Shape gesture state376 // Shape gesture state
389 private enum ShapeDrag { case none, create, move, resize }377 private enum ShapeDrag { case none, create, move, resize }
...@@ -596,12 +584,6 @@ final class BoardCanvas: NSView {...@@ -596,12 +584,6 @@ final class BoardCanvas: NSView {
596584
597 // MARK: Raster strokes (BoardStore's engine does the pixel work)585 // MARK: Raster strokes (BoardStore's engine does the pixel work)
598586
599 func undoRaster() {
600 guard let board else { return }
601 boards.undoStroke(board.id)
602 needsDisplay = true
603 }
604
605 private func strokeSegment(from a: CGPoint, to b: CGPoint, pressure: CGFloat) {587 private func strokeSegment(from a: CGPoint, to b: CGPoint, pressure: CGFloat) {
606 guard let board, let width = tool.strokeWidth else { return }588 guard let board, let width = tool.strokeWidth else { return }
607 boards.strokeSegment(589 boards.strokeSegment(
sequencer/Sources/Sequencer/Theme.swift+23
...@@ -84,4 +84,27 @@ enum Theme {...@@ -84,4 +84,27 @@ enum Theme {
84 static var dragHint: NSColor {84 static var dragHint: NSColor {
85 pick(NSColor(calibratedWhite: 0.3, alpha: 1), NSColor(calibratedWhite: 0.55, alpha: 1))85 pick(NSColor(calibratedWhite: 0.3, alpha: 1), NSColor(calibratedWhite: 0.55, alpha: 1))
86 }86 }
87
88 // Optimization strip (the thin band under the ruler). Muted so a mostly
89 // red/yellow strip informs rather than alarms.
90 /// Proxy built at the full preview-quality target.
91 static var stripFull: NSColor {
92 pick(NSColor(calibratedHue: 0.36, saturation: 0.70, brightness: 0.62, alpha: 1),
93 NSColor(calibratedHue: 0.36, saturation: 0.65, brightness: 0.60, alpha: 1))
94 }
95 /// Proxy built below target — usable now, an upgrade is still queued.
96 static var stripUsable: NSColor {
97 pick(NSColor(calibratedHue: 0.36, saturation: 0.50, brightness: 0.42, alpha: 1),
98 NSColor(calibratedHue: 0.36, saturation: 0.40, brightness: 0.72, alpha: 1))
99 }
100 /// Building right now (or a rescue slice standing in).
101 static var stripBuilding: NSColor {
102 pick(NSColor(calibratedHue: 0.13, saturation: 0.80, brightness: 0.72, alpha: 1),
103 NSColor(calibratedHue: 0.13, saturation: 0.85, brightness: 0.80, alpha: 1))
104 }
105 /// No proxy on disk (never built, evicted, or failed).
106 static var stripMissing: NSColor {
107 pick(NSColor(calibratedHue: 0.01, saturation: 0.65, brightness: 0.48, alpha: 1),
108 NSColor(calibratedHue: 0.01, saturation: 0.55, brightness: 0.75, alpha: 1))
109 }
87}110}
sequencer/Sources/Sequencer/TimelineView.swift+1119-230
...@@ -7,8 +7,13 @@ enum DrawProf {...@@ -7,8 +7,13 @@ enum DrawProf {
7 static var acc: [String: Double] = [:]7 static var acc: [String: Double] = [:]
8 static var order: [String] = []8 static var order: [String] = []
9 static var thumbHits = 0 // filmstrip images actually blitted (cache hits)9 static var thumbHits = 0 // filmstrip images actually blitted (cache hits)
10 static var tileRenders = 0 // scene tiles rasterized (cache misses)
11 static var tileBlits = 0 // scene tiles blitted (cache hits + fresh)
10 @inline(__always) static func t<T>(_ label: String, _ body: () -> T) -> T {12 @inline(__always) static func t<T>(_ label: String, _ body: () -> T) -> T {
11 if !on { return body() }13 // Main-thread only: lanes rasterize in parallel, and racing the
14 // accumulator dictionaries would crash. Off-main work is simply
15 // not attributed (the enclosing main-thread section still is).
16 if !on || !Thread.isMainThread { return body() }
12 let t0 = DispatchTime.now().uptimeNanoseconds17 let t0 = DispatchTime.now().uptimeNanoseconds
13 let r = body()18 let r = body()
14 let dt = Double(DispatchTime.now().uptimeNanoseconds - t0) / 1e619 let dt = Double(DispatchTime.now().uptimeNanoseconds - t0) / 1e6
...@@ -55,10 +60,26 @@ final class TimelineView: NSView {...@@ -55,10 +60,26 @@ final class TimelineView: NSView {
55 // New document ⇒ new model; the scene cache must rebuild even though60 // New document ⇒ new model; the scene cache must rebuild even though
56 // no `.projectChanged` fires for the context swap itself.61 // no `.projectChanged` fires for the context swap itself.
57 sceneDirty = true62 sceneDirty = true
63 stripKey = nil
64 laneRefsCache = nil
65 flushTiles()
58 }66 }
59 }67 }
60 private var store: Store { ctx.store }68 private var store: Store { ctx.store }
61 private var project: ProjectModel { ctx.store.project }69 private var project: ProjectModel { ctx.store.project }
70
71 /// The model's `laneRefs` getter scans EVERY clip (`hasStoryboard`) —
72 /// O(clips). The draw path reaches it per clip drawn (`drawClip →
73 /// clipRect → laneRect`), which is quadratic on big projects: 98% CPU
74 /// redraw storms. Cache it; invalidated on `.projectChanged`/
75 /// `.viewOptionsChanged` (redraw()) and on document swap (ctx.didSet).
76 private var laneRefsCache: [TrackRef]?
77 private var laneRows: [TrackRef] {
78 if let rows = laneRefsCache { return rows }
79 let rows = project.laneRefs
80 laneRefsCache = rows
81 return rows
82 }
62 private var playback: PlaybackController { ctx.playback }83 private var playback: PlaybackController { ctx.playback }
63 private var comps: FusionComps { ctx.comps }84 private var comps: FusionComps { ctx.comps }
64 private var boards: BoardStore { ctx.boards }85 private var boards: BoardStore { ctx.boards }
...@@ -72,12 +93,21 @@ final class TimelineView: NSView {...@@ -72,12 +93,21 @@ final class TimelineView: NSView {
72 override init(frame: NSRect) {93 override init(frame: NSRect) {
73 super.init(frame: frame)94 super.init(frame: frame)
74 registerForDraggedTypes([.fileURL])95 registerForDraggedTypes([.fileURL])
75 for name: Notification.Name in [.projectChanged, .selectionChanged,96 // Only model/view-option changes invalidate the grouped-clip scene
76 .mediaStatusChanged, .viewOptionsChanged,97 // cache. Selection changes just refresh the link-mate cache, and
77 .compsChanged] {98 // media-status/comps changes (thumbnails landing, comps rescans) only
99 // need a repaint — none of them regroup or re-sort thousands of clips.
100 for name: Notification.Name in [.projectChanged, .viewOptionsChanged] {
78 NotificationCenter.default.addObserver(self, selector: #selector(redraw),101 NotificationCenter.default.addObserver(self, selector: #selector(redraw),
79 name: name, object: nil)102 name: name, object: nil)
80 }103 }
104 NotificationCenter.default.addObserver(self, selector: #selector(selectionRedraw),
105 name: .selectionChanged, object: nil)
106 NotificationCenter.default.addObserver(self, selector: #selector(mediaRedraw(_:)),
107 name: .mediaStatusChanged, object: nil)
108 // The comps band isn't tiled — a comps rescan is repaint-only.
109 NotificationCenter.default.addObserver(self, selector: #selector(repaintOnly),
110 name: .compsChanged, object: nil)
81 // Per-document: bound against the current (headless) ctx here, re-bound111 // Per-document: bound against the current (headless) ctx here, re-bound
82 // when a real ctx is injected (see `ctx.didSet`).112 // when a real ctx is injected (see `ctx.didSet`).
83 ctx.notify.addObserver(self, selector: #selector(playheadMoved),113 ctx.notify.addObserver(self, selector: #selector(playheadMoved),
...@@ -88,7 +118,37 @@ final class TimelineView: NSView {...@@ -88,7 +118,37 @@ final class TimelineView: NSView {
88118
89 required init?(coder: NSCoder) { fatalError() }119 required init?(coder: NSCoder) { fatalError() }
90120
91 @objc private func redraw() { sceneDirty = true; needsDisplay = true }121 @objc private func redraw() {
122 sceneDirty = true
123 stripKey = nil
124 laneRefsCache = nil
125 flushTiles()
126 needsDisplay = true
127 }
128
129 @objc private func repaintOnly() { needsDisplay = true }
130
131 /// `.mediaStatusChanged` is posted for two very different things: scene
132 /// pixels changing (a filmstrip/waveform/board raster landed — posts carry
133 /// `scene: true`) and proxy-chunk bookkeeping (status bar / viewer badges —
134 /// no timeline pixels). Only the former invalidates the tile cache; chunk
135 /// churn during playback must not force full scene re-renders.
136 @objc private func mediaRedraw(_ note: Notification) {
137 if note.userInfo?["scene"] != nil { flushTiles() }
138 stripKey = nil // chunk states changed — recompute the strip's runs
139 needsDisplay = true
140 }
141
142 @objc private func selectionRedraw() {
143 // Link-mates of the selection outline aqua; recompute just that set —
144 // a selection click must not regroup/re-sort the whole project.
145 if !sceneDirty {
146 linkedSelectionCache = project.expandLinks(store.selection)
147 .subtracting(store.selection)
148 }
149 flushTiles() // selection tint/borders are baked into the rasters
150 needsDisplay = true
151 }
92152
93 @objc private func playheadMoved() {153 @objc private func playheadMoved() {
94 // Auto-follow while playing.154 // Auto-follow while playing.
...@@ -127,24 +187,31 @@ final class TimelineView: NSView {...@@ -127,24 +187,31 @@ final class TimelineView: NSView {
127 }187 }
128 private var frameDur: Double { 1.0 / project.fps }188 private var frameDur: Double { 1.0 / project.fps }
129189
190 /// The optimization strip: a thin band under the ruler showing, per
191 /// timeline column, how optimized the media the visible tracks need there
192 /// is (see drawOptimizeStrip).
193 private let stripH: CGFloat = 5
130 private var fusionBandH: CGFloat { comps.visible ? 46 : 0 }194 private var fusionBandH: CGFloat { comps.visible ? 46 : 0 }
131 private var lanesTop: CGFloat { rulerH + fusionBandH }195 private var fusionTop: CGFloat { rulerH + stripH }
196 private var lanesTop: CGFloat { rulerH + stripH + fusionBandH }
132197
133 private func laneHeight(_ ref: TrackRef) -> CGFloat {198 private func laneHeight(_ ref: TrackRef) -> CGFloat {
134 max(24, baseLaneH * session.laneScale * (session.trackHeights[ref] ?? 1))199 // Device-pixel quantized so lane rasters (scene tiles) blit without
200 // resampling; the ≤ half-device-pixel rounding is imperceptible.
201 quantized(max(24, baseLaneH * session.laneScale * (session.trackHeights[ref] ?? 1)))
135 }202 }
136 private var defaultLaneH: CGFloat { max(24, baseLaneH * session.laneScale) }203 private var defaultLaneH: CGFloat { quantized(max(24, baseLaneH * session.laneScale)) }
137204
138 /// Total height of all lanes (for vertical scroll clamping).205 /// Total height of all lanes (for vertical scroll clamping).
139 private var lanesContentHeight: CGFloat {206 private var lanesContentHeight: CGFloat {
140 project.laneRefs.reduce(laneGap) { $0 + laneHeight($1) + laneGap }207 laneRows.reduce(laneGap) { $0 + laneHeight($1) + laneGap }
141 }208 }
142 private var maxScrollY: CGFloat {209 private var maxScrollY: CGFloat {
143 max(0, lanesContentHeight - (bounds.height - lanesTop) + defaultLaneH)210 max(0, lanesContentHeight - (bounds.height - lanesTop) + defaultLaneH)
144 }211 }
145212
146 private func laneRect(row: Int) -> NSRect {213 private func laneRect(row: Int) -> NSRect {
147 let rows = project.laneRefs214 let rows = laneRows
148 var y = lanesTop + laneGap - scrollY215 var y = lanesTop + laneGap - scrollY
149 for (i, ref) in rows.enumerated() {216 for (i, ref) in rows.enumerated() {
150 let h = laneHeight(ref)217 let h = laneHeight(ref)
...@@ -161,7 +228,7 @@ final class TimelineView: NSView {...@@ -161,7 +228,7 @@ final class TimelineView: NSView {
161 private func rowAt(y: CGFloat) -> Int? {228 private func rowAt(y: CGFloat) -> Int? {
162 guard y > lanesTop else { return nil }229 guard y > lanesTop else { return nil }
163 var yy = lanesTop + laneGap - scrollY230 var yy = lanesTop + laneGap - scrollY
164 let rows = project.laneRefs231 let rows = laneRows
165 for (i, ref) in rows.enumerated() {232 for (i, ref) in rows.enumerated() {
166 let h = laneHeight(ref)233 let h = laneHeight(ref)
167 if y < yy + h + laneGap { return i }234 if y < yy + h + laneGap { return i }
...@@ -173,7 +240,7 @@ final class TimelineView: NSView {...@@ -173,7 +240,7 @@ final class TimelineView: NSView {
173 /// Row whose bottom edge is under the cursor (for track-height resizing).240 /// Row whose bottom edge is under the cursor (for track-height resizing).
174 private func trackBoundaryAt(y: CGFloat) -> Int? {241 private func trackBoundaryAt(y: CGFloat) -> Int? {
175 guard y > lanesTop else { return nil }242 guard y > lanesTop else { return nil }
176 let rows = project.laneRefs243 let rows = laneRows
177 var yy = lanesTop + laneGap - scrollY244 var yy = lanesTop + laneGap - scrollY
178 for (i, ref) in rows.enumerated() {245 for (i, ref) in rows.enumerated() {
179 yy += laneHeight(ref)246 yy += laneHeight(ref)
...@@ -191,15 +258,17 @@ final class TimelineView: NSView {...@@ -191,15 +258,17 @@ final class TimelineView: NSView {
191258
192 /// The lane shown at a row, or nil past the last real lane (ghost rows).259 /// The lane shown at a row, or nil past the last real lane (ghost rows).
193 private func laneRef(row: Int) -> TrackRef? {260 private func laneRef(row: Int) -> TrackRef? {
194 let rows = project.laneRefs261 let rows = laneRows
195 return rows.indices.contains(row) ? rows[row] : nil262 return rows.indices.contains(row) ? rows[row] : nil
196 }263 }
197264
198 private func clipAt(point: NSPoint) -> (clip: Clip, row: Int)? {265 private func clipAt(point: NSPoint) -> (clip: Clip, row: Int)? {
199 guard let row = rowAt(y: point.y), let ref = laneRef(row: row) else { return nil }266 guard let row = rowAt(y: point.y), let ref = laneRef(row: row) else { return nil }
267 // The cached per-lane arrays are already start-sorted — no need to
268 // re-filter and re-sort the whole project per mouse event.
269 rebuildSceneIfNeeded()
200 // Later clips draw on top, so hit-test in reverse.270 // Later clips draw on top, so hit-test in reverse.
201 for clip in project.clips.filter({ $0.track == ref })271 for clip in (clipsByLane[ref] ?? []).reversed() {
202 .sorted(by: { $0.start < $1.start }).reversed() {
203 if clipRect(clip, row: row).contains(point) { return (clip, row) }272 if clipRect(clip, row: row).contains(point) { return (clip, row) }
204 }273 }
205 return nil274 return nil
...@@ -207,7 +276,8 @@ final class TimelineView: NSView {...@@ -207,7 +276,8 @@ final class TimelineView: NSView {
207276
208 private func overlapAt(point: NSPoint) -> ClipOverlap? {277 private func overlapAt(point: NSPoint) -> ClipOverlap? {
209 guard let row = rowAt(y: point.y), let ref = laneRef(row: row) else { return nil }278 guard let row = rowAt(y: point.y), let ref = laneRef(row: row) else { return nil }
210 for o in project.overlaps(on: ref) {279 rebuildSceneIfNeeded()
280 for o in overlapsByLane[ref] ?? [] {
211 let lane = laneRect(row: row)281 let lane = laneRect(row: row)
212 let r = NSRect(x: xFor(o.start), y: lane.minY,282 let r = NSRect(x: xFor(o.start), y: lane.minY,
213 width: max(2, xFor(o.end) - xFor(o.start)), height: lane.height)283 width: max(2, xFor(o.end) - xFor(o.start)), height: lane.height)
...@@ -228,6 +298,16 @@ final class TimelineView: NSView {...@@ -228,6 +298,16 @@ final class TimelineView: NSView {
228 // large project is now a redraw of cached geometry, not a full recompute.298 // large project is now a redraw of cached geometry, not a full recompute.
229 private var clipsByLane: [TrackRef: [Clip]] = [:]299 private var clipsByLane: [TrackRef: [Clip]] = [:]
230 private var overlapsByLane: [TrackRef: [ClipOverlap]] = [:]300 private var overlapsByLane: [TrackRef: [ClipOverlap]] = [:]
301 /// Ids of clips that are part of an overlap, per lane — hoisted out of
302 /// `drawLane`, which used to rebuild this Set per lane per frame.
303 private var overlapIdsByLane: [TrackRef: Set<UUID>] = [:]
304 /// Longest clip duration per lane — bounds the binary-searched draw/culling
305 /// window (a clip can start at most this far left of the view and still be
306 /// visible).
307 private var maxClipDurByLane: [TrackRef: Double] = [:]
308 /// `project.timelineDuration` scans every clip; cached here so per-frame
309 /// chrome (scrollbars, origin clamping) doesn't rescan 4k+ clips.
310 private var cachedTimelineDuration: Double = 0
231 private var mediaById: [UUID: MediaItem] = [:]311 private var mediaById: [UUID: MediaItem] = [:]
232 private var sceneDirty = true312 private var sceneDirty = true
233313
...@@ -239,6 +319,43 @@ final class TimelineView: NSView {...@@ -239,6 +319,43 @@ final class TimelineView: NSView {
239 .foregroundColor: NSColor(calibratedWhite: 0.92, alpha: 1),319 .foregroundColor: NSColor(calibratedWhite: 0.92, alpha: 1),
240 ]320 ]
241321
322 /// Clip titles as cached CTLines. `NSString.draw` runs a full CoreText
323 /// layout per call — ~200 visible labels re-laid-out every cold frame,
324 /// even though a multicam project has a handful of distinct names. Layout
325 /// once per distinct title, then `CTLineDraw` is a glyph blit. Capped:
326 /// titles are media names + panel names, a small closed set in practice.
327 private static var titleLineCache: [String: (line: CTLine, ascent: CGFloat)] = [:]
328 private static let titleLineLock = NSLock() // lanes rasterize in parallel
329 private static func titleLine(_ s: String) -> (line: CTLine, ascent: CGFloat) {
330 titleLineLock.lock()
331 defer { titleLineLock.unlock() }
332 if let c = titleLineCache[s] { return c }
333 if titleLineCache.count > 512 { titleLineCache.removeAll(keepingCapacity: true) }
334 let line = CTLineCreateWithAttributedString(
335 NSAttributedString(string: s, attributes: titleAttrs))
336 var ascent: CGFloat = 0
337 CTLineGetTypographicBounds(line, &ascent, nil, nil)
338 let entry = (line, ascent)
339 titleLineCache[s] = entry
340 return entry
341 }
342
343 /// Draw a cached title line with its top-left at `point` in the flipped
344 /// view space — positioned to match what `NSString.draw(at:)` produced.
345 private func drawTitle(_ s: String, at point: NSPoint) {
346 guard let cg = NSGraphicsContext.current?.cgContext else { return }
347 let (line, ascent) = Self.titleLine(s)
348 cg.saveGState()
349 // Flipped context: un-flip locally for glyph drawing. The box-top to
350 // baseline offset is the line's ascent, matching NSString.draw(at:).
351 cg.translateBy(x: point.x, y: point.y + ascent)
352 cg.scaleBy(x: 1, y: -1)
353 cg.textMatrix = .identity
354 cg.textPosition = .zero
355 CTLineDraw(line, cg)
356 cg.restoreGState()
357 }
358
242 /// SF Symbols, tinted and baked into flat bitmaps, cached by `key`. Building359 /// SF Symbols, tinted and baked into flat bitmaps, cached by `key`. Building
243 /// an SF Symbol and tinting it (`tinted` does a `lockFocus` composite) *per360 /// an SF Symbol and tinting it (`tinted` does a `lockFocus` composite) *per
244 /// draw* was the single biggest timeline draw cost: nearly every clip is361 /// draw* was the single biggest timeline draw cost: nearly every clip is
...@@ -246,9 +363,12 @@ final class TimelineView: NSView {...@@ -246,9 +363,12 @@ final class TimelineView: NSView {
246 /// the track-header buttons re-baked every frame too. Cached, drawing a badge363 /// the track-header buttons re-baked every frame too. Cached, drawing a badge
247 /// is a plain blit. Main-thread only (all drawing is).364 /// is a plain blit. Main-thread only (all drawing is).
248 private static var symbolCache: [String: NSImage] = [:]365 private static var symbolCache: [String: NSImage] = [:]
366 private static let symbolCacheLock = NSLock() // lanes rasterize in parallel
249 static func bakedSymbol(_ name: String, pointSize: CGFloat = 0,367 static func bakedSymbol(_ name: String, pointSize: CGFloat = 0,
250 weight: NSFont.Weight = .regular,368 weight: NSFont.Weight = .regular,
251 tint: NSColor, key: String) -> NSImage? {369 tint: NSColor, key: String) -> NSImage? {
370 symbolCacheLock.lock()
371 defer { symbolCacheLock.unlock() }
252 if let img = symbolCache[key] { return img }372 if let img = symbolCache[key] { return img }
253 var base = NSImage(systemSymbolName: name, accessibilityDescription: name)373 var base = NSImage(systemSymbolName: name, accessibilityDescription: name)
254 if pointSize > 0 {374 if pointSize > 0 {
...@@ -281,25 +401,28 @@ final class TimelineView: NSView {...@@ -281,25 +401,28 @@ final class TimelineView: NSView {
281 bakedSymbol("link", tint: .white, key: "badge.link")401 bakedSymbol("link", tint: .white, key: "badge.link")
282 }402 }
283403
284 /// True while the user is actively scrolling/zooming (cleared ~120 ms after404 /// Index range of a lane's (start-sorted) clips that can intersect the
285 /// the last scroll event, which triggers one full-detail redraw). On its own405 /// time window — binary-searched so a frame's work is proportional to
286 /// it changes nothing — clips draw at full detail while scrolling. It only406 /// what's on screen, not to how far into a 10-hour project it sits. The
287 /// gates the `lightScroll` fallback: a *very dense* frame (see407 /// lower bound backs off by the lane's longest clip (any clip starting
288 /// `denseScrollClips`) sheds the heavy filmstrip/waveform pass mid-scroll so408 /// earlier than that has necessarily ended before `left`); the upper bound
289 /// a pathological timeline can't stall, restoring detail the moment it settles.409 /// is the first start past `right`. Callers still skip the odd short clip
290 private var isScrolling = false410 /// inside the backoff span with an `end < left` check.
291 private var scrollSettleToken = 0411 private func visibleIndexRange(_ clips: [Clip], lane: TrackRef,
292412 left: Double, right: Double) -> Range<Int> {
293 /// Mark a scroll/zoom in progress and schedule the settle redraw.413 let backoff = left - (maxClipDurByLane[lane] ?? 0)
294 private func noteScrolling() {414 var lo = 0, hi = clips.count
295 isScrolling = true415 while lo < hi { // first index with start >= backoff
296 scrollSettleToken &+= 1416 let mid = (lo + hi) / 2
297 let token = scrollSettleToken417 if clips[mid].start < backoff { lo = mid + 1 } else { hi = mid }
298 DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { [weak self] in418 }
299 guard let self, self.scrollSettleToken == token else { return }419 let first = lo
300 self.isScrolling = false420 hi = clips.count
301 self.needsDisplay = true421 while lo < hi { // first index with start > right
422 let mid = (lo + hi) / 2
423 if clips[mid].start <= right { lo = mid + 1 } else { hi = mid }
302 }424 }
425 return first..<lo
303 }426 }
304427
305 private func rebuildSceneIfNeeded() {428 private func rebuildSceneIfNeeded() {
...@@ -315,6 +438,13 @@ final class TimelineView: NSView {...@@ -315,6 +438,13 @@ final class TimelineView: NSView {
315 // `overlaps()` (no arg) computes every lane in one pass; grouping by438 // `overlaps()` (no arg) computes every lane in one pass; grouping by
316 // track yields the same per-lane arrays `overlaps(on:)` returned.439 // track yields the same per-lane arrays `overlaps(on:)` returned.
317 overlapsByLane = Dictionary(grouping: project.overlaps(), by: \.track)440 overlapsByLane = Dictionary(grouping: project.overlaps(), by: \.track)
441 overlapIdsByLane = overlapsByLane.mapValues {
442 Set($0.flatMap { [$0.a.id, $0.b.id] })
443 }
444 maxClipDurByLane = clipsByLane.mapValues {
445 $0.map(\.duration).max() ?? 0
446 }
447 cachedTimelineDuration = project.timelineDuration
318 mediaById = Dictionary(project.media.map { ($0.id, $0) },448 mediaById = Dictionary(project.media.map { ($0.id, $0) },
319 uniquingKeysWith: { a, _ in a })449 uniquingKeysWith: { a, _ in a })
320 // Per-lane colours: the track colour and its black-blended body/strip450 // Per-lane colours: the track colour and its black-blended body/strip
...@@ -322,7 +452,7 @@ final class TimelineView: NSView {...@@ -322,7 +452,7 @@ final class TimelineView: NSView {
322 // alloc + two colour-space `blended()` conversions) for every clip every452 // alloc + two colour-space `blended()` conversions) for every clip every
323 // frame. Compute them once per lane here.453 // frame. Compute them once per lane here.
324 laneColorCache = [:]454 laneColorCache = [:]
325 for ref in project.laneRefs {455 for ref in laneRows {
326 let base = trackColor(ref)456 let base = trackColor(ref)
327 laneColorCache[ref] = LaneColors(457 laneColorCache[ref] = LaneColors(
328 base: base,458 base: base,
...@@ -357,39 +487,308 @@ final class TimelineView: NSView {...@@ -357,39 +487,308 @@ final class TimelineView: NSView {
357 let left = originSecond487 let left = originSecond
358 let right = originSecond + Double(bounds.width - headerW) / pxPerSecond488 let right = originSecond + Double(bounds.width - headerW) / pxPerSecond
359 var n = 0489 var n = 0
360 for ref in project.laneRefs {490 for ref in laneRows {
361 for c in clipsByLane[ref] ?? [] {491 let clips = clipsByLane[ref] ?? []
362 if c.start > right { break }492 for i in visibleIndexRange(clips, lane: ref, left: left, right: right)
363 if c.end < left { continue }493 where clips[i].end >= left {
364 n += 1494 n += 1
365 }495 }
366 }496 }
367 return n497 return n
368 }498 }
369499
370 /// While actively scrolling a *very dense* frame, shed the heavy filmstrip/500 /// Below this on-screen width a clip physically cannot show its corner
371 /// waveform pass so the pan stays fluid; it returns the instant the scroll501 /// radius, title strip text, badges, or a meaningful filmstrip/waveform
372 /// settles. Ordinary-density views keep full detail — thumbnails, waveforms502 /// column — so `drawClip` takes a flat-rect fast path that produces the
373 /// and labels — even mid-scroll, so nothing visibly "drops out" in normal use.503 /// same pixels for a fraction of the cost. Detail is a function of what's
374 private var lightScroll = false504 /// resolvable at the current zoom, never of whether the user is scrolling:
375 private static let denseScrollClips = 350505 /// this replaces the old `lightScroll` mode, which shed thumbnails and
506 /// labels mid-pan (visibly) yet saved almost nothing — the real per-clip
507 /// cost was the path/clip-state chrome that ran for sub-pixel clips.
508 private static let lodMinWidth: CGFloat = 3
509
510 // MARK: - Scene tile cache
511 //
512 // The scene (lane clips: bodies, filmstrips, waveforms, strips, labels,
513 // badges, selection tint) is rasterized into per-lane, 512-pt-wide tiles in
514 // TIMELINE space — a tile's x axis is (t − sliceStart)·pxPerSecond, which
515 // is independent of the pan origin. Panning and the 60 Hz playhead redraw
516 // therefore cost a handful of blits plus the (cheap) chrome, instead of
517 // re-rendering thousands of clips. Anything that changes scene pixels —
518 // model edits, selection, thumbnails landing, theme, zoom, lane heights —
519 // flushes; tiles rebuild lazily, visible-first, at direct-render cost.
520 // Active edit gestures bypass tiles entirely (direct draw), so a drag never
521 // thrashes the cache mid-gesture.
522
523 private struct TileKey: Hashable {
524 let ref: TrackRef
525 let slice: Int
526 }
527 /// Everything a tile's pixels depend on besides the model/selection (those
528 /// flush via notifications). A mismatch flushes the whole cache.
529 private struct TileParams: Equatable {
530 var pps: Double
531 var scale: CGFloat
532 var light: Bool
533 var heights: [TrackRef: CGFloat]
534 var showFilmstrips: Bool
535 }
536 private var tiles: [TileKey: CGImage] = [:]
537 private var tileUse: [TileKey: Int] = [:]
538 private var tileTick = 0
539 private var tileParams: TileParams?
540 private static let tileW: CGFloat = 512
541 /// ~96 tiles ≈ a few viewports of 2× lane strips (~100 MB worst case).
542 private static let maxTiles = 96
543 /// Cap on tile rasterizations per frame. A cold viewport (fresh zoom, big
544 /// jump) draws the un-cached slices directly this frame — same pixels,
545 /// direct cost — and fills the cache over the next few frames instead of
546 /// paying ~25 bitmap allocations in one frame.
547 private static let tileRendersPerFrame = 6
548 private var tileRenderBudget = 0
549 private var tileFillPending = false
550 private var lastDrawOrigin = 0.0
551 private var lastDrawPps = 0.0
552 /// Escape hatch for A/B measurement and debugging: SEQ_NOTILES=1 forces
553 /// the direct (Stage-1) render path. Mutable so --perftest can A/B both
554 /// paths inside one process (immune to thermal drift between runs).
555 static var tilesDisabled =
556 ProcessInfo.processInfo.environment["SEQ_NOTILES"] == "1"
557
558 private func flushTiles() {
559 tiles.removeAll(keepingCapacity: true)
560 tileUse.removeAll(keepingCapacity: true)
561 }
562
563 /// Device-pixel scale of the surface actually being drawn into, captured
564 /// from the context CTM at the top of `draw`. Falling back to the window's
565 /// backing scale is only right when they agree — an offscreen 1× target
566 /// (like --perftest's bitmap) on a 2× machine would otherwise get 2× tiles
567 /// downsampled on every blit.
568 private var renderScale: CGFloat = 2
569
570 private var backingScale: CGFloat {
571 window?.backingScaleFactor ?? NSScreen.main?.backingScaleFactor ?? 2
572 }
573
574 /// Quantize a length/offset to the device-pixel grid. Tiles are blitted at
575 /// integral device pixels; quantizing the shared geometry (origin, scroll,
576 /// lane heights) keeps the tile and direct paths on the same sub-pixel
577 /// phase, so their output is pixel-identical and blits never resample.
578 private func quantized(_ v: CGFloat) -> CGFloat {
579 (v * renderScale).rounded() / renderScale
580 }
581
582 private func evictTilesIfNeeded() {
583 guard tiles.count > Self.maxTiles else { return }
584 for (key, _) in tileUse.sorted(by: { $0.value < $1.value })
585 .prefix(tiles.count - Self.maxTiles) {
586 tiles[key] = nil
587 tileUse[key] = nil
588 }
589 }
590
591 /// Blit the lane's visible tiles. Missing tiles are rendered up to the
592 /// per-frame budget; past it, their span draws directly (identical pixels)
593 /// and a follow-up display pass finishes filling the cache.
594 private func blitLaneTiles(ref: TrackRef, lane: NSRect) {
595 guard let cg = NSGraphicsContext.current?.cgContext else { return }
596 let sliceDur = Double(Self.tileW) / pxPerSecond
597 let leftSec = secondsFor(headerW)
598 let rightSec = secondsFor(bounds.width)
599 var slice = Int(floor(leftSec / sliceDur))
600 let last = Int(floor(rightSec / sliceDur))
601 while slice <= last {
602 let x = xFor(Double(slice) * sliceDur)
603 if let img = tileImage(ref: ref, lane: lane, slice: slice, sliceDur: sliceDur) {
604 // CGContext blit (NSImage.draw pays rep-matching/colorspace
605 // overhead per call). Local flip: CGImages draw bottom-up.
606 cg.saveGState()
607 cg.translateBy(x: x, y: lane.maxY)
608 cg.scaleBy(x: 1, y: -1)
609 cg.draw(img, in: CGRect(x: 0, y: 0, width: Self.tileW,
610 height: lane.height))
611 cg.restoreGState()
612 if DrawProf.on { DrawProf.tileBlits += 1 }
613 } else {
614 // Over budget this frame: draw the slice's span directly.
615 NSGraphicsContext.current?.saveGraphicsState()
616 NSRect(x: x, y: lane.minY, width: Self.tileW,
617 height: lane.height).clip()
618 drawLaneClips(ref: ref, lane: lane, cullX0: x, cullX1: x + Self.tileW)
619 NSGraphicsContext.current?.restoreGraphicsState()
620 scheduleTileFill()
621 }
622 slice += 1
623 }
624 }
625
626 /// One coalesced follow-up display pass to keep rasterizing missed tiles
627 /// after a budget-limited frame.
628 private func scheduleTileFill() {
629 guard !tileFillPending else { return }
630 tileFillPending = true
631 DispatchQueue.main.async { [weak self] in
632 guard let self else { return }
633 self.tileFillPending = false
634 self.needsDisplay = true
635 }
636 }
637
638 /// The cached tile for (lane, slice), rendering it via the SAME
639 /// `drawLaneClips` code the direct path uses — pixel-equivalent by
640 /// construction. The context is translated so the slice's start lands at
641 /// x = 0 and the lane's top at y = 0; both offsets cancel the pan origin,
642 /// which is what makes the raster reusable across scroll positions.
643 private func tileImage(ref: TrackRef, lane: NSRect, slice: Int,
644 sliceDur: Double) -> CGImage? {
645 let key = TileKey(ref: ref, slice: slice)
646 tileTick += 1
647 tileUse[key] = tileTick
648 if let img = tiles[key] { return img }
649 guard tileRenderBudget > 0 else { return nil }
650 tileRenderBudget -= 1
651
652 let scale = renderScale
653 let pxW = Int((Self.tileW * scale).rounded())
654 let pxH = Int((lane.height * scale).rounded())
655 // Native Quartz raster format (BGRA premultiplied, little-endian) in
656 // the window's own colorspace: blits are then straight memory copies.
657 // An NSBitmapImageRep here (RGBA, deviceRGB) costs a per-blit swizzle
658 // + colorspace conversion — ~1.5 ms per tile, wiping out the caching.
659 let space = window?.colorSpace?.cgColorSpace
660 ?? CGColorSpace(name: CGColorSpace.sRGB)!
661 guard pxW > 0, pxH > 0,
662 let cg = CGContext(
663 data: nil, width: pxW, height: pxH, bitsPerComponent: 8,
664 bytesPerRow: 0, space: space,
665 bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue
666 | CGBitmapInfo.byteOrder32Little.rawValue) else { return nil }
667
668 NSGraphicsContext.saveGraphicsState()
669 // Map: device pixels → points, unflipped → flipped (the view draws
670 // top-down), view space → tile space. Wrapped in a `flipped: true`
671 // NSGraphicsContext so text and images orient correctly.
672 cg.scaleBy(x: scale, y: scale)
673 cg.translateBy(x: 0, y: lane.height)
674 cg.scaleBy(x: 1, y: -1)
675 let x0 = xFor(Double(slice) * sliceDur)
676 cg.translateBy(x: -x0, y: -lane.minY)
677 NSGraphicsContext.current = NSGraphicsContext(cgContext: cg, flipped: true)
678 drawLaneClips(ref: ref, lane: lane, cullX0: x0, cullX1: x0 + Self.tileW)
679 NSGraphicsContext.restoreGraphicsState()
680
681 guard let img = cg.makeImage() else { return nil }
682 tiles[key] = img
683 evictTilesIfNeeded()
684 if DrawProf.on { DrawProf.tileRenders += 1 }
685 return img
686 }
687
688 /// Live draw-time telemetry, on when the app is launched with
689 /// SEQ_DRAWPROF=1 in the environment (run the binary directly with stderr
690 /// to a file — NSLog is invisible under `open`). Prints one line per
691 /// second: draws that second and mean/max ms per draw. Purely additive so
692 /// GUI perf work can be judged against real numbers, like --perftest.
693 private static let liveProf = ProcessInfo.processInfo.environment["SEQ_DRAWPROF"] == "1"
694 private var profWindowStart = CACurrentMediaTime()
695 private var profFrames = 0
696 private var profTotalMs = 0.0
697 private var profMaxMs = 0.0
376698
377 override func draw(_ dirtyRect: NSRect) {699 override func draw(_ dirtyRect: NSRect) {
700 let profT0 = Self.liveProf ? CACurrentMediaTime() : 0
701 defer {
702 if Self.liveProf {
703 let ms = (CACurrentMediaTime() - profT0) * 1000
704 profFrames += 1
705 profTotalMs += ms
706 profMaxMs = max(profMaxMs, ms)
707 let now = CACurrentMediaTime()
708 if now - profWindowStart >= 1.0 {
709 FileHandle.standardError.write(Data(String(format:
710 "[drawprof] %d draws, avg %.2f ms, max %.2f ms\n",
711 profFrames, profTotalMs / Double(profFrames),
712 profMaxMs).utf8))
713 profWindowStart = now
714 profFrames = 0
715 profTotalMs = 0
716 profMaxMs = 0
717 }
718 }
719 }
378 Theme.timelineBg.setFill()720 Theme.timelineBg.setFill()
379 bounds.fill()721 bounds.fill()
380722
723 // Publish the on-screen time span — a heat anchor for the proxy
724 // builder (the user scrubs inside what they can see). Cheap enough
725 // to set every frame; read lazily by the chunk scheduler.
726 let visibleSpan = Double(bounds.width - headerW) / pxPerSecond
727 ctx.session.visibleTimeRange = originSecond...(originSecond + max(1, visibleSpan))
728
381 DrawProf.t("scene") { rebuildSceneIfNeeded() }729 DrawProf.t("scene") { rebuildSceneIfNeeded() }
382 lightScroll = isScrolling && visibleClipCount() > Self.denseScrollClips730 DrawProf.t("cull") {
383 scrollY = min(scrollY, maxScrollY)731 // Match the tile raster scale to the surface being drawn into.
384 let rows = project.laneRefs732 if let ctm = NSGraphicsContext.current?.cgContext
733 .userSpaceToDeviceSpaceTransform {
734 let s = abs(ctm.a)
735 if s > 0.1, s != renderScale {
736 renderScale = s
737 flushTiles()
738 }
739 }
740 scrollY = quantized(min(scrollY, maxScrollY))
741 // Snap the pan origin to the device-pixel grid (≤ half a device
742 // pixel, imperceptible) so tile blits land on integral pixels and
743 // the tile and direct paths share one sub-pixel phase.
744 let q = pxPerSecond * Double(renderScale)
745 originSecond = (originSecond * q).rounded() / q
746 }
747 // Route the frame: warm frames (rest, pan, playback) blit the tile
748 // cache; cold frames — a zoom in flight (every tile invalid), a
749 // teleport jump (a whole viewport of new slices), or a live edit
750 // gesture (model changes per event) — rasterize all lanes in parallel
751 // instead, and the cache refills once things settle.
752 let editing = store.gestureBaseModel != nil || drag.mode == .box
753 let jumped = abs(originSecond - lastDrawOrigin) * pxPerSecond
754 > Double(Self.tileW)
755 let zoomed = pxPerSecond != lastDrawPps
756 lastDrawOrigin = originSecond
757 lastDrawPps = pxPerSecond
758 let tiled = !Self.tilesDisabled && !editing && !zoomed && !jumped
759 tileRenderBudget = Self.tileRendersPerFrame
760 if tiled {
761 let params = TileParams(
762 pps: pxPerSecond, scale: renderScale, light: Theme.light,
763 heights: Dictionary(uniqueKeysWithValues:
764 laneRows.map { ($0, laneHeight($0)) }),
765 showFilmstrips: session.showFilmstrips)
766 if params != tileParams {
767 flushTiles()
768 tileParams = params
769 }
770 } else if zoomed || jumped {
771 // The gesture invalidated the cache; warm it back up as soon as
772 // the stream of cold frames stops.
773 flushTiles()
774 tileParams = nil
775 scheduleTileFill()
776 }
777 let rows = laneRows
385 DrawProf.t("lanes") {778 DrawProf.t("lanes") {
386 for row in 0..<rows.count {779 if tiled {
387 drawLane(row: row, ref: rows[row])780 for row in 0..<rows.count {
781 drawLane(row: row, ref: rows[row], tiled: true)
782 }
783 } else {
784 drawLanesDirectParallel(rows: rows)
388 }785 }
389 }786 }
390 drawDragHintLanes()787 DrawProf.t("hints") {
391 drawFileDropPreview()788 drawDragHintLanes()
392 drawFusionBand()789 drawFileDropPreview()
790 }
791 DrawProf.t("fusion") { drawFusionBand() }
393 // Opaque header column so clips never show behind the buttons.792 // Opaque header column so clips never show behind the buttons.
394 Theme.timelineBg.setFill()793 Theme.timelineBg.setFill()
395 NSRect(x: 0, y: rulerH, width: headerW, height: bounds.height - rulerH).fill()794 NSRect(x: 0, y: rulerH, width: headerW, height: bounds.height - rulerH).fill()
...@@ -399,13 +798,16 @@ final class TimelineView: NSView {...@@ -399,13 +798,16 @@ final class TimelineView: NSView {
399 drawTrackHeader(row: row, ref: rows[row], lane: laneRect(row: row))798 drawTrackHeader(row: row, ref: rows[row], lane: laneRect(row: row))
400 }799 }
401 }800 }
402 drawRuler()801 DrawProf.t("ruler") { drawRuler() }
403 drawInOut()802 DrawProf.t("strip") { drawOptimizeStrip() }
404 drawMarkers()803 DrawProf.t("overlays") {
405 drawSnapIndicator()804 drawInOut()
406 drawBoxSelect()805 drawMarkers()
407 drawPlayhead()806 drawSnapIndicator()
408 drawScrollbars()807 drawBoxSelect()
808 drawPlayhead()
809 }
810 DrawProf.t("scrollbars") { drawScrollbars() }
409 }811 }
410812
411 // MARK: - Scroll/zoom bars813 // MARK: - Scroll/zoom bars
...@@ -427,7 +829,8 @@ final class TimelineView: NSView {...@@ -427,7 +829,8 @@ final class TimelineView: NSView {
427 private func hDomain() -> (lo: Double, hi: Double) {829 private func hDomain() -> (lo: Double, hi: Double) {
428 let viewSec = Double(bounds.width - headerW) / pxPerSecond830 let viewSec = Double(bounds.width - headerW) / pxPerSecond
429 let lo = min(0, originSecond)831 let lo = min(0, originSecond)
430 let hi = max(project.timelineDuration + 10, originSecond + viewSec)832 rebuildSceneIfNeeded()
833 let hi = max(cachedTimelineDuration + 10, originSecond + viewSec)
431 return (lo, hi)834 return (lo, hi)
432 }835 }
433836
...@@ -491,33 +894,30 @@ final class TimelineView: NSView {...@@ -491,33 +894,30 @@ final class TimelineView: NSView {
491 return nil894 return nil
492 }895 }
493896
494 private func drawLane(row: Int, ref: TrackRef) {897 private func drawLane(row: Int, ref: TrackRef, tiled: Bool) {
495 let lane = laneRect(row: row)898 let lane = laneRect(row: row)
496 guard lane.minY < bounds.maxY, lane.maxY > lanesTop else { return }899 guard lane.minY < bounds.maxY, lane.maxY > lanesTop else { return }
497 (ref == .storyboard ? Theme.storyboardLaneBg : Theme.laneBg).setFill()900 (ref == .storyboard ? Theme.storyboardLaneBg : Theme.laneBg).setFill()
498 NSBezierPath(roundedRect: lane, xRadius: 4, yRadius: 4).fill()901 NSBezierPath(roundedRect: lane, xRadius: 4, yRadius: 4).fill()
499902
500 let overlaps = overlapsByLane[ref] ?? []903 if tiled {
501 let overlappingIds = Set(overlaps.flatMap { [$0.a.id, $0.b.id] })904 blitLaneTiles(ref: ref, lane: lane)
502905 } else {
503 // Horizontal culling: clips are sorted by start, so once one starts past906 drawLaneClips(ref: ref, lane: lane,
504 // the right edge every later clip does too — stop. Clips ending before907 cullX0: headerW, cullX1: bounds.width)
505 // the left edge are skipped. Keeps the loop proportional to what's
506 // on-screen, not to the whole (possibly huge) lane.
507 let leftSec = originSecond
508 let rightSec = originSecond + Double(bounds.width - headerW) / pxPerSecond
509 let colors = laneColors(ref)
510 for clip in clipsByLane[ref] ?? [] {
511 if clip.start > rightSec { break }
512 if clip.end < leftSec { continue }
513 drawClip(clip, row: row, ref: ref, colors: colors,
514 overlapping: overlappingIds.contains(clip.id))
515 }908 }
909 drawLaneDecorations(ref: ref, lane: lane)
910 }
911
912 /// Pinned labels + overlap bands — main-thread, over blits or direct clips.
913 private func drawLaneDecorations(ref: TrackRef, lane: NSRect) {
914 drawPinnedLabels(ref: ref, lane: lane)
516915
517 // Bright red overlap ranges on top of the clip bodies.916 // Bright red overlap ranges on top of the clip bodies.
518 for o in overlaps {917 for o in overlapsByLane[ref] ?? [] {
519 let r = NSRect(x: xFor(o.start), y: lane.minY + 1,918 let r = NSRect(x: xFor(o.start), y: lane.minY + 1,
520 width: max(2, xFor(o.end) - xFor(o.start)), height: lane.height - 2)919 width: max(2, xFor(o.end) - xFor(o.start)), height: lane.height - 2)
920 guard r.maxX > headerW, r.minX < bounds.width else { continue }
521 NSColor.systemRed.withAlphaComponent(0.40).setFill()921 NSColor.systemRed.withAlphaComponent(0.40).setFill()
522 r.fill()922 r.fill()
523 NSColor.systemRed.setStroke()923 NSColor.systemRed.setStroke()
...@@ -527,6 +927,123 @@ final class TimelineView: NSView {...@@ -527,6 +927,123 @@ final class TimelineView: NSView {
527 }927 }
528 }928 }
529929
930 // MARK: - Parallel direct rendering (cold frames)
931
932 /// Persistent per-lane raster buffers for cold frames; recreated on size/
933 /// scale change. ~7 MB per lane at 2× on a wide window — the price of
934 /// rasterizing a zoom gesture on every core instead of one.
935 private var laneBufs: [TrackRef: CGContext] = [:]
936
937 /// A cold frame (zoom in flight, teleport jump, live edit gesture) must
938 /// re-render every visible lane from scratch — but lanes are independent,
939 /// so rasterize them CONCURRENTLY into per-lane buffers with the exact
940 /// same `drawLaneClips` code and composite on main. Same rasterizer, same
941 /// pixels; wall-clock is the slowest lane, not the sum. The storyboard
942 /// lane draws serially on main (BoardStore's raster cache isn't locked).
943 private func drawLanesDirectParallel(rows: [TrackRef]) {
944 var work: [(ref: TrackRef, lane: NSRect)] = []
945 for (row, ref) in rows.enumerated() {
946 let lane = laneRect(row: row)
947 guard lane.minY < bounds.maxY, lane.maxY > lanesTop else { continue }
948 (ref == .storyboard ? Theme.storyboardLaneBg : Theme.laneBg).setFill()
949 NSBezierPath(roundedRect: lane, xRadius: 4, yRadius: 4).fill()
950 work.append((ref, lane))
951 }
952 guard !work.isEmpty, let mainCG = NSGraphicsContext.current?.cgContext
953 else { return }
954
955 let scale = renderScale
956 let pxW = Int((bounds.width * scale).rounded())
957 let space = window?.colorSpace?.cgColorSpace
958 ?? CGColorSpace(name: CGColorSpace.sRGB)!
959 let ctxs: [CGContext?] = work.map { item in
960 if item.ref == .storyboard { return nil }
961 let pxH = Int((item.lane.height * scale).rounded())
962 if let c = laneBufs[item.ref], c.width == pxW, c.height == pxH {
963 return c
964 }
965 let c = CGContext(
966 data: nil, width: pxW, height: pxH, bitsPerComponent: 8,
967 bytesPerRow: 0, space: space,
968 bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue
969 | CGBitmapInfo.byteOrder32Little.rawValue)
970 laneBufs[item.ref] = c
971 return c
972 }
973
974 DispatchQueue.concurrentPerform(iterations: work.count) { i in
975 guard let cg = ctxs[i] else { return } // storyboard → main below
976 let (ref, lane) = work[i]
977 autoreleasepool {
978 cg.clear(CGRect(x: 0, y: 0, width: CGFloat(cg.width),
979 height: CGFloat(cg.height)))
980 cg.saveGState()
981 cg.scaleBy(x: scale, y: scale)
982 cg.translateBy(x: 0, y: lane.height)
983 cg.scaleBy(x: 1, y: -1)
984 cg.translateBy(x: 0, y: -lane.minY)
985 // NSGraphicsContext.current is thread-local — but
986 // concurrentPerform runs one of these iterations ON the
987 // calling (main) thread, so the view's own context must be
988 // restored, not nilled.
989 let prev = NSGraphicsContext.current
990 NSGraphicsContext.current = NSGraphicsContext(cgContext: cg, flipped: true)
991 drawLaneClips(ref: ref, lane: lane,
992 cullX0: headerW, cullX1: bounds.width)
993 NSGraphicsContext.current = prev
994 cg.restoreGState()
995 }
996 }
997
998 for (i, item) in work.enumerated() {
999 if let cg = ctxs[i] {
1000 if let img = cg.makeImage() {
1001 mainCG.saveGState()
1002 mainCG.translateBy(x: 0, y: item.lane.maxY)
1003 mainCG.scaleBy(x: 1, y: -1)
1004 mainCG.draw(img, in: CGRect(x: 0, y: 0, width: bounds.width,
1005 height: item.lane.height))
1006 mainCG.restoreGState()
1007 }
1008 } else {
1009 drawLaneClips(ref: item.ref, lane: item.lane,
1010 cullX0: headerW, cullX1: bounds.width)
1011 }
1012 drawLaneDecorations(ref: item.ref, lane: item.lane)
1013 }
1014 }
1015
1016 /// One lane's clips into the current context, culled and clamped to the
1017 /// window [cullX0, cullX1] (view coordinates). The window is the view for
1018 /// direct drawing and the tile's span (± bleed) when rendering a tile —
1019 /// the drawing itself is identical, so the two paths are pixel-equivalent.
1020 private func drawLaneClips(ref: TrackRef, lane: NSRect,
1021 cullX0: CGFloat, cullX1: CGFloat) {
1022 let overlappingIds = overlapIdsByLane[ref] ?? []
1023 // Bleed: fade handles / shot dividers / borders paint a few points
1024 // past a clip's rect, so a clip just outside the window still owns
1025 // pixels inside it.
1026 let bleed: CGFloat = 8
1027 let leftSec = secondsFor(cullX0 - bleed)
1028 let rightSec = secondsFor(cullX1 + bleed)
1029 let colors = laneColors(ref)
1030 let clips = clipsByLane[ref] ?? []
1031 // A storyboard panel's left divider depends on whether an earlier panel
1032 // exists on the lane; the array is start-sorted, so any predecessor
1033 // with a strictly smaller start means "has previous".
1034 let range = visibleIndexRange(clips, lane: ref, left: leftSec, right: rightSec)
1035 for i in range {
1036 let clip = clips[i]
1037 if clip.end < leftSec { continue }
1038 let hasPrevPanel = ref == .storyboard && i > 0
1039 && clips[0].start < clip.start - 1e-6
1040 drawClip(clip, lane: lane, ref: ref, colors: colors,
1041 overlapping: overlappingIds.contains(clip.id),
1042 hasPrevPanel: hasPrevPanel,
1043 cullX0: cullX0 - bleed, cullX1: cullX1 + bleed)
1044 }
1045 }
1046
530 /// Circular SF-Symbol button in the header column (hide preview / focus).1047 /// Circular SF-Symbol button in the header column (hide preview / focus).
531 private func drawHeaderButton(_ symbolName: String, centerY: CGFloat, on: Bool) {1048 private func drawHeaderButton(_ symbolName: String, centerY: CGFloat, on: Bool) {
532 drawHeaderButton(symbolName, in: NSRect(x: headerW / 2 - 8.5, y: centerY - 8.5,1049 drawHeaderButton(symbolName, in: NSRect(x: headerW / 2 - 8.5, y: centerY - 8.5,
...@@ -637,7 +1154,7 @@ final class TimelineView: NSView {...@@ -637,7 +1154,7 @@ final class TimelineView: NSView {
6371154
638 private func drawFusionHeader() {1155 private func drawFusionHeader() {
639 guard fusionBandH > 0 else { return }1156 guard fusionBandH > 0 else { return }
640 let band = NSRect(x: 0, y: rulerH, width: headerW, height: fusionBandH)1157 let band = NSRect(x: 0, y: fusionTop, width: headerW, height: fusionBandH)
641 FusionComps.yellow.withAlphaComponent(session.fusionHidden ? 0.25 : 0.6).setFill()1158 FusionComps.yellow.withAlphaComponent(session.fusionHidden ? 0.25 : 0.6).setFill()
642 NSBezierPath(roundedRect: band.insetBy(dx: 2, dy: 2), xRadius: 3, yRadius: 3).fill()1159 NSBezierPath(roundedRect: band.insetBy(dx: 2, dy: 2), xRadius: 3, yRadius: 3).fill()
643 let attrs: [NSAttributedString.Key: Any] = [1160 let attrs: [NSAttributedString.Key: Any] = [
...@@ -656,7 +1173,7 @@ final class TimelineView: NSView {...@@ -656,7 +1173,7 @@ final class TimelineView: NSView {
656 // become tracks. No resident placeholder lane. (Dropped FILES get their1173 // become tracks. No resident placeholder lane. (Dropped FILES get their
657 // own landing preview in drawFileDropPreview.)1174 // own landing preview in drawFileDropPreview.)
658 var rows: [Int] = []1175 var rows: [Int] = []
659 let count = project.laneRefs.count1176 let count = laneRows.count
660 if drag.mode == .move, let row = dragHintRow, row >= count {1177 if drag.mode == .move, let row = dragHintRow, row >= count {
661 rows = Array(count...row)1178 rows = Array(count...row)
662 }1179 }
...@@ -672,46 +1189,113 @@ final class TimelineView: NSView {...@@ -672,46 +1189,113 @@ final class TimelineView: NSView {
672 }1189 }
673 }1190 }
6741191
675 private func drawClip(_ clip: Clip, row: Int, ref: TrackRef, colors: LaneColors,1192 private func drawClip(_ clip: Clip, lane: NSRect, ref: TrackRef, colors: LaneColors,
676 overlapping: Bool) {1193 overlapping: Bool, hasPrevPanel: Bool,
677 let rect = clipRect(clip, row: row)1194 cullX0: CGFloat, cullX1: CGFloat) {
678 guard rect.maxX > headerW, rect.minX < bounds.width else { return }1195 let x0 = xFor(clip.start), x1 = xFor(clip.end)
1196 let rect = NSRect(x: x0, y: lane.minY, width: max(2, x1 - x0), height: lane.height)
1197 guard rect.maxX > cullX0, rect.minX < cullX1 else { return }
679 let media = clip.mediaId.flatMap { mediaById[$0] }1198 let media = clip.mediaId.flatMap { mediaById[$0] }
680 let color = colors.base1199 let color = colors.base
681 let selected = store.selection.contains(clip.id)1200 let selected = store.selection.contains(clip.id)
1201 let linkedSel = !selected && linkedSelectionCache.contains(clip.id)
6821202
683 // Storyboard panels tile edge-to-edge (they're gapless) so the track1203 // Storyboard panels tile edge-to-edge (they're gapless) so the track
684 // reads as one continuous filmstrip — square corners, no per-panel1204 // reads as one continuous filmstrip — square corners, no per-panel
685 // card, dividers drawn between shots below.1205 // card, dividers drawn between shots below.
686 let storyboard = clip.kind == .storyboard1206 let storyboard = clip.kind == .storyboard
1207
1208 func bodyFillColor() -> NSColor {
1209 switch clip.kind {
1210 case .storyboard: NSColor(calibratedWhite: 0.88, alpha: 1)
1211 case .audio: colors.audioBody
1212 case .video: media == nil ? NSColor(calibratedWhite: 0.25, alpha: 1)
1213 : colors.videoBody
1214 }
1215 }
1216
1217 // ---- Fast path: a clip too narrow to resolve any detail. ----
1218 // Below `lodMinWidth` the rounded corners, strip text, badges and
1219 // filmstrip/waveform columns are physically invisible, so flat rect
1220 // fills produce the same picture without the bezier/clip-state chrome
1221 // that made dense zooms cost hundreds of ms. Everything that still
1222 // reads at this size is kept: body + strip colors, the audio center
1223 // line and fade handles (the texture of dense audio lanes), status
1224 // tint/borders, storyboard dividers.
1225 if rect.width < Self.lodMinWidth {
1226 bodyFillColor().setFill()
1227 rect.fill()
1228 colors.strip.setFill()
1229 NSRect(x: rect.minX, y: rect.minY, width: rect.width, height: 13).fill()
1230 if clip.kind == .audio {
1231 color.withAlphaComponent(0.35).setFill()
1232 NSRect(x: rect.minX, y: rect.midY + 6, width: rect.width, height: 1).fill()
1233 // Fade-handle slivers: the full path clips the 7 px handle
1234 // ovals to the clip body, so at this width only a couple of
1235 // white columns survive — the speckle texture of a dense audio
1236 // lane. Fill the same intersection directly.
1237 (selected ? NSColor.white : NSColor(calibratedWhite: 0.85, alpha: 0.9)).setFill()
1238 let hy = rect.minY + 13.5
1239 for x in [xFor(clip.start + clip.fadeIn), xFor(clip.end - clip.fadeOut)] {
1240 let sliver = NSRect(x: x - 3.5, y: hy, width: 7, height: 7)
1241 .intersection(rect)
1242 if !sliver.isEmpty { sliver.fill() }
1243 }
1244 }
1245 // Status tint only: at this width the border stroke's inset path is
1246 // degenerate (sub-zero width) and renders nothing today, and the
1247 // red overlap band is painted at lane level. The tint is what reads.
1248 if selected || linkedSel {
1249 (selected ? NSColor.controlAccentColor : NSColor.systemCyan)
1250 .withAlphaComponent(selected ? 0.34 : 0.20).setFill()
1251 rect.fill()
1252 }
1253 if storyboard { drawShotDivider(clip, rect: rect, hasPrev: hasPrevPanel) }
1254 return
1255 }
1256
1257 // A zoomed-in clip's rect can be literally millions of points wide;
1258 // building paths and image draws at that size costs real time even
1259 // though almost all of it is clipped away. Clamp the card geometry to
1260 // the view plus a margin that keeps the rounded corners and border
1261 // strokes of the clamped edges safely offscreen — the pixels inside
1262 // the view are identical. (The filmstrip keeps the TRUE rect: its
1263 // thumbnail tiling is phase-anchored to the clip's real left edge, and
1264 // it already culls to the visible span internally.)
1265 let clampMargin: CGFloat = 12
1266 let cardRect: NSRect = {
1267 let cx0 = max(rect.minX, cullX0 - clampMargin)
1268 let cx1 = min(rect.maxX, cullX1 + clampMargin)
1269 return NSRect(x: cx0, y: rect.minY, width: cx1 - cx0, height: rect.height)
1270 }()
1271
687 let bodyRect = storyboard1272 let bodyRect = storyboard
688 ? NSRect(x: rect.minX, y: rect.minY + 0.5, width: rect.width, height: rect.height - 1)1273 ? NSRect(x: cardRect.minX, y: cardRect.minY + 0.5,
689 : rect.insetBy(dx: 0.5, dy: 0.5)1274 width: cardRect.width, height: cardRect.height - 1)
1275 : cardRect.insetBy(dx: 0.5, dy: 0.5)
690 let bodyRadius: CGFloat = storyboard ? 0 : 31276 let bodyRadius: CGFloat = storyboard ? 0 : 3
691 let body = NSBezierPath(roundedRect: bodyRect, xRadius: bodyRadius, yRadius: bodyRadius)1277 let body = DrawProf.t("clip.body") {
692 switch clip.kind {1278 let p = NSBezierPath(roundedRect: bodyRect, xRadius: bodyRadius,
693 case .storyboard:1279 yRadius: bodyRadius)
694 NSColor(calibratedWhite: 0.88, alpha: 1).setFill()1280 bodyFillColor().setFill()
695 case .audio:1281 p.fill()
696 colors.audioBody.setFill()1282 return p
697 case .video:1283 }
698 (media == nil ? NSColor(calibratedWhite: 0.25, alpha: 1) : colors.videoBody).setFill()1284
699 }1285 DrawProf.t("clip.content") {
700 body.fill()
701
702 // Full detail (thumbnails, waveforms, labels) normally — including while
703 // scrolling. Only a very dense frame mid-scroll drops to bodies + strip +
704 // border (see `lightScroll`), and only until the pan settles.
705 let detail = !lightScroll
706
707 if detail {
708 NSGraphicsContext.current?.saveGraphicsState()1286 NSGraphicsContext.current?.saveGraphicsState()
709 body.addClip()1287 body.addClip()
710 switch clip.kind {1288 switch clip.kind {
711 case .video:1289 case .video:
712 if let media, session.showFilmstrips { drawFilmstrip(clip, media: media, rect: rect) }1290 if let media, session.showFilmstrips {
1291 drawFilmstrip(clip, media: media, rect: rect,
1292 cullX0: cullX0, cullX1: cullX1)
1293 }
713 case .audio:1294 case .audio:
714 if let media { drawWaveform(clip, media: media, rect: rect, color: color) }1295 if let media {
1296 drawWaveform(clip, media: media, rect: rect, color: color,
1297 cullX0: cullX0, cullX1: cullX1)
1298 }
715 drawFades(clip, rect: rect, selected: selected)1299 drawFades(clip, rect: rect, selected: selected)
716 case .storyboard:1300 case .storyboard:
717 drawBoardThumb(clip, rect: rect)1301 drawBoardThumb(clip, rect: rect)
...@@ -719,23 +1303,22 @@ final class TimelineView: NSView {...@@ -719,23 +1303,22 @@ final class TimelineView: NSView {
719 NSGraphicsContext.current?.restoreGraphicsState()1303 NSGraphicsContext.current?.restoreGraphicsState()
720 }1304 }
7211305
722 let linkedSel = !selected && linkedSelectionCache.contains(clip.id)
723
724 // Title strip1306 // Title strip
725 var title = media?.displayName1307 let title = clipTitle(clip, media: media)
726 ?? (clip.kind == .storyboard1308 // The label is drawn only when the clip is wide enough to read; a clip
727 ? (panelNamesCache[clip.id] ?? "Panel") : "missing media")1309 // narrower than this shows no legible text anyway.
728 if clip.kind != .storyboard && media == nil { title = "⚠︎ " + title }1310 let showLabel = rect.width >= 22
729 if clip.kind == .audio { title = "♪ " + title }1311 DrawProf.t("clip.strip") {
730 // The label is drawn only at rest and only when the clip is wide enough1312 NSGraphicsContext.current?.saveGraphicsState()
731 // to read; a clip narrower than this shows no legible text anyway.1313 body.addClip()
732 let showLabel = detail && rect.width >= 22
733 if detail { NSGraphicsContext.current?.saveGraphicsState(); body.addClip() }
734 colors.strip.setFill()1314 colors.strip.setFill()
735 NSRect(x: rect.minX, y: rect.minY, width: rect.width, height: 13).fill()1315 NSRect(x: cardRect.minX, y: cardRect.minY, width: cardRect.width, height: 13).fill()
736 if showLabel {1316 if showLabel {
737 title.draw(at: NSPoint(x: max(rect.minX, headerW) + 5, y: rect.minY + 1),1317 // Unpinned: the label sits at the clip's true left edge. A clip
738 withAttributes: Self.titleAttrs)1318 // scrolled off the left edge gets its label re-pinned to the view
1319 // edge by `drawPinnedLabels` — an overlay pass, so the pinning
1320 // never gets baked into a cached tile.
1321 drawTitle(title, at: NSPoint(x: rect.minX + 5, y: rect.minY + 1))
7391322
740 var badgeX = rect.maxX - 161323 var badgeX = rect.maxX - 16
741 if clip.speed != 1 {1324 if clip.speed != 1 {
...@@ -759,16 +1342,19 @@ final class TimelineView: NSView {...@@ -759,16 +1342,19 @@ final class TimelineView: NSView {
759 from: .zero, operation: .sourceOver, fraction: 0.7)1342 from: .zero, operation: .sourceOver, fraction: 0.7)
760 }1343 }
761 }1344 }
762 if detail { NSGraphicsContext.current?.restoreGraphicsState() }1345 NSGraphicsContext.current?.restoreGraphicsState()
1346 }
7631347
1348 DrawProf.t("clip.chrome") {
764 // Selection reads as a full-card tint, not just an outline. Link-mates1349 // Selection reads as a full-card tint, not just an outline. Link-mates
765 // of the selection (they act selected) tint aqua.1350 // of the selection (they act selected) tint aqua.
766 if selected || linkedSel {1351 if selected || linkedSel {
767 if detail { NSGraphicsContext.current?.saveGraphicsState(); body.addClip() }1352 NSGraphicsContext.current?.saveGraphicsState()
1353 body.addClip()
768 (selected ? NSColor.controlAccentColor : NSColor.systemCyan)1354 (selected ? NSColor.controlAccentColor : NSColor.systemCyan)
769 .withAlphaComponent(selected ? 0.34 : 0.20).setFill()1355 .withAlphaComponent(selected ? 0.34 : 0.20).setFill()
770 (detail ? rect : bodyRect).fill()1356 cardRect.fill()
771 if detail { NSGraphicsContext.current?.restoreGraphicsState() }1357 NSGraphicsContext.current?.restoreGraphicsState()
772 }1358 }
7731359
774 // Border LAST, on top of the tint: overlap = red, selection = accent1360 // Border LAST, on top of the tint: overlap = red, selection = accent
...@@ -777,7 +1363,7 @@ final class TimelineView: NSView {...@@ -777,7 +1363,7 @@ final class TimelineView: NSView {
777 // outline (selected / linked / overlapping).1363 // outline (selected / linked / overlapping).
778 if !storyboard || selected || linkedSel || overlapping {1364 if !storyboard || selected || linkedSel || overlapping {
779 let radius: CGFloat = storyboard ? 0 : 31365 let radius: CGFloat = storyboard ? 0 : 3
780 let border = NSBezierPath(roundedRect: rect.insetBy(dx: 1.25, dy: 1.25),1366 let border = NSBezierPath(roundedRect: cardRect.insetBy(dx: 1.25, dy: 1.25),
781 xRadius: radius, yRadius: radius)1367 xRadius: radius, yRadius: radius)
782 border.lineWidth = selected ? 3.5 : linkedSel ? 3 : (overlapping ? 2 : 1.5)1368 border.lineWidth = selected ? 3.5 : linkedSel ? 3 : (overlapping ? 2 : 1.5)
783 (selected ? Theme.selection1369 (selected ? Theme.selection
...@@ -786,42 +1372,141 @@ final class TimelineView: NSView {...@@ -786,42 +1372,141 @@ final class TimelineView: NSView {
786 border.stroke()1372 border.stroke()
787 }1373 }
7881374
789 // Shot divider: an opaque line sitting ON the boundary between two1375 if storyboard { drawShotDivider(clip, rect: rect, hasPrev: hasPrevPanel) }
790 // storyboard panels (they tile gaplessly). A new shot gets a bold1376 }
791 // orange bar; frames within a shot get a thin neutral line. The first1377 }
792 // panel of the track has no divider on its left.1378
793 if storyboard {1379 private func clipTitle(_ clip: Clip, media: MediaItem?) -> String {
794 let hasPrev = project.clips.contains {1380 var title = media?.displayName
795 $0.id != clip.id && $0.kind == .storyboard1381 ?? (clip.kind == .storyboard
796 && $0.track == clip.track && $0.start < clip.start - 1e-61382 ? (panelNamesCache[clip.id] ?? "Panel") : "missing media")
1383 if clip.kind != .storyboard && media == nil { title = "⚠︎ " + title }
1384 if clip.kind == .audio { title = "♪ " + title }
1385 return title
1386 }
1387
1388 /// Re-pin the title of any clip whose left edge is scrolled offscreen: the
1389 /// label follows the view edge (as it always has), but the pinning is an
1390 /// overlay so it is never baked into a scene tile. The patch repaints the
1391 /// clip's own strip pixels under the new label position (covering the
1392 /// baked, unpinned label's tail), then restrokes the border segment it
1393 /// covered — output matches the old single-pass pinned render.
1394 private func drawPinnedLabels(ref: TrackRef, lane: NSRect) {
1395 let leftSec = secondsFor(headerW)
1396 let colors = laneColors(ref)
1397 let clips = clipsByLane[ref] ?? []
1398 // Only clips STRADDLING the left edge qualify; binary search the
1399 // candidate window instead of scanning every clip left of the view.
1400 for i in visibleIndexRange(clips, lane: ref, left: leftSec, right: leftSec) {
1401 let clip = clips[i]
1402 guard clip.start < leftSec, clip.end > leftSec else { continue }
1403 let x0 = xFor(clip.start), x1 = xFor(clip.end)
1404 let rect = NSRect(x: x0, y: lane.minY, width: max(2, x1 - x0),
1405 height: lane.height)
1406 guard rect.width >= 22 else { continue }
1407 let media = clip.mediaId.flatMap { mediaById[$0] }
1408 let title = clipTitle(clip, media: media)
1409 let selected = store.selection.contains(clip.id)
1410 let linkedSel = !selected && linkedSelectionCache.contains(clip.id)
1411 let overlapping = (overlapIdsByLane[ref] ?? []).contains(clip.id)
1412
1413 let labelW = title.size(withAttributes: Self.titleAttrs).width
1414 let patch = NSRect(x: headerW, y: rect.minY,
1415 width: min(labelW + 12, rect.maxX - headerW), height: 13)
1416 let storyboard = clip.kind == .storyboard
1417 let cx1 = min(rect.maxX, bounds.width + 12)
1418 let cardRect = NSRect(x: max(rect.minX, headerW - 12), y: rect.minY,
1419 width: cx1 - max(rect.minX, headerW - 12),
1420 height: rect.height)
1421 let bodyRect = storyboard
1422 ? NSRect(x: cardRect.minX, y: cardRect.minY + 0.5,
1423 width: cardRect.width, height: cardRect.height - 1)
1424 : cardRect.insetBy(dx: 0.5, dy: 0.5)
1425 let radius: CGFloat = storyboard ? 0 : 3
1426 let body = NSBezierPath(roundedRect: bodyRect, xRadius: radius, yRadius: radius)
1427
1428 NSGraphicsContext.current?.saveGraphicsState()
1429 NSRect(x: patch.minX, y: patch.minY, width: patch.width,
1430 height: patch.height).clip()
1431 body.addClip()
1432 // Rebuild the strip composite: body color under the 0.85-alpha strip.
1433 switch clip.kind {
1434 case .storyboard: NSColor(calibratedWhite: 0.88, alpha: 1).setFill()
1435 case .audio: colors.audioBody.setFill()
1436 case .video: (media == nil ? NSColor(calibratedWhite: 0.25, alpha: 1)
1437 : colors.videoBody).setFill()
797 }1438 }
798 if hasPrev {1439 patch.fill()
799 if clip.newShot {1440 colors.strip.setFill()
800 NSColor.systemOrange.setFill()1441 patch.fill()
801 NSRect(x: rect.minX - 1.5, y: rect.minY, width: 3, height: rect.height).fill()1442 drawTitle(title, at: NSPoint(x: headerW + 5, y: rect.minY + 1))
802 } else {1443 if selected || linkedSel {
803 NSColor(calibratedWhite: Theme.light ? 0.45 : 0.30, alpha: 1).setFill()1444 (selected ? NSColor.controlAccentColor : NSColor.systemCyan)
804 NSRect(x: rect.minX - 0.5, y: rect.minY, width: 1, height: rect.height).fill()1445 .withAlphaComponent(selected ? 0.34 : 0.20).setFill()
805 }1446 patch.fill()
1447 }
1448 NSGraphicsContext.current?.restoreGraphicsState()
1449
1450 // Restroke the border segment the patch painted over — clipped to
1451 // the patch only (drawClip strokes the border unclipped, so a
1452 // body clip here would shave its outer antialiasing).
1453 if !storyboard || selected || linkedSel || overlapping {
1454 NSGraphicsContext.current?.saveGraphicsState()
1455 patch.insetBy(dx: 0, dy: -1).clip()
1456 let border = NSBezierPath(
1457 roundedRect: cardRect.insetBy(dx: 1.25, dy: 1.25),
1458 xRadius: radius, yRadius: radius)
1459 border.lineWidth = selected ? 3.5 : linkedSel ? 3 : (overlapping ? 2 : 1.5)
1460 (selected ? Theme.selection
1461 : linkedSel ? NSColor.systemCyan
1462 : overlapping ? NSColor.systemRed : colors.base).setStroke()
1463 border.stroke()
1464 NSGraphicsContext.current?.restoreGraphicsState()
806 }1465 }
807 }1466 }
808 }1467 }
8091468
810 private func drawFilmstrip(_ clip: Clip, media: MediaItem, rect: NSRect) {1469 /// Shot divider: an opaque line sitting ON the boundary between two
1470 /// storyboard panels (they tile gaplessly). A new shot gets a bold
1471 /// orange bar; frames within a shot get a thin neutral line. The first
1472 /// panel of the track has no divider on its left.
1473 private func drawShotDivider(_ clip: Clip, rect: NSRect, hasPrev: Bool) {
1474 guard hasPrev else { return }
1475 if clip.newShot {
1476 NSColor.systemOrange.setFill()
1477 NSRect(x: rect.minX - 1.5, y: rect.minY, width: 3, height: rect.height).fill()
1478 } else {
1479 NSColor(calibratedWhite: Theme.light ? 0.45 : 0.30, alpha: 1).setFill()
1480 NSRect(x: rect.minX - 0.5, y: rect.minY, width: 1, height: rect.height).fill()
1481 }
1482 }
1483
1484 private func drawFilmstrip(_ clip: Clip, media: MediaItem, rect: NSRect,
1485 cullX0: CGFloat, cullX1: CGFloat) {
811 let thumbH = rect.height - 141486 let thumbH = rect.height - 14
812 guard thumbH > 6 else { return }1487 guard thumbH > 6 else { return }
813 let mediaAspect = media.width > 0 && media.height > 01488 let mediaAspect = media.width > 0 && media.height > 0
814 ? CGFloat(media.width) / CGFloat(media.height) : 16.0 / 9.01489 ? CGFloat(media.width) / CGFloat(media.height) : 16.0 / 9.0
815 let thumbW = thumbH * mediaAspect1490 let thumbW = thumbH * mediaAspect
816 let visX0 = max(rect.minX, headerW), visX1 = min(rect.maxX, bounds.width)1491 // Thumb tiling stays phase-anchored to the clip's TRUE left edge (so
1492 // thumbnails never slide as the window changes); the window only culls.
1493 let visX0 = max(rect.minX, cullX0), visX1 = min(rect.maxX, cullX1)
1494 guard let cg = NSGraphicsContext.current?.cgContext else { return }
817 var x = rect.minX + floor((visX0 - rect.minX) / thumbW) * thumbW1495 var x = rect.minX + floor((visX0 - rect.minX) / thumbW) * thumbW
818 while x < visX1 {1496 while x < visX1 {
819 let tlSec = secondsFor(x + thumbW / 2)1497 let tlSec = secondsFor(x + thumbW / 2)
820 let srcSec = clip.sourceTime(at: tlSec)1498 let srcSec = clip.sourceTime(at: tlSec)
821 if let img = MediaPipeline.shared.filmstripImage(for: media, at: max(0, srcSec)) {1499 if let img = MediaPipeline.shared.filmstripImage(for: media, at: max(0, srcSec)) {
822 if DrawProf.on { DrawProf.thumbHits += 1 }1500 if DrawProf.on { DrawProf.thumbHits += 1 }
823 img.draw(in: NSRect(x: x, y: rect.minY + 14, width: thumbW, height: thumbH),1501 // CGContext blit with a local flip (CGImages draw bottom-up;
824 from: .zero, operation: .sourceOver, fraction: 0.9)1502 // the view is flipped). The images are pre-converted to the
1503 // display's format, so this is a plain copy.
1504 cg.saveGState()
1505 cg.setAlpha(0.9)
1506 cg.translateBy(x: x, y: rect.minY + 14 + thumbH)
1507 cg.scaleBy(x: 1, y: -1)
1508 cg.draw(img, in: CGRect(x: 0, y: 0, width: thumbW, height: thumbH))
1509 cg.restoreGState()
825 }1510 }
826 x += thumbW1511 x += thumbW
827 }1512 }
...@@ -829,20 +1514,36 @@ final class TimelineView: NSView {...@@ -829,20 +1514,36 @@ final class TimelineView: NSView {
829 rect.fill()1514 rect.fill()
830 }1515 }
8311516
832 private func drawWaveform(_ clip: Clip, media: MediaItem, rect: NSRect, color: NSColor) {1517 private func drawWaveform(_ clip: Clip, media: MediaItem, rect: NSRect, color: NSColor,
1518 cullX0: CGFloat, cullX1: CGFloat) {
833 // Center line1519 // Center line
834 color.withAlphaComponent(0.35).setFill()1520 color.withAlphaComponent(0.35).setFill()
835 NSRect(x: rect.minX, y: rect.midY + 6, width: rect.width, height: 1).fill()1521 NSRect(x: rect.minX, y: rect.midY + 6, width: rect.width, height: 1).fill()
836 guard let img = MediaPipeline.shared.waveformImage(for: media), media.duration > 01522 guard let img = MediaPipeline.shared.waveformImage(for: media), media.duration > 0
837 else { return }1523 else { return }
838 let imgW = img.size.width1524 // Draw only the visible span — a zoomed-in clip's full rect can be
1525 // millions of points wide, and image draws at that size are slow even
1526 // though it's all clipped away. CGImage has no source-rect draw, so
1527 // map the FULL image through the source→dest transform and clip to
1528 // the visible span: pixel-identical to a fractional source rect
1529 // (cropping(to:) would round to whole source pixels), and Quartz only
1530 // rasterizes the clipped part.
1531 let visX0 = max(rect.minX, cullX0 - 2), visX1 = min(rect.maxX, cullX1 + 2)
1532 guard visX1 > visX0, let cg = NSGraphicsContext.current?.cgContext else { return }
1533 let imgW = CGFloat(img.width)
839 let fromX = CGFloat(clip.srcIn / media.duration) * imgW1534 let fromX = CGFloat(clip.srcIn / media.duration) * imgW
840 let fromW = CGFloat(clip.duration / media.duration) * imgW1535 let fromW = max(1, CGFloat(clip.duration / media.duration) * imgW)
841 let dest = NSRect(x: rect.minX, y: rect.minY + 14,1536 let dest = NSRect(x: rect.minX, y: rect.minY + 14,
842 width: rect.width, height: rect.height - 16)1537 width: rect.width, height: rect.height - 16)
843 img.draw(in: dest, from: NSRect(x: fromX, y: 0, width: max(1, fromW),1538 let scaleX = dest.width / fromW
844 height: img.size.height),1539 cg.saveGState()
845 operation: .sourceOver, fraction: 0.85)1540 cg.setAlpha(0.85)
1541 cg.clip(to: CGRect(x: visX0, y: dest.minY, width: visX1 - visX0,
1542 height: dest.height))
1543 cg.translateBy(x: dest.minX - fromX * scaleX, y: dest.maxY)
1544 cg.scaleBy(x: 1, y: -1)
1545 cg.draw(img, in: CGRect(x: 0, y: 0, width: imgW * scaleX, height: dest.height))
1546 cg.restoreGState()
846 }1547 }
8471548
848 private func drawFades(_ clip: Clip, rect: NSRect, selected: Bool) {1549 private func drawFades(_ clip: Clip, rect: NSRect, selected: Bool) {
...@@ -867,7 +1568,16 @@ final class TimelineView: NSView {...@@ -867,7 +1568,16 @@ final class TimelineView: NSView {
867 let xOut = xFor(clip.end - clip.fadeOut)1568 let xOut = xFor(clip.end - clip.fadeOut)
868 if clip.fadeIn > 0.001 { fadeShape(from: xFor(clip.start), to: xIn, leading: true) }1569 if clip.fadeIn > 0.001 { fadeShape(from: xFor(clip.start), to: xIn, leading: true) }
869 if clip.fadeOut > 0.001 { fadeShape(from: xFor(clip.end), to: xOut, leading: false) }1570 if clip.fadeOut > 0.001 { fadeShape(from: xFor(clip.end), to: xOut, leading: false) }
870 // Handles (always visible so fades stay discoverable).1571 drawFadeHandles(clip, rect: rect, selected: selected)
1572 }
1573
1574 /// Fade handles (always visible so fades stay discoverable). Split out of
1575 /// `drawFades` because narrow (LOD) clips draw the handles — they're the
1576 /// speckled texture of a dense audio lane — without the fade shapes.
1577 private func drawFadeHandles(_ clip: Clip, rect: NSRect, selected: Bool) {
1578 let top = rect.minY + 13
1579 let xIn = xFor(clip.start + clip.fadeIn)
1580 let xOut = xFor(clip.end - clip.fadeOut)
871 for x in [xIn, xOut] {1581 for x in [xIn, xOut] {
872 let r = NSRect(x: x - 3.5, y: top - 3.5 + 4, width: 7, height: 7)1582 let r = NSRect(x: x - 3.5, y: top - 3.5 + 4, width: 7, height: 7)
873 (selected ? NSColor.white : NSColor(calibratedWhite: 0.85, alpha: 0.9)).setFill()1583 (selected ? NSColor.white : NSColor(calibratedWhite: 0.85, alpha: 0.9)).setFill()
...@@ -891,8 +1601,9 @@ final class TimelineView: NSView {...@@ -891,8 +1601,9 @@ final class TimelineView: NSView {
891 x += w + 21601 x += w + 2
892 break // one panel image; boards are one still, no need to tile1602 break // one panel image; boards are one still, no need to tile
893 }1603 }
894 NSImage(systemSymbolName: "pencil.and.outline", accessibilityDescription: nil)?1604 Self.bakedSymbol("pencil.and.outline",
895 .tinted(NSColor(calibratedWhite: 0.2, alpha: 1))1605 tint: NSColor(calibratedWhite: 0.2, alpha: 1),
1606 key: "board.pencil")?
896 .draw(in: NSRect(x: rect.minX + 4, y: rect.minY + 16, width: 11, height: 11),1607 .draw(in: NSRect(x: rect.minX + 4, y: rect.minY + 16, width: 11, height: 11),
897 from: .zero, operation: .sourceOver, fraction: 0.9)1608 from: .zero, operation: .sourceOver, fraction: 0.9)
898 }1609 }
...@@ -901,7 +1612,7 @@ final class TimelineView: NSView {...@@ -901,7 +1612,7 @@ final class TimelineView: NSView {
9011612
902 private func drawFusionBand() {1613 private func drawFusionBand() {
903 guard fusionBandH > 0 else { return }1614 guard fusionBandH > 0 else { return }
904 let band = NSRect(x: 0, y: rulerH, width: bounds.width, height: fusionBandH)1615 let band = NSRect(x: 0, y: fusionTop, width: bounds.width, height: fusionBandH)
905 NSColor(calibratedRed: 0.16, green: 0.14, blue: 0.05, alpha: 1).setFill()1616 NSColor(calibratedRed: 0.16, green: 0.14, blue: 0.05, alpha: 1).setFill()
906 band.fill()1617 band.fill()
907 FusionComps.yellow.withAlphaComponent(0.5).setFill()1618 FusionComps.yellow.withAlphaComponent(0.5).setFill()
...@@ -943,10 +1654,10 @@ final class TimelineView: NSView {...@@ -943,10 +1654,10 @@ final class TimelineView: NSView {
943 }1654 }
9441655
945 private func compAt(point: NSPoint) -> FusionComp? {1656 private func compAt(point: NSPoint) -> FusionComp? {
946 guard fusionBandH > 0, point.y > rulerH, point.y < rulerH + fusionBandH1657 guard fusionBandH > 0, point.y > fusionTop, point.y < fusionTop + fusionBandH
947 else { return nil }1658 else { return nil }
948 let fps = project.fps1659 let fps = project.fps
949 let band = NSRect(x: 0, y: rulerH, width: bounds.width, height: fusionBandH)1660 let band = NSRect(x: 0, y: fusionTop, width: bounds.width, height: fusionBandH)
950 for (comp, lane, lanes) in comps.stacked() {1661 for (comp, lane, lanes) in comps.stacked() {
951 let x0 = xFor(comp.startSeconds(fps: fps))1662 let x0 = xFor(comp.startSeconds(fps: fps))
952 let x1 = xFor(comp.endSeconds(fps: fps))1663 let x1 = xFor(comp.endSeconds(fps: fps))
...@@ -958,6 +1669,109 @@ final class TimelineView: NSView {...@@ -958,6 +1669,109 @@ final class TimelineView: NSView {
958 return nil1669 return nil
959 }1670 }
9601671
1672 // MARK: - Optimization strip
1673
1674 /// Per-column optimization state for the thin band under the ruler:
1675 /// GREEN — every proxy chunk the visible tracks need at that time is
1676 /// built at the preview-quality target; DIM GREEN — built but below
1677 /// target (usable now, upgrade queued); YELLOW — building right now (or
1678 /// a rescue slice is standing in); RED — no proxy on disk (never built,
1679 /// evicted, or failed). Aggregated worst-wins across visible video
1680 /// tracks, so one glance says whether playing here will be instant.
1681 ///
1682 /// The runs are cached — recomputed only when the view moves (origin/
1683 /// zoom/width) or something actually changed (.projectChanged /
1684 /// .mediaStatusChanged clear the key) — so playback frames just re-fill
1685 /// a few dozen rects.
1686 private var stripRuns: [(x: CGFloat, w: CGFloat, color: NSColor)] = []
1687 private var stripKey: (origin: Double, pps: Double, width: CGFloat)?
1688
1689 private func rebuildStripIfNeeded() {
1690 if let k = stripKey, k.origin == originSecond, k.pps == pxPerSecond,
1691 k.width == bounds.width { return }
1692 stripKey = (originSecond, pxPerSecond, bounds.width)
1693 stripRuns.removeAll(keepingCapacity: true)
1694 rebuildSceneIfNeeded()
1695
1696 let colW: CGFloat = 2
1697 let ncols = Int((bounds.width - headerW) / colW) + 1
1698 guard ncols > 0 else { return }
1699 var vals = [UInt8](repeating: .max, count: ncols)
1700 let leftSec = secondsFor(headerW), rightSec = secondsFor(bounds.width)
1701 var snaps: [String: ChunkManager.StripSnapshot?] = [:]
1702
1703 func mark(_ lo: Double, _ hi: Double, _ v: UInt8) {
1704 let c0 = max(0, Int((xFor(lo) - headerW) / colW))
1705 let c1 = min(ncols - 1, Int((xFor(hi) - headerW) / colW))
1706 guard c1 >= c0 else { return }
1707 for c in c0...c1 where vals[c] > v { vals[c] = v }
1708 }
1709
1710 for ref in laneRows {
1711 guard ref.videoIndex != nil, !session.hiddenTracks.contains(ref) else { continue }
1712 let clips = clipsByLane[ref] ?? []
1713 for i in visibleIndexRange(clips, lane: ref, left: leftSec, right: rightSec) {
1714 let clip = clips[i]
1715 guard clip.kind == .video,
1716 let media = project.media(clip.mediaId), !media.isAudio,
1717 media.duration > 0 else { continue }
1718 let a = max(clip.start, leftSec), b = min(clip.end, rightSec)
1719 guard b > a else { continue }
1720 let snap: ChunkManager.StripSnapshot?
1721 if let cached = snaps[media.cacheKey] { snap = cached }
1722 else { snap = ctx.chunks.stripSnapshot(media: media); snaps[media.cacheKey] = snap }
1723 guard let snap else { mark(a, b, 0); continue } // unscanned: unknown
1724 if snap.fullProxy { mark(a, b, 3); continue } // legacy whole-file proxy
1725 let srcA = clip.srcIn + (a - clip.start) * clip.speed
1726 let srcB = clip.srcIn + (b - clip.start) * clip.speed
1727 let ci = min(snap.n - 1, ChunkManager.chunkIndex(forSource: min(srcA, srcB)))
1728 let cj = min(snap.n - 1, ChunkManager.chunkIndex(forSource: max(srcA, srcB) - 1e-6))
1729 for idx in ci...max(ci, cj) {
1730 let v: UInt8
1731 if let w = snap.built[idx] {
1732 v = w >= snap.target ? 3 : 2
1733 } else if snap.inFlight.contains(idx) || snap.partial.contains(idx) {
1734 v = 1
1735 } else {
1736 v = 0 // missing, evicted, or failed
1737 }
1738 let t0 = clip.start
1739 + (Double(idx) * ChunkManager.chunkSeconds - clip.srcIn) / max(1e-4, clip.speed)
1740 let t1 = clip.start
1741 + (Double(idx + 1) * ChunkManager.chunkSeconds - clip.srcIn) / max(1e-4, clip.speed)
1742 mark(max(min(t0, t1), a), min(max(t0, t1), b), v)
1743 }
1744 }
1745 }
1746
1747 // Compress equal-valued columns into fill runs.
1748 let palette = [Theme.stripMissing, Theme.stripBuilding,
1749 Theme.stripUsable, Theme.stripFull]
1750 var c = 0
1751 while c < ncols {
1752 let v = vals[c]
1753 var e = c + 1
1754 while e < ncols, vals[e] == v { e += 1 }
1755 if v != .max {
1756 stripRuns.append((x: headerW + CGFloat(c) * colW,
1757 w: CGFloat(e - c) * colW,
1758 color: palette[Int(v)]))
1759 }
1760 c = e
1761 }
1762 }
1763
1764 private func drawOptimizeStrip() {
1765 Theme.timelineBg.setFill()
1766 NSRect(x: 0, y: rulerH, width: bounds.width, height: stripH).fill()
1767 rebuildStripIfNeeded()
1768 let y = rulerH + 1
1769 for run in stripRuns {
1770 run.color.setFill()
1771 NSRect(x: run.x, y: y, width: run.w, height: stripH - 2).fill()
1772 }
1773 }
1774
961 // MARK: - Ruler / playhead / indicators1775 // MARK: - Ruler / playhead / indicators
9621776
963 private func drawRuler() {1777 private func drawRuler() {
...@@ -1164,7 +1978,7 @@ final class TimelineView: NSView {...@@ -1164,7 +1978,7 @@ final class TimelineView: NSView {
1164 // MARK: - Mouse editing1978 // MARK: - Mouse editing
11651979
1166 private enum DragMode {1980 private enum DragMode {
1167 case none, scrub, move, trimIn, trimOut, rippleOut, slip,1981 case none, scrub, move, trimIn, trimOut, rippleOut, rippleIn, slip,
1168 stretchIn, stretchOut, fadeIn, fadeOut, box, resizeTrack,1982 stretchIn, stretchOut, fadeIn, fadeOut, box, resizeTrack,
1169 hBarPan, hBarLeft, hBarRight, vBarPan, vBarTop, vBarBottom1983 hBarPan, hBarLeft, hBarRight, vBarPan, vBarTop, vBarBottom
1170 }1984 }
...@@ -1235,7 +2049,7 @@ final class TimelineView: NSView {...@@ -1235,7 +2049,7 @@ final class TimelineView: NSView {
1235 return2049 return
1236 }2050 }
12372051
1238 if p.y < rulerH {2052 if p.y < rulerH + stripH { // ruler + optimization strip both scrub
1239 drag.mode = .scrub2053 drag.mode = .scrub
1240 playback.setRate(0)2054 playback.setRate(0)
1241 playback.seek(to: max(0, quantize(secondsFor(p.x))))2055 playback.seek(to: max(0, quantize(secondsFor(p.x))))
...@@ -1243,8 +2057,8 @@ final class TimelineView: NSView {...@@ -1243,8 +2057,8 @@ final class TimelineView: NSView {
1243 }2057 }
12442058
1245 // Fusion band header: hide preview (top) / focus (bottom)2059 // Fusion band header: hide preview (top) / focus (bottom)
1246 if fusionBandH > 0, p.x < headerW, p.y > rulerH, p.y < lanesTop {2060 if fusionBandH > 0, p.x < headerW, p.y > fusionTop, p.y < lanesTop {
1247 if p.y < rulerH + fusionBandH * 0.55 { session.fusionHidden.toggle() }2061 if p.y < fusionTop + fusionBandH * 0.55 { session.fusionHidden.toggle() }
1248 else { session.fusionFocus.toggle() }2062 else { session.fusionFocus.toggle() }
1249 needsDisplay = true2063 needsDisplay = true
1250 return2064 return
...@@ -1348,13 +2162,17 @@ final class TimelineView: NSView {...@@ -1348,13 +2162,17 @@ final class TimelineView: NSView {
1348 if drag.mode == .none {2162 if drag.mode == .none {
1349 let stretch = event.modifierFlags.contains(.command) && clip.kind == .video2163 let stretch = event.modifierFlags.contains(.command) && clip.kind == .video
1350 let opt = event.modifierFlags.contains(.option)2164 let opt = event.modifierFlags.contains(.option)
1351 if opt, rect.maxX - p.x < edge {2165 let onIn = p.x - rect.minX < edge
1352 drag.mode = .rippleOut // ⌥-drag out edge: push everything after2166 let onOut = rect.maxX - p.x < edge
2167 if opt, onOut {
2168 drag.mode = .rippleOut // ⌥-drag out edge: ripple the tail away
2169 } else if opt, onIn, clip.kind != .storyboard {
2170 drag.mode = .rippleIn // ⌥-drag in edge: ripple-trim the head
1353 } else if opt || session.mainTool == .slide {2171 } else if opt || session.mainTool == .slide {
1354 drag.mode = .slip2172 drag.mode = .slip // ⌥-drag body: slip source under the clip
1355 } else if p.x - rect.minX < edge {2173 } else if onIn {
1356 drag.mode = stretch ? .stretchIn : .trimIn2174 drag.mode = stretch ? .stretchIn : .trimIn
1357 } else if rect.maxX - p.x < edge {2175 } else if onOut {
1358 drag.mode = stretch ? .stretchOut : .trimOut2176 drag.mode = stretch ? .stretchOut : .trimOut
1359 } else {2177 } else {
1360 drag.mode = .move2178 drag.mode = .move
...@@ -1395,7 +2213,7 @@ final class TimelineView: NSView {...@@ -1395,7 +2213,7 @@ final class TimelineView: NSView {
13952213
1396 // Dragging against the view edges pans the timeline (there's no2214 // Dragging against the view edges pans the timeline (there's no
1397 // enclosing scroll view, so the playhead could never leave the screen).2215 // enclosing scroll view, so the playhead could never leave the screen).
1398 if [.scrub, .move, .trimIn, .trimOut, .rippleOut, .slip,2216 if [.scrub, .move, .trimIn, .trimOut, .rippleOut, .rippleIn, .slip,
1399 .stretchIn, .stretchOut, .fadeIn, .fadeOut].contains(drag.mode) {2217 .stretchIn, .stretchOut, .fadeIn, .fadeOut].contains(drag.mode) {
1400 if p.x > bounds.width - 30 {2218 if p.x > bounds.width - 30 {
1401 originSecond += Double(p.x - (bounds.width - 30)) * 0.12 / pxPerSecond2219 originSecond += Double(p.x - (bounds.width - 30)) * 0.12 / pxPerSecond
...@@ -1413,6 +2231,7 @@ final class TimelineView: NSView {...@@ -1413,6 +2231,7 @@ final class TimelineView: NSView {
1413 case .trimIn: dragTrimIn(dSec: dSec)2231 case .trimIn: dragTrimIn(dSec: dSec)
1414 case .trimOut: dragTrimOut(dSec: dSec)2232 case .trimOut: dragTrimOut(dSec: dSec)
1415 case .rippleOut: dragRippleOut(dSec: dSec)2233 case .rippleOut: dragRippleOut(dSec: dSec)
2234 case .rippleIn: dragRippleIn(dSec: dSec)
1416 case .slip: dragSlip(dSec: dSec)2235 case .slip: dragSlip(dSec: dSec)
1417 case .stretchIn: dragStretch(dSec: dSec, fromStart: true)2236 case .stretchIn: dragStretch(dSec: dSec, fromStart: true)
1418 case .stretchOut: dragStretch(dSec: dSec, fromStart: false)2237 case .stretchOut: dragStretch(dSec: dSec, fromStart: false)
...@@ -1458,8 +2277,9 @@ final class TimelineView: NSView {...@@ -1458,8 +2277,9 @@ final class TimelineView: NSView {
1458 case .box:2277 case .box:
1459 let r = boxRect()2278 let r = boxRect()
1460 var hit = drag.baseSelection2279 var hit = drag.baseSelection
1461 for (row, ref) in project.laneRefs.enumerated() {2280 rebuildSceneIfNeeded()
1462 for clip in project.clips(on: ref)2281 for (row, ref) in laneRows.enumerated() {
2282 for clip in clipsByLane[ref] ?? []
1463 where clipRect(clip, row: row).intersects(r) {2283 where clipRect(clip, row: row).intersects(r) {
1464 hit.insert(clip.id)2284 hit.insert(clip.id)
1465 }2285 }
...@@ -1472,12 +2292,6 @@ final class TimelineView: NSView {...@@ -1472,12 +2292,6 @@ final class TimelineView: NSView {
1472 session.trackHeights[ref] = min(4, max(0.35, factor))2292 session.trackHeights[ref] = min(4, max(0.35, factor))
1473 }2293 }
1474 }2294 }
1475 // Dragging the floating scrollbars is a scroll/zoom — simplify while it
1476 // moves, like the wheel and pinch paths.
1477 if [.hBarPan, .hBarLeft, .hBarRight,
1478 .vBarPan, .vBarTop, .vBarBottom].contains(drag.mode) {
1479 noteScrolling()
1480 }
1481 drag.moved = true2295 drag.moved = true
1482 autoscroll(with: event)2296 autoscroll(with: event)
1483 needsDisplay = true2297 needsDisplay = true
...@@ -1485,7 +2299,7 @@ final class TimelineView: NSView {...@@ -1485,7 +2299,7 @@ final class TimelineView: NSView {
14852299
1486 override func mouseUp(with event: NSEvent) {2300 override func mouseUp(with event: NSEvent) {
1487 switch drag.mode {2301 switch drag.mode {
1488 case .move, .trimIn, .trimOut, .rippleOut, .slip,2302 case .move, .trimIn, .trimOut, .rippleOut, .rippleIn, .slip,
1489 .stretchIn, .stretchOut, .fadeIn, .fadeOut:2303 .stretchIn, .stretchOut, .fadeIn, .fadeOut:
1490 store.endGesture()2304 store.endGesture()
1491 default:2305 default:
...@@ -1518,7 +2332,6 @@ final class TimelineView: NSView {...@@ -1518,7 +2332,6 @@ final class TimelineView: NSView {
1518 if maxScrollY > 0 {2332 if maxScrollY > 0 {
1519 scrollY = min(max(0, panDrag.origScrollY - (p.y - panDrag.start.y)), maxScrollY)2333 scrollY = min(max(0, panDrag.origScrollY - (p.y - panDrag.start.y)), maxScrollY)
1520 }2334 }
1521 noteScrolling()
1522 needsDisplay = true2335 needsDisplay = true
1523 }2336 }
15242337
...@@ -1532,14 +2345,11 @@ final class TimelineView: NSView {...@@ -1532,14 +2345,11 @@ final class TimelineView: NSView {
1532 // Vertical retracking only for a lone unlinked clip.2345 // Vertical retracking only for a lone unlinked clip.
1533 let multi = drag.origSelection.count > 1 || store.selection.count > 12346 let multi = drag.origSelection.count > 1 || store.selection.count > 1
15342347
1535 var delta = dSec2348 // Snap+quantize the moved start edge, then clamp to t >= 0 for all moved clips.
1536 if let adj = snapAdjust(start: orig.start + dSec, duration: orig.duration,2349 let snappedStart = snapAndQuantize(orig.start + dSec,
1537 excluding: Set(drag.origSelection.keys)) {2350 excluding: Set(drag.origSelection.keys),
1538 delta += adj.adjust2351 duration: orig.duration)
1539 activeSnapTarget = adj.target2352 var delta = snappedStart - orig.start
1540 }
1541 // Frame-quantize the moved edge, clamp to t >= 0 for all moved clips.
1542 delta = quantize(orig.start + delta) - orig.start
1543 let minStart = drag.origSelection.values.map(\.start).min() ?? 02353 let minStart = drag.origSelection.values.map(\.start).min() ?? 0
1544 if minStart + delta < 0 { delta = -minStart }2354 if minStart + delta < 0 { delta = -minStart }
15452355
...@@ -1625,6 +2435,16 @@ final class TimelineView: NSView {...@@ -1625,6 +2435,16 @@ final class TimelineView: NSView {
1625 }2435 }
1626 }2436 }
16272437
2438 /// A trim/slip drag is exposing source material the clip didn't use
2439 /// before — tell the proxy builder NOW (mid-drag), not at mouse-up, so
2440 /// the newly extended range is often already covered when the user plays
2441 /// it. `direction` is which way the exposure grows in source time.
2442 private func prefetchExposure(_ orig: Clip, sourceTime: Double, direction: Int) {
2443 guard orig.kind == .video, let media = project.media(orig.mediaId) else { return }
2444 ctx.chunks.noteGestureExposure(media: media, sourceTime: sourceTime,
2445 direction: direction)
2446 }
2447
1628 /// Non-audio clips on the same track whose head would be swallowed by2448 /// Non-audio clips on the same track whose head would be swallowed by
1629 /// dragging `orig`'s out-edge to `newEnd` (audio layers freely, so it2449 /// dragging `orig`'s out-edge to `newEnd` (audio layers freely, so it
1630 /// never gets pushed).2450 /// never gets pushed).
...@@ -1657,12 +2477,7 @@ final class TimelineView: NSView {...@@ -1657,12 +2477,7 @@ final class TimelineView: NSView {
1657 return2477 return
1658 }2478 }
1659 let media = project.media(orig.mediaId)2479 let media = project.media(orig.mediaId)
1660 var desired = orig.end + dSec2480 let desired = snapAndQuantize(orig.end + dSec, excluding: [orig.id])
1661 if let adj = snapAdjust(start: desired, duration: 0, excluding: [orig.id]) {
1662 desired += adj.adjust
1663 activeSnapTarget = adj.target
1664 }
1665 desired = quantize(desired)
1666 var maxEnd = Double.greatestFiniteMagnitude2481 var maxEnd = Double.greatestFiniteMagnitude
1667 if let media {2482 if let media {
1668 maxEnd = orig.start + (media.duration - orig.srcIn) / max(0.001, orig.speed)2483 maxEnd = orig.start + (media.duration - orig.srcIn) / max(0.001, orig.speed)
...@@ -1677,6 +2492,10 @@ final class TimelineView: NSView {...@@ -1677,6 +2492,10 @@ final class TimelineView: NSView {
1677 // The grabbed clip's clamped change is the delta applied to every2492 // The grabbed clip's clamped change is the delta applied to every
1678 // selected/linked clip.2493 // selected/linked clip.
1679 let delta = newEnd - orig.end2494 let delta = newEnd - orig.end
2495 if delta > 0 { // extending the tail exposes fresh source material
2496 prefetchExposure(orig, sourceTime: orig.srcIn + (newEnd - orig.start) * orig.speed,
2497 direction: 1)
2498 }
1680 let targets = drag.origSelection.values.filter { $0.kind != .storyboard }2499 let targets = drag.origSelection.values.filter { $0.kind != .storyboard }
1681 let single = targets.count <= 12500 let single = targets.count <= 1
1682 store.updateGesture { model in2501 store.updateGesture { model in
...@@ -1703,12 +2522,7 @@ final class TimelineView: NSView {...@@ -1703,12 +2522,7 @@ final class TimelineView: NSView {
17032522
1704 private func dragTrimIn(dSec: Double) {2523 private func dragTrimIn(dSec: Double) {
1705 guard let orig = drag.origClip else { return }2524 guard let orig = drag.origClip else { return }
1706 var desired = orig.start + dSec2525 let desired = snapAndQuantize(orig.start + dSec, excluding: [orig.id])
1707 if let adj = snapAdjust(start: desired, duration: 0, excluding: [orig.id]) {
1708 desired += adj.adjust
1709 activeSnapTarget = adj.target
1710 }
1711 desired = quantize(desired)
1712 // Storyboard panels are pure start positions (no source media), so the2526 // Storyboard panels are pure start positions (no source media), so the
1713 // in-edge just slides the panel's start either way — bounded only by the2527 // in-edge just slides the panel's start either way — bounded only by the
1714 // previous panel (a frame of clearance) and this panel's own end.2528 // previous panel (a frame of clearance) and this panel's own end.
...@@ -1732,6 +2546,10 @@ final class TimelineView: NSView {...@@ -1732,6 +2546,10 @@ final class TimelineView: NSView {
1732 minStart = max(0, minStart)2546 minStart = max(0, minStart)
1733 let maxStart = orig.end - frameDur2547 let maxStart = orig.end - frameDur
1734 let newStart = min(max(desired, minStart), maxStart)2548 let newStart = min(max(desired, minStart), maxStart)
2549 if newStart < orig.start { // extending the head exposes earlier source
2550 prefetchExposure(orig, sourceTime: orig.srcIn + (newStart - orig.start) * orig.speed,
2551 direction: -1)
2552 }
1735 let base = store.gestureBaseModel ?? project2553 let base = store.gestureBaseModel ?? project
1736 // The grabbed clip's clamped change is the delta applied to every2554 // The grabbed clip's clamped change is the delta applied to every
1737 // selected/linked clip. A storyboard in-edge just moves that panel's2555 // selected/linked clip. A storyboard in-edge just moves that panel's
...@@ -1768,12 +2586,7 @@ final class TimelineView: NSView {...@@ -1768,12 +2586,7 @@ final class TimelineView: NSView {
1768 private func dragRippleOut(dSec: Double) {2586 private func dragRippleOut(dSec: Double) {
1769 guard let orig = drag.origClip else { return }2587 guard let orig = drag.origClip else { return }
1770 let base = store.gestureBaseModel ?? project2588 let base = store.gestureBaseModel ?? project
1771 var desired = quantize(orig.end + dSec)2589 let desired = snapAndQuantize(orig.end + dSec, excluding: [orig.id])
1772 if let adj = snapAdjust(start: desired, duration: 0, excluding: [orig.id]) {
1773 desired += adj.adjust
1774 activeSnapTarget = adj.target
1775 desired = quantize(desired)
1776 }
1777 var newEnd = max(desired, orig.start + frameDur)2590 var newEnd = max(desired, orig.start + frameDur)
1778 if orig.kind == .video, let media = base.media(orig.mediaId) {2591 if orig.kind == .video, let media = base.media(orig.mediaId) {
1779 newEnd = min(newEnd, orig.start + (media.duration - orig.srcIn) / max(0.001, orig.speed))2592 newEnd = min(newEnd, orig.start + (media.duration - orig.srcIn) / max(0.001, orig.speed))
...@@ -1785,6 +2598,11 @@ final class TimelineView: NSView {...@@ -1785,6 +2598,11 @@ final class TimelineView: NSView {
1785 // Never push anything below t = 0.2598 // Never push anything below t = 0.
1786 let minStart = followers.map(\.start).min() ?? 02599 let minStart = followers.map(\.start).min() ?? 0
1787 let clampedDelta = max(delta, -minStart)2600 let clampedDelta = max(delta, -minStart)
2601 if clampedDelta > 0 { // ripple-extending the tail exposes fresh source
2602 prefetchExposure(orig,
2603 sourceTime: orig.srcIn + (orig.end + clampedDelta - orig.start) * orig.speed,
2604 direction: 1)
2605 }
1788 store.updateGesture { model in2606 store.updateGesture { model in
1789 if orig.kind != .storyboard,2607 if orig.kind != .storyboard,
1790 let i = model.clips.firstIndex(where: { $0.id == orig.id }) {2608 let i = model.clips.firstIndex(where: { $0.id == orig.id }) {
...@@ -1797,10 +2615,54 @@ final class TimelineView: NSView {...@@ -1797,10 +2615,54 @@ final class TimelineView: NSView {
1797 }2615 }
1798 }2616 }
17992617
2618 /// ⌥-drag a clip's IN edge: ripple-trim the head while pinning the clip's
2619 /// start. The in-point (`srcIn`) and length change —
2620 /// the clip's OUT edge moves — and every later clip on the track shifts by the
2621 /// same amount so the timeline stays gapless. Unlike a normal trim-in (which
2622 /// slides the clip's start and opens blank space *before* it), the start stays
2623 /// put and the space is taken out of the track. Dragging right trims the head
2624 /// (clip shrinks, followers pull in); left extends it (clip grows, followers
2625 /// push out). The out edge — the source out-point — never moves.
2626 private func dragRippleIn(dSec: Double) {
2627 guard let orig = drag.origClip, orig.kind != .storyboard else { return }
2628 let base = store.gestureBaseModel ?? project
2629 let followers = base.clips.filter {
2630 $0.id != orig.id && $0.track == orig.track && $0.start >= orig.end - 1e-9
2631 }
2632 // Snap the moving OUT edge to static references only — the followers ride
2633 // along with it, so they can't be snap targets. d > 0 trims the head.
2634 var exclude = Set(followers.map(\.id)); exclude.insert(orig.id)
2635 let newEnd = snapAndQuantize(orig.end - dSec, excluding: exclude)
2636 var d = orig.end - newEnd
2637 d = min(d, orig.duration - frameDur) // keep ≥ one frame
2638 d = max(d, -orig.srcIn / max(0.001, orig.speed)) // head ≥ source start
2639 if let firstStart = followers.map(\.start).min() { // followers ≥ t = 0
2640 d = min(d, firstStart)
2641 }
2642 if d < 0 { // ripple-extending the head exposes earlier source
2643 prefetchExposure(orig, sourceTime: orig.srcIn + d * orig.speed, direction: -1)
2644 }
2645 store.updateGesture { model in
2646 guard let i = model.clips.firstIndex(where: { $0.id == orig.id }) else { return }
2647 model.clips[i].srcIn = orig.srcIn + d * orig.speed
2648 model.clips[i].duration = orig.duration - d
2649 for f in followers {
2650 guard let j = model.clips.firstIndex(where: { $0.id == f.id }) else { continue }
2651 model.clips[j].start = f.start - d
2652 }
2653 }
2654 }
2655
1800 private func dragSlip(dSec: Double) {2656 private func dragSlip(dSec: Double) {
1801 guard let orig = drag.origClip, let media = project.media(orig.mediaId) else { return }2657 guard let orig = drag.origClip, let media = project.media(orig.mediaId) else { return }
1802 let maxIn = max(0, media.duration - orig.sourceLength)2658 let maxIn = max(0, media.duration - orig.sourceLength)
1803 let newIn = min(max(orig.srcIn - dSec * orig.speed, 0), maxIn)2659 let newIn = min(max(orig.srcIn - dSec * orig.speed, 0), maxIn)
2660 // Slipping reveals source on the side the content is sliding from.
2661 if newIn < orig.srcIn {
2662 prefetchExposure(orig, sourceTime: newIn, direction: -1)
2663 } else if newIn > orig.srcIn {
2664 prefetchExposure(orig, sourceTime: newIn + orig.sourceLength, direction: 1)
2665 }
1804 store.updateGesture { model in2666 store.updateGesture { model in
1805 guard let i = model.clips.firstIndex(where: { $0.id == drag.clipId }) else { return }2667 guard let i = model.clips.firstIndex(where: { $0.id == drag.clipId }) else { return }
1806 model.clips[i].srcIn = newIn2668 model.clips[i].srcIn = newIn
...@@ -1847,6 +2709,21 @@ final class TimelineView: NSView {...@@ -1847,6 +2709,21 @@ final class TimelineView: NSView {
1847 }2709 }
1848 }2710 }
18492711
2712 /// The single place edge alignment is decided: snap `desired` to nearby clip
2713 /// edges / playhead / 0 (unless snapping is off), record the snap indicator,
2714 /// then frame-quantize — snap FIRST, quantize ONCE. Every drag handler routes
2715 /// through here so trims, moves and ripples align identically (previously each
2716 /// re-implemented this, and the ripple handler quantized twice out of order).
2717 private func snapAndQuantize(_ desired: Double, excluding: Set<UUID>,
2718 duration: Double = 0) -> Double {
2719 var v = desired
2720 if let adj = snapAdjust(start: v, duration: duration, excluding: excluding) {
2721 v += adj.adjust
2722 activeSnapTarget = adj.target
2723 }
2724 return quantize(v)
2725 }
2726
1850 private func snapAdjust(start: Double, duration: Double, excluding: Set<UUID>)2727 private func snapAdjust(start: Double, duration: Double, excluding: Set<UUID>)
1851 -> (adjust: Double, target: Double)? {2728 -> (adjust: Double, target: Double)? {
1852 // Holding ⇧ mid-drag temporarily inverts snapping.2729 // Holding ⇧ mid-drag temporarily inverts snapping.
...@@ -1882,36 +2759,50 @@ final class TimelineView: NSView {...@@ -1882,36 +2759,50 @@ final class TimelineView: NSView {
1882 }2759 }
18832760
1884 override func mouseMoved(with event: NSEvent) {2761 override func mouseMoved(with event: NSEvent) {
1885 let p = convert(event.locationInWindow, from: nil)2762 lastMousePoint = convert(event.locationInWindow, from: nil)
1886 lastMousePoint = p2763 cursor(for: event.modifierFlags).set()
1887 var cursor = NSCursor.arrow2764 }
2765
2766 // Holding a modifier changes what a drag would do, so refresh the cursor even
2767 // when the pointer is still (mouseMoved won't fire on a bare key press).
2768 override func flagsChanged(with event: NSEvent) {
2769 cursor(for: event.modifierFlags).set()
2770 }
2771
2772 /// The cursor for the pointer's current spot, given the held modifiers, so it
2773 /// previews the gesture a drag would start: ⌥ over a clip body slips it, ⌥/⌘
2774 /// or a plain hover over an edge resizes (trim / ripple / stretch).
2775 private func cursor(for mods: NSEvent.ModifierFlags) -> NSCursor {
2776 let p = lastMousePoint
1888 if let barMode = scrollbarHit(p) {2777 if let barMode = scrollbarHit(p) {
1889 switch barMode {2778 switch barMode {
1890 case .hBarLeft, .hBarRight: cursor = .resizeLeftRight2779 case .hBarLeft, .hBarRight: return .resizeLeftRight
1891 case .vBarTop, .vBarBottom: cursor = .resizeUpDown2780 case .vBarTop, .vBarBottom: return .resizeUpDown
1892 default: break2781 default: return .arrow
1893 }
1894 } else if trackBoundaryAt(y: p.y) != nil, p.y > lanesTop {
1895 cursor = .resizeUpDown
1896 } else if let (clip, row) = clipAt(point: p) {
1897 if session.mainTool == .blade {
1898 cursor = .crosshair
1899 } else if session.mainTool == .slide {
1900 cursor = .openHand
1901 } else {
1902 let rect = clipRect(clip, row: row)
1903 if p.x - rect.minX < 7 || rect.maxX - p.x < 7 {
1904 cursor = .resizeLeftRight
1905 }
1906 }2782 }
1907 }2783 }
1908 cursor.set()2784 if trackBoundaryAt(y: p.y) != nil, p.y > lanesTop { return .resizeUpDown }
2785 guard let (clip, row) = clipAt(point: p) else { return .arrow }
2786 if session.mainTool == .blade { return .crosshair }
2787 if session.mainTool == .slide { return .openHand }
2788 let rect = clipRect(clip, row: row)
2789 let onEdge = p.x - rect.minX < 7 || rect.maxX - p.x < 7
2790 if mods.contains(.option), !onEdge { return .openHand } // ⌥ body: slip
2791 return onEdge ? .resizeLeftRight : .arrow
1909 }2792 }
19102793
1911 // MARK: - Keyboard (fallbacks; the menu bar owns the canonical bindings)2794 // MARK: - Keyboard (fallbacks; the menu bar owns the canonical bindings)
19122795
1913 override func keyDown(with event: NSEvent) {2796 override func keyDown(with event: NSEvent) {
1914 let pc = playback2797 let pc = playback
2798 // A key pressed WHILE a mouse drag holds an open gesture must not run —
2799 // a mutating shortcut (split/delete/nudge) calls `store.mutate`, whose
2800 // `precondition(gestureBase == nil)` would trap mid-drag. Ignore keys
2801 // until the drag ends; Escape still cancels it.
2802 if store.gestureBaseModel != nil {
2803 if event.keyCode == 53 { cancelOperation(nil) } // Esc → cancel drag
2804 return
2805 }
1915 switch event.charactersIgnoringModifiers?.lowercased() {2806 switch event.charactersIgnoringModifiers?.lowercased() {
1916 case " ": pc.togglePlay()2807 case " ": pc.togglePlay()
1917 case "j": pc.shuttle(-1)2808 case "j": pc.shuttle(-1)
...@@ -2745,7 +3636,7 @@ final class TimelineView: NSView {...@@ -2745,7 +3636,7 @@ final class TimelineView: NSView {
2745 add("Rescan Comps", #selector(ctxRescanComps))3636 add("Rescan Comps", #selector(ctxRescanComps))
2746 return menu3637 return menu
2747 }3638 }
2748 if fusionBandH > 0, p.y > rulerH, p.y < lanesTop {3639 if fusionBandH > 0, p.y > fusionTop, p.y < lanesTop {
2749 add(session.fusionHidden ? "Show Fusion Preview" : "Hide Fusion Preview",3640 add(session.fusionHidden ? "Show Fusion Preview" : "Hide Fusion Preview",
2750 #selector(ctxToggleFusionHidden))3641 #selector(ctxToggleFusionHidden))
2751 add(session.fusionFocus ? "Unfocus Fusion" : "Focus Fusion",3642 add(session.fusionFocus ? "Unfocus Fusion" : "Focus Fusion",
...@@ -2888,7 +3779,8 @@ final class TimelineView: NSView {...@@ -2888,7 +3779,8 @@ final class TimelineView: NSView {
2888 /// past the last clip (the playhead may live out there), but bounded.3779 /// past the last clip (the playhead may live out there), but bounded.
2889 private func clampOrigin(_ o: Double) -> Double {3780 private func clampOrigin(_ o: Double) -> Double {
2890 let overscroll = 600.0 / pxPerSecond3781 let overscroll = 600.0 / pxPerSecond
2891 return min(max(o, -overscroll), project.timelineDuration + 120)3782 rebuildSceneIfNeeded()
3783 return min(max(o, -overscroll), cachedTimelineDuration + 120)
2892 }3784 }
28933785
2894 override func scrollWheel(with event: NSEvent) {3786 override func scrollWheel(with event: NSEvent) {
...@@ -2905,12 +3797,10 @@ final class TimelineView: NSView {...@@ -2905,12 +3797,10 @@ final class TimelineView: NSView {
2905 originSecond = clampOrigin(originSecond - Double(dx) / pxPerSecond)3797 originSecond = clampOrigin(originSecond - Double(dx) / pxPerSecond)
2906 }3798 }
2907 }3799 }
2908 noteScrolling()
2909 needsDisplay = true3800 needsDisplay = true
2910 }3801 }
29113802
2912 override func magnify(with event: NSEvent) {3803 override func magnify(with event: NSEvent) {
2913 noteScrolling()
2914 zoom(by: 1 + event.magnification, anchorX: convert(event.locationInWindow, from: nil).x)3804 zoom(by: 1 + event.magnification, anchorX: convert(event.locationInWindow, from: nil).x)
2915 }3805 }
29163806
...@@ -2969,7 +3859,7 @@ final class TimelineView: NSView {...@@ -2969,7 +3859,7 @@ final class TimelineView: NSView {
2969 let streams = expandDropStreams(droppableFiles(from: sender))3859 let streams = expandDropStreams(droppableFiles(from: sender))
2970 guard !streams.isEmpty else { fileDropPreview = nil; needsDisplay = true; return }3860 guard !streams.isEmpty else { fileDropPreview = nil; needsDisplay = true; return }
2971 let p = convert(sender.draggingLocation, from: nil)3861 let p = convert(sender.draggingLocation, from: nil)
2972 let count = project.laneRefs.count3862 let count = laneRows.count
2973 fileDropPreview = FileDropPreview(3863 fileDropPreview = FileDropPreview(
2974 streams: streams,3864 streams: streams,
2975 minOffset: streams.map(\.offset).min() ?? 0,3865 minOffset: streams.map(\.offset).min() ?? 0,
...@@ -3001,7 +3891,7 @@ final class TimelineView: NSView {...@@ -3001,7 +3891,7 @@ final class TimelineView: NSView {
3001 let files = droppableFiles(from: sender)3891 let files = droppableFiles(from: sender)
3002 guard !files.isEmpty else { return false }3892 guard !files.isEmpty else { return false }
3003 let dropSec = max(0, quantize(secondsFor(p.x)))3893 let dropSec = max(0, quantize(secondsFor(p.x)))
3004 let row = rowAt(y: p.y).flatMap { $0 < project.laneRefs.count ? $0 : nil }3894 let row = rowAt(y: p.y).flatMap { $0 < laneRows.count ? $0 : nil }
3005 importFiles(files, atSecond: dropSec, targetRow: row)3895 importFiles(files, atSecond: dropSec, targetRow: row)
3006 return true3896 return true
3007 }3897 }
...@@ -3011,7 +3901,7 @@ final class TimelineView: NSView {...@@ -3011,7 +3901,7 @@ final class TimelineView: NSView {
3011 /// track exactly the way dragging an existing clip down does.3901 /// track exactly the way dragging an existing clip down does.
3012 private func drawFileDropPreview() {3902 private func drawFileDropPreview() {
3013 guard let dp = fileDropPreview else { return }3903 guard let dp = fileDropPreview else { return }
3014 let count = project.laneRefs.count3904 let count = laneRows.count
3015 for (i, s) in dp.streams.enumerated() {3905 for (i, s) in dp.streams.enumerated() {
3016 let row = dp.baseRow + i3906 let row = dp.baseRow + i
3017 let lane = laneRect(row: row)3907 let lane = laneRect(row: row)
...@@ -3167,7 +4057,6 @@ final class TimelineView: NSView {...@@ -3167,7 +4057,6 @@ final class TimelineView: NSView {
3167 func testVThumb() -> NSRect { vThumbRect() }4057 func testVThumb() -> NSRect { vThumbRect() }
3168 func testSetOrigin(_ sec: Double) { originSecond = sec }4058 func testSetOrigin(_ sec: Double) { originSecond = sec }
3169 func testSetPxPerSecond(_ p: Double) { pxPerSecond = p }4059 func testSetPxPerSecond(_ p: Double) { pxPerSecond = p }
3170 func testSetScrolling(_ b: Bool) { isScrolling = b }
3171 /// Draw straight into the current graphics context (set by the harness),4060 /// Draw straight into the current graphics context (set by the harness),
3172 /// bypassing `cacheDisplay`'s per-call bitmap allocation so we time `draw`.4061 /// bypassing `cacheDisplay`'s per-call bitmap allocation so we time `draw`.
3173 func testRedraw() { draw(bounds) }4062 func testRedraw() { draw(bounds) }
sequencer/Sources/Sequencer/Tools.swift+46-6
...@@ -220,6 +220,8 @@ final class SettingsWindow: NSObject {...@@ -220,6 +220,8 @@ final class SettingsWindow: NSObject {
220 private let compsLabel = NSTextField(labelWithString: "—")220 private let compsLabel = NSTextField(labelWithString: "—")
221 // Global tab221 // Global tab
222 private let cacheField = NSTextField(string: "")222 private let cacheField = NSTextField(string: "")
223 private let ramField = NSTextField(string: "")
224 private let usageLabel = NSTextField(labelWithString: "")
223 private let cacheLabel = NSTextField(labelWithString: "")225 private let cacheLabel = NSTextField(labelWithString: "")
224226
225 // Aspect label → concrete storyboard resolution (stored in the model as227 // Aspect label → concrete storyboard resolution (stored in the model as
...@@ -231,9 +233,9 @@ final class SettingsWindow: NSObject {...@@ -231,9 +233,9 @@ final class SettingsWindow: NSObject {
231233
232 func show() {234 func show() {
233 buildIfNeeded()235 buildIfNeeded()
234 sync()236 window?.makeKeyAndOrderFront(nil) // visible first — sync() guards on it
235 window?.makeKeyAndOrderFront(nil)
236 NSApp.activate(ignoringOtherApps: true)237 NSApp.activate(ignoringOtherApps: true)
238 sync()
237 }239 }
238240
239 private func label(_ s: String) -> NSTextField {241 private func label(_ s: String) -> NSTextField {
...@@ -242,9 +244,17 @@ final class SettingsWindow: NSObject {...@@ -242,9 +244,17 @@ final class SettingsWindow: NSObject {
242 return l244 return l
243 }245 }
244246
247 /// Key (fields need focus) but never MAIN: `DocumentContext.current`
248 /// resolves through the main window, so a main Settings window would read
249 /// a blank headless project (fps/comps/estimate all defaults) AND starve
250 /// the front-document build gate while it's open.
251 private final class SettingsPanel: NSWindow {
252 override var canBecomeMain: Bool { false }
253 }
254
245 private func buildIfNeeded() {255 private func buildIfNeeded() {
246 guard window == nil else { return }256 guard window == nil else { return }
247 let w = NSWindow(257 let w = SettingsPanel(
248 contentRect: NSRect(x: 0, y: 0, width: 560, height: 300),258 contentRect: NSRect(x: 0, y: 0, width: 560, height: 300),
249 styleMask: [.titled, .closable],259 styleMask: [.titled, .closable],
250 backing: .buffered, defer: false)260 backing: .buffered, defer: false)
...@@ -300,13 +310,27 @@ final class SettingsWindow: NSObject {...@@ -300,13 +310,27 @@ final class SettingsWindow: NSObject {
300 cacheField.widthAnchor.constraint(equalToConstant: 60).isActive = true310 cacheField.widthAnchor.constraint(equalToConstant: 60).isActive = true
301 let cacheRow = NSStackView(views: [cacheField, NSTextField(labelWithString: "GB")])311 let cacheRow = NSStackView(views: [cacheField, NSTextField(labelWithString: "GB")])
302 cacheRow.spacing = 4312 cacheRow.spacing = 4
313 ramField.target = self
314 ramField.action = #selector(ramChanged)
315 ramField.widthAnchor.constraint(equalToConstant: 60).isActive = true
316 let ramRow = NSStackView(views: [ramField, NSTextField(labelWithString: "GB")])
317 ramRow.spacing = 4
318 let ramNote = NSTextField(labelWithString:
319 "Decoded stand-in frames + player read-ahead; more = fewer hiccups.")
320 ramNote.textColor = .secondaryLabelColor
321 ramNote.font = .systemFont(ofSize: 11)
303 let reveal = NSButton(title: "Reveal Cache", target: self, action: #selector(revealCache))322 let reveal = NSButton(title: "Reveal Cache", target: self, action: #selector(revealCache))
304 reveal.controlSize = .small323 reveal.controlSize = .small
324 usageLabel.textColor = .secondaryLabelColor
325 usageLabel.font = .systemFont(ofSize: 11)
305 cacheLabel.textColor = .secondaryLabelColor326 cacheLabel.textColor = .secondaryLabelColor
306 cacheLabel.font = .systemFont(ofSize: 11)327 cacheLabel.font = .systemFont(ofSize: 11)
307328
308 let globalGrid = NSGridView(views: [329 let globalGrid = NSGridView(views: [
309 [label("Proxy cache limit"), cacheRow],330 [label("Proxy cache limit"), cacheRow],
331 [label("RAM frame cache"), ramRow],
332 [NSView(), ramNote],
333 [NSView(), usageLabel],
310 [NSView(), reveal],334 [NSView(), reveal],
311 [NSView(), cacheLabel],335 [NSView(), cacheLabel],
312 ])336 ])
...@@ -343,7 +367,9 @@ final class SettingsWindow: NSObject {...@@ -343,7 +367,9 @@ final class SettingsWindow: NSObject {
343 }367 }
344368
345 @objc private func sync() {369 @objc private func sync() {
346 guard window != nil else { return }370 // The estimate below walks each media's chunk dir — fine on demand,
371 // wasteful on every .projectChanged while the window is closed.
372 guard let window, window.isVisible else { return }
347 let project = DocumentContext.current.store.project373 let project = DocumentContext.current.store.project
348 // Rebuild the fps popup: presets plus the project's own rate when it's374 // Rebuild the fps popup: presets plus the project's own rate when it's
349 // not a preset (e.g. 29.50 fps probed from a screen recording).375 // not a preset (e.g. 29.50 fps probed from a screen recording).
...@@ -363,7 +389,14 @@ final class SettingsWindow: NSObject {...@@ -363,7 +389,14 @@ final class SettingsWindow: NSObject {
363 compsLabel.stringValue = project.compsFolder ?? "not set"389 compsLabel.stringValue = project.compsFolder ?? "not set"
364 let gb = UserDefaults.standard.integer(forKey: "maxCacheGB")390 let gb = UserDefaults.standard.integer(forKey: "maxCacheGB")
365 cacheField.stringValue = "\(gb > 0 ? gb : 50)"391 cacheField.stringValue = "\(gb > 0 ? gb : 50)"
366 cacheLabel.stringValue = "Cache: \(MediaPipeline.shared.cacheRoot.path)"392 ramField.stringValue = "\(FrameCache.ramGB)"
393 let pipeline = MediaPipeline.shared
394 let est = DocumentContext.current.chunks.optimizeEstimate(for: project)
395 usageLabel.stringValue = String(
396 format: "Using %.1f of %.0f GB · this project fully optimized ≈ %.1f GB (%.1f GB built)",
397 Double(pipeline.ledgerBytes) / 1e9, Double(pipeline.maxCacheBytes) / 1e9,
398 Double(est.total) / 1e9, Double(est.built) / 1e9)
399 cacheLabel.stringValue = "Cache: \(pipeline.cacheRoot.path)"
367 }400 }
368401
369 @objc private func fpsChanged() {402 @objc private func fpsChanged() {
...@@ -393,7 +426,14 @@ final class SettingsWindow: NSObject {...@@ -393,7 +426,14 @@ final class SettingsWindow: NSObject {
393 @objc private func cacheChanged() {426 @objc private func cacheChanged() {
394 let gb = Int(cacheField.stringValue) ?? 50427 let gb = Int(cacheField.stringValue) ?? 50
395 UserDefaults.standard.set(max(1, gb), forKey: "maxCacheGB")428 UserDefaults.standard.set(max(1, gb), forKey: "maxCacheGB")
396 MediaPipeline.shared.evictIfNeeded()429 MediaPipeline.shared.evictIfNeeded(reconcile: true)
430 sync()
431 }
432 @objc private func ramChanged() {
433 let gb = Int(ramField.stringValue) ?? 2
434 UserDefaults.standard.set(max(1, gb), forKey: "maxRAMGB")
435 FrameCache.shared.refreshBudget()
436 sync()
397 }437 }
398 @objc private func revealCache() {438 @objc private func revealCache() {
399 NSWorkspace.shared.activateFileViewerSelecting([MediaPipeline.shared.cacheRoot])439 NSWorkspace.shared.activateFileViewerSelecting([MediaPipeline.shared.cacheRoot])
sequencer/Sources/Sequencer/Transcript.swift created+469
...@@ -0,0 +1,469 @@
1import AppKit
2
3// Clover-recorder transcript integration: when a `cam.mov` has a sibling
4// `transcript.json` (the format the recorder writes), the viewer shows
5// synchronized two-line karaoke captions over the picture.
6//
7// All transcript times are in the media's OWN (source) timebase — seconds from
8// the start of cam.mov — so a clip's `sourceTime(at:)` maps the playhead into
9// them, respecting trim (srcIn) and speed.
10
11/// One spoken word with its timing (source seconds) and which segment
12/// (sentence) it came from — segment changes force a line break so sentences
13/// don't run together.
14struct TranscriptWord {
15 var text: String
16 var start: Double
17 var end: Double
18 var seg: Int
19}
20
21/// A parsed clover `transcript.json`: words flattened across segments in time
22/// order. `key` is the file path — its identity for layout caching.
23struct Transcript {
24 var words: [TranscriptWord]
25 var key: String
26
27 // MARK: Decoding shapes (tolerant — unknown/missing fields are skipped)
28
29 private struct RawWord: Decodable { var word: String?; var start: Double?; var end: Double? }
30 private struct RawSeg: Decodable {
31 var start: Double?; var end: Double?; var text: String?; var words: [RawWord]?
32 }
33 private struct RawTranscript: Decodable { var segments: [RawSeg]? }
34
35 /// Parse raw JSON. Returns nil if there's nothing usable to show.
36 static func parse(_ data: Data, key: String) -> Transcript? {
37 guard let raw = try? JSONDecoder().decode(RawTranscript.self, from: data),
38 let segments = raw.segments else { return nil }
39 var words: [TranscriptWord] = []
40 for (si, seg) in segments.enumerated() {
41 let wordList = seg.words ?? []
42 var added = false
43 for rw in wordList {
44 guard let start = rw.start else { continue }
45 let text = (rw.word ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
46 guard !text.isEmpty else { continue }
47 words.append(TranscriptWord(text: text, start: start,
48 end: max(start, rw.end ?? start), seg: si))
49 added = true
50 }
51 // A segment with no per-word timing still shows as one block spanning
52 // the segment, so nothing goes silently missing.
53 if !added, let start = seg.start {
54 let text = (seg.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
55 if !text.isEmpty {
56 words.append(TranscriptWord(text: text, start: start,
57 end: max(start, seg.end ?? start), seg: si))
58 }
59 }
60 }
61 guard !words.isEmpty else { return nil }
62 // Recorder output is monotonic in start time; sort defensively (stable,
63 // so an already-sorted file keeps its segment contiguity) so the
64 // active-word binary search below is always valid.
65 words.sort { $0.start < $1.start }
66 return Transcript(words: words, key: key)
67 }
68
69 // MARK: Sibling-file loading (cached)
70
71 private static var cache: [String: Transcript?] = [:]
72
73 /// The transcript for a media file, or nil unless it is named `cam.mov` and
74 /// has a readable `transcript.json` in the same folder. Cached by path so
75 /// the per-frame visibility probe is cheap.
76 static func load(forVideo url: URL) -> Transcript? {
77 guard url.lastPathComponent.lowercased() == "cam.mov" else { return nil }
78 let path = url.deletingLastPathComponent()
79 .appendingPathComponent("transcript.json").path
80 if let hit = cache[path] { return hit }
81 let parsed = (try? Data(contentsOf: URL(fileURLWithPath: path)))
82 .flatMap { parse($0, key: path) }
83 cache[path] = parsed
84 return parsed
85 }
86
87 /// Drop cached parses so a re-imported/edited transcript is re-read.
88 static func clearCache() { cache.removeAll() }
89
90 // MARK: Lookup
91
92 /// Index of the word active at source time `s` — the last word that has
93 /// started. A word stays "current" until the next one begins, so there are
94 /// no gaps. Returns -1 before the first word.
95 func activeIndex(at s: Double) -> Int {
96 var lo = 0, hi = words.count - 1, res = -1
97 while lo <= hi {
98 let mid = (lo + hi) / 2
99 if words[mid].start <= s { res = mid; lo = mid + 1 } else { hi = mid - 1 }
100 }
101 return res
102 }
103}
104
105/// Two-line synchronized captions drawn over the viewer: white text on a black
106/// clipped background, the active word in magenta, always on the TOP line.
107/// New lines rise from the bottom and leave above the top as speech advances.
108///
109/// **The whole thing is a pure function of the playhead.** Nothing here uses a
110/// timer, Core Animation, or wall-clock tween: the vertical scroll offset is
111/// computed straight from the source time, so stepping frame-by-frame steps the
112/// animation and seeking lands with no motion (exactly what was asked for).
113final class SubtitleOverlay: NSView {
114 var ctx: DocumentContext = .headless {
115 didSet {
116 guard oldValue !== ctx else { return }
117 oldValue.notify.removeObserver(self, name: .playheadChanged, object: nil)
118 ctx.notify.addObserver(self, selector: #selector(sync),
119 name: .playheadChanged, object: nil)
120 sync()
121 }
122 }
123 private var store: Store { ctx.store }
124 private var playback: PlaybackController { ctx.playback }
125 private var session: SessionState { ctx.session }
126
127 /// The transcript being shown and the source time to render it at — both
128 /// recomputed on every playhead move in `sync()`.
129 private var transcript: Transcript?
130 private var sourceTime: Double = 0
131 /// The clip the transcript is being shown for — kept so a word click can map
132 /// the word's source time back to a timeline moment (inverse of the scroll).
133 private var clip: Clip?
134
135 /// Laid-out lines (each an array of positioned words) cached per transcript
136 /// + box width. `lineOfWord[i]` is the line word `i` landed on.
137 private struct LaidWord { var index: Int; var text: String; var x: CGFloat; var width: CGFloat }
138 private var lines: [[LaidWord]] = []
139 private var lineOfWord: [Int] = []
140 /// Natural content width (points) of each laid-out line — drives the black
141 /// backing that hugs the visible two lines.
142 private var lineWidths: [CGFloat] = []
143 private var laidKey: String?
144 private var laidWidth: CGFloat = -1
145
146 /// What to mark at the current source time: a spoken word painted magenta,
147 /// or — during a real pause between two words — a magenta caret sitting
148 /// after the last spoken word, signalling that nothing is being said.
149 private enum Mark { case word(Int); case caret(after: Int); case none }
150
151 /// Gaps up to this (seconds) hold the previous word lit instead of blinking
152 /// off; longer gaps show the caret. Small inter-word silences are noise and
153 /// shouldn't flicker the highlight.
154 private static let extendGap = 0.25
155
156 private static let magenta = NSColor(srgbRed: 1.0, green: 0.22, blue: 0.86, alpha: 1)
157
158 override init(frame: NSRect) {
159 super.init(frame: frame)
160 wantsLayer = true
161 layer?.backgroundColor = NSColor.clear.cgColor
162 isHidden = true
163 NotificationCenter.default.addObserver(self, selector: #selector(sync),
164 name: .projectChanged, object: nil)
165 NotificationCenter.default.addObserver(self, selector: #selector(sync),
166 name: .viewOptionsChanged, object: nil)
167 NotificationCenter.default.addObserver(self, selector: #selector(sync),
168 name: .viewerNeedsRefresh, object: nil)
169 ctx.notify.addObserver(self, selector: #selector(sync),
170 name: .playheadChanged, object: nil)
171 }
172 required init?(coder: NSCoder) { fatalError() }
173
174 override var isFlipped: Bool { true } // y grows downward: top line at small y
175
176 // Swallow clicks only when they land on a word — a word click scrubs the
177 // playhead to that word (see `mouseDown`). Clicks on the box's gaps/padding
178 // pass through to the cell beneath so clip selection still works there.
179 override func hitTest(_ point: NSPoint) -> NSView? {
180 guard !isHidden, transcript != nil else { return nil }
181 return wordIndex(atLocal: convert(point, from: superview)) != nil ? self : nil
182 }
183
184 /// Scrub to the clicked word's start.
185 override func mouseDown(with event: NSEvent) {
186 let local = convert(event.locationInWindow, from: nil)
187 guard let transcript, let clip,
188 let idx = wordIndex(atLocal: local) else { return }
189 playback.seek(to: clip.timelineTime(forSource: transcript.words[idx].start))
190 }
191
192 /// The transcript word whose drawn rect contains `p` (this view's flipped,
193 /// local coords), or nil. Mirrors `draw`'s layout so hit-testing and painting
194 /// never disagree.
195 private func wordIndex(atLocal p: NSPoint) -> Int? {
196 guard transcript != nil, bounds.width > 40 else { return nil }
197 rebuildLayoutIfNeeded()
198 guard !lines.isEmpty else { return nil }
199 let m = Metrics(width: bounds.width)
200 // Only the visible two-line band is clickable (matches what's drawn).
201 guard p.y >= m.vpad, p.y <= bounds.height - m.vpad else { return nil }
202 let scroll = scrollLines(at: sourceTime)
203 for (li, line) in lines.enumerated() {
204 let topY = m.vpad + (CGFloat(li) - scroll) * m.lineH
205 guard p.y >= topY, p.y < topY + m.lineH else { continue }
206 for lw in line where p.x >= lw.x && p.x < lw.x + lw.width {
207 return lw.index
208 }
209 }
210 return nil
211 }
212
213 // MARK: Geometry
214
215 /// Box metrics for a given width. Font scales gently with the box so the
216 /// captions stay legible on small viewers without ballooning on large ones.
217 struct Metrics {
218 let fontSize, lineH, vpad, hpad, height: CGFloat
219 init(width: CGFloat) {
220 fontSize = min(26, max(14, width / 30))
221 lineH = (fontSize * 1.32).rounded(.up)
222 vpad = (lineH * 0.34).rounded()
223 hpad = 16
224 height = lineH * 2 + vpad * 2
225 }
226 }
227
228 private static func font(_ size: CGFloat) -> NSFont {
229 .systemFont(ofSize: size, weight: .semibold)
230 }
231
232 /// Where the caption box sits inside the viewer: centred horizontally, near
233 /// the bottom. Sized only from the viewer bounds, so the parent can position
234 /// it in `layout()` without knowing the content.
235 func preferredFrame(in bounds: NSRect) -> NSRect {
236 let w = min(max(bounds.width * 0.72, 260), 860)
237 let m = Metrics(width: w)
238 let x = ((bounds.width - w) / 2).rounded()
239 let margin = max(16, bounds.height * 0.045)
240 let y = max(m.vpad, (bounds.height - m.height - margin).rounded())
241 return NSRect(x: x, y: y, width: w, height: m.height)
242 }
243
244 // MARK: State
245
246 /// Recompute which transcript/clip is under the playhead and at what source
247 /// time, then redraw. Hidden whenever the toggle is off or nothing under the
248 /// playhead carries a transcript.
249 @objc func sync() {
250 guard session.subtitlesEnabled, let (clip, media) = transcriptClipUnderPlayhead() else {
251 if !isHidden { isHidden = true }
252 transcript = nil
253 self.clip = nil
254 return
255 }
256 transcript = Transcript.load(forVideo: media.url)
257 self.clip = clip
258 sourceTime = max(0, clip.sourceTime(at: playback.playhead))
259 isHidden = transcript == nil
260 needsDisplay = true
261 }
262
263 /// The visible video clip under the playhead whose media is a transcript-
264 /// bearing `cam.mov`. Mirrors the viewer's own hide/focus visibility rule;
265 /// the Priority pane wins when it qualifies.
266 private func transcriptClipUnderPlayhead() -> (Clip, MediaItem)? {
267 let project = store.project
268 let t = playback.playhead
269 let focusActive = !session.focusedTracks.isEmpty || session.fusionFocus
270 func visible(_ ref: TrackRef) -> Bool {
271 focusActive ? session.focusedTracks.contains(ref)
272 : !session.hiddenTracks.contains(ref)
273 }
274 var found: (TrackRef, Clip, MediaItem)?
275 for ref in project.laneRefs where visible(ref) {
276 guard let clip = project.clipAt(track: ref, time: t, kind: .video),
277 let m = project.media(clip.mediaId),
278 Transcript.load(forVideo: m.url) != nil else { continue }
279 if ref == session.priorityPane { return (clip, m) }
280 if found == nil { found = (ref, clip, m) }
281 }
282 return found.map { ($0.1, $0.2) }
283 }
284
285 // MARK: Layout
286
287 /// Wrap the transcript into center-aligned lines that fit the box, breaking
288 /// at segment (sentence) boundaries. Cached until the box width or the
289 /// transcript changes.
290 private func rebuildLayoutIfNeeded() {
291 guard let transcript else { lines = []; lineOfWord = []; lineWidths = []; return }
292 let m = Metrics(width: bounds.width)
293 if laidKey == transcript.key && abs(laidWidth - bounds.width) < 0.5 { return }
294 laidKey = transcript.key
295 laidWidth = bounds.width
296
297 let font = Self.font(m.fontSize)
298 let attrs: [NSAttributedString.Key: Any] = [.font: font]
299 func measure(_ s: String) -> CGFloat { (s as NSString).size(withAttributes: attrs).width }
300 let space = measure(" ")
301 let maxW = bounds.width - m.hpad * 2
302
303 var built: [[LaidWord]] = []
304 var widths: [CGFloat] = []
305 var lineIdx = [Int](repeating: 0, count: transcript.words.count)
306 var cur: [LaidWord] = []
307 var penX: CGFloat = 0
308
309 func flush() {
310 guard !cur.isEmpty else { return }
311 let lineW = (cur.last?.x ?? 0) + (cur.last?.width ?? 0)
312 let off = (m.hpad + max(0, (maxW - lineW) / 2)).rounded()
313 built.append(cur.map { LaidWord(index: $0.index, text: $0.text,
314 x: $0.x + off, width: $0.width) })
315 widths.append(lineW)
316 cur = []
317 penX = 0
318 }
319
320 for (gi, w) in transcript.words.enumerated() {
321 let width = measure(w.text)
322 let newSegment = gi > 0 && w.seg != transcript.words[gi - 1].seg
323 let x = cur.isEmpty ? 0 : penX + space
324 if newSegment || (!cur.isEmpty && x + width > maxW) {
325 flush()
326 }
327 let placeX = cur.isEmpty ? 0 : penX + space
328 cur.append(LaidWord(index: gi, text: w.text, x: placeX, width: width))
329 penX = placeX + width
330 lineIdx[gi] = built.count // the line this word will land on once flushed
331 }
332 flush()
333 lines = built
334 lineOfWord = lineIdx
335 lineWidths = widths
336 }
337
338 /// Content width (points) to draw the black backing at for a given vertical
339 /// scroll. At rest on line `k` this is the wider of the two visible lines
340 /// (`k` and `k+1`); mid-slide it blends the outgoing and incoming pairs, so
341 /// the box width eases in lockstep with the scroll — same playhead-driven,
342 /// frame-steppable motion, no Core Animation.
343 private func contentWidth(atScroll s: CGFloat) -> CGFloat {
344 guard !lineWidths.isEmpty else { return 0 }
345 func pair(_ k: Int) -> CGFloat {
346 let a = (k >= 0 && k < lineWidths.count) ? lineWidths[k] : 0
347 let b = (k + 1 >= 0 && k + 1 < lineWidths.count) ? lineWidths[k + 1] : 0
348 return max(a, b)
349 }
350 let k = Int(s.rounded(.down))
351 let f = s - CGFloat(k)
352 return pair(k) + (pair(k + 1) - pair(k)) * f
353 }
354
355 /// Continuous vertical scroll, in line units, as a pure function of source
356 /// time. Holds on the active word's line, then slides up over ~0.3s when a
357 /// new line begins — so the focused word slides into the top row and stays
358 /// there. Interpolating over time (not a CA animation) is what makes it
359 /// frame-steppable and animation-free on a seek.
360 private func scrollLines(at s: Double) -> CGFloat {
361 guard let transcript, !lineOfWord.isEmpty else { return 0 }
362 let i = transcript.activeIndex(at: s)
363 guard i >= 0 else { return 0 }
364 let cur = lineOfWord[i]
365 let prev = i > 0 ? lineOfWord[i - 1] : cur
366 if cur == prev { return CGFloat(cur) }
367 let startI = transcript.words[i].start
368 let nextStart = i + 1 < transcript.words.count
369 ? transcript.words[i + 1].start : transcript.words[i].end
370 let slide = min(0.30, max(0.0001, nextStart - startI))
371 let t = min(1, max(0, (s - startI) / slide))
372 let e = t * t * (3 - 2 * t) // smoothstep
373 return CGFloat(prev) + (CGFloat(cur) - CGFloat(prev)) * e
374 }
375
376 /// Resolve what to highlight at source time `s`. While a word is being
377 /// spoken it lights up. In the gap after it: a *small* gap holds the word lit
378 /// right up to the next one (so brief silences don't flicker); a *longer* gap
379 /// shows a caret between the two words, marking the pause. Trailing silence
380 /// after the final word fades to nothing.
381 private func mark(at s: Double) -> Mark {
382 guard let transcript else { return .none }
383 let i = transcript.activeIndex(at: s)
384 guard i >= 0 else { return .none }
385 let w = transcript.words[i]
386 if s <= w.end { return .word(i) } // still being spoken
387 if i + 1 < transcript.words.count {
388 let gap = transcript.words[i + 1].start - w.end
389 return gap <= Self.extendGap ? .word(i) // tiny gap: extend it
390 : .caret(after: i) // real pause: caret
391 }
392 return s <= w.end + 0.05 ? .word(i) : .none // trailing silence
393 }
394
395 // MARK: Draw
396
397 override func draw(_ dirty: NSRect) {
398 guard transcript != nil, bounds.width > 40 else { return }
399 rebuildLayoutIfNeeded()
400 guard !lines.isEmpty else { return }
401 let m = Metrics(width: bounds.width)
402
403 let font = Self.font(m.fontSize)
404 let scroll = scrollLines(at: sourceTime)
405 let markResult = mark(at: sourceTime)
406 var highlight = -1
407 if case .word(let idx) = markResult { highlight = idx }
408
409 // Black rounded backing, sized to hug the two visible lines and centred
410 // in the (fixed, transparent) container — width eases with the scroll.
411 let boxW = min(bounds.width, (contentWidth(atScroll: scroll) + m.hpad * 2).rounded())
412 let boxX = ((bounds.width - boxW) / 2).rounded()
413 let boxRect = NSRect(x: boxX, y: 0, width: boxW, height: bounds.height)
414 let bg = NSBezierPath(roundedRect: boxRect, xRadius: 10, yRadius: 10)
415 NSColor(calibratedWhite: 0, alpha: 0.8).setFill()
416 bg.fill()
417 NSGraphicsContext.saveGraphicsState()
418 bg.addClip()
419 // Clip text to the inner TWO-line band (inset by the vertical padding),
420 // not the full padded box. This is what keeps the focused word pinned to
421 // the top line: without it, a settled neighbour line's descenders bleed
422 // into the top/bottom padding and the active line reads as the 2nd row.
423 // Lines still animate through this band — they're simply cut off cleanly
424 // at its top/bottom edges as they rise away / come up from below. The
425 // rounded-box clip above also trims lines sliding through a narrower box.
426 NSBezierPath(rect: NSRect(x: 0, y: m.vpad,
427 width: bounds.width,
428 height: bounds.height - m.vpad * 2)).addClip()
429
430 // Vertical inset so the glyphs sit centred in their line box.
431 let glyphH = font.ascender - font.descender
432 let textInset = ((m.lineH - glyphH) / 2).rounded()
433
434 // A caret (magenta cursor) sits just after the last spoken word during a
435 // real pause. Resolve its line/x from that word's laid-out position.
436 var caretLine = -1
437 var caretX: CGFloat = 0
438 if case .caret(let after) = markResult, after < lineOfWord.count {
439 let li = lineOfWord[after]
440 if li < lines.count, let prev = lines[li].first(where: { $0.index == after }) {
441 caretLine = li
442 let rightEdge = prev.x + prev.width
443 // Centre the caret in the gap to the next word when it shares
444 // this line; if the next word wrapped away, sit just past this one.
445 if let next = lines[li].first(where: { $0.index == after + 1 }) {
446 caretX = ((rightEdge + next.x) / 2).rounded()
447 } else {
448 caretX = (rightEdge + 3).rounded()
449 }
450 }
451 }
452
453 for (li, line) in lines.enumerated() {
454 let topY = m.vpad + (CGFloat(li) - scroll) * m.lineH
455 if topY > bounds.height || topY + m.lineH < 0 { continue } // fully clipped
456 let baselineY = topY + textInset
457 for lw in line {
458 let color = lw.index == highlight ? Self.magenta : NSColor.white
459 let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: color]
460 (lw.text as NSString).draw(at: NSPoint(x: lw.x, y: baselineY), withAttributes: attrs)
461 }
462 if li == caretLine {
463 Self.magenta.setFill()
464 NSBezierPath(rect: NSRect(x: caretX - 1, y: baselineY, width: 2, height: glyphH)).fill()
465 }
466 }
467 NSGraphicsContext.restoreGraphicsState()
468 }
469}
sequencer/Sources/Sequencer/TransportBar.swift+25-2
...@@ -90,6 +90,7 @@ final class TransportBar: NSView {...@@ -90,6 +90,7 @@ final class TransportBar: NSView {
90 private let shapesButton = InstantButton(title: "", target: nil, action: nil)90 private let shapesButton = InstantButton(title: "", target: nil, action: nil)
91 private let colorSwatch = InstantButton(title: "", target: nil, action: nil)91 private let colorSwatch = InstantButton(title: "", target: nil, action: nil)
92 private let snapButton = InstantButton(title: "", target: nil, action: nil)92 private let snapButton = InstantButton(title: "", target: nil, action: nil)
93 private let subtitleButton = InstantButton(title: "", target: nil, action: nil)
93 private let filmstripButton = InstantButton(title: "", target: nil, action: nil)94 private let filmstripButton = InstantButton(title: "", target: nil, action: nil)
94 private let viewerButton = InstantButton(title: "", target: nil, action: nil)95 private let viewerButton = InstantButton(title: "", target: nil, action: nil)
9596
...@@ -237,6 +238,11 @@ final class TransportBar: NSView {...@@ -237,6 +238,11 @@ final class TransportBar: NSView {
237 snapButton.target = self238 snapButton.target = self
238 snapButton.action = #selector(toggleSnap)239 snapButton.action = #selector(toggleSnap)
239 styleIconButton(snapButton, tip: "Snap (Y)")240 styleIconButton(snapButton, tip: "Snap (Y)")
241 subtitleButton.image = NSImage(systemSymbolName: "captions.bubble",
242 accessibilityDescription: "subtitles")
243 subtitleButton.target = self
244 subtitleButton.action = #selector(toggleSubtitles)
245 styleIconButton(subtitleButton, tip: "Subtitles")
240 filmstripButton.image = NSImage(systemSymbolName: "film",246 filmstripButton.image = NSImage(systemSymbolName: "film",
241 accessibilityDescription: "clip thumbnails")247 accessibilityDescription: "clip thumbnails")
242 filmstripButton.target = self248 filmstripButton.target = self
...@@ -257,8 +263,8 @@ final class TransportBar: NSView {...@@ -257,8 +263,8 @@ final class TransportBar: NSView {
257 heightSlider.toolTip = "Track height (⌥⌘= / ⌥⌘- / ⌥⌘0)"263 heightSlider.toolTip = "Track height (⌥⌘= / ⌥⌘- / ⌥⌘0)"
258 heightSlider.widthAnchor.constraint(equalToConstant: 80).isActive = true264 heightSlider.widthAnchor.constraint(equalToConstant: 80).isActive = true
259265
260 let rightStack = NSStackView(views: [netWarn, jobs, snapButton, filmstripButton,266 let rightStack = NSStackView(views: [netWarn, jobs, snapButton, subtitleButton,
261 viewerButton, heightSlider])267 filmstripButton, viewerButton, heightSlider])
262 rightStack.orientation = .horizontal268 rightStack.orientation = .horizontal
263 rightStack.spacing = 4269 rightStack.spacing = 4
264 rightStack.setCustomSpacing(8, after: netWarn)270 rightStack.setCustomSpacing(8, after: netWarn)
...@@ -390,6 +396,7 @@ final class TransportBar: NSView {...@@ -390,6 +396,7 @@ final class TransportBar: NSView {
390 }396 }
391397
392 @objc private func toggleSnap() { session.snapping.toggle() }398 @objc private func toggleSnap() { session.snapping.toggle() }
399 @objc private func toggleSubtitles() { session.subtitlesEnabled.toggle() }
393 @objc private func toggleFilmstrips() { session.showFilmstrips.toggle() }400 @objc private func toggleFilmstrips() { session.showFilmstrips.toggle() }
394401
395 @objc private func viewerClicked() {402 @objc private func viewerClicked() {
...@@ -614,6 +621,7 @@ final class TransportBar: NSView {...@@ -614,6 +621,7 @@ final class TransportBar: NSView {
614 shapesButton.alphaValue = canDraw ? 1 : 0.3621 shapesButton.alphaValue = canDraw ? 1 : 0.3
615 colorSwatch.layer?.backgroundColor = session.drawColor.cgColor622 colorSwatch.layer?.backgroundColor = session.drawColor.cgColor
616 highlight(snapButton, session.snapping)623 highlight(snapButton, session.snapping)
624 highlight(subtitleButton, session.subtitlesEnabled)
617 highlight(filmstripButton, session.showFilmstrips)625 highlight(filmstripButton, session.showFilmstrips)
618 highlight(viewerButton, session.previewsOnLeft626 highlight(viewerButton, session.previewsOnLeft
619 || ((window?.windowController as? SequencerWindowController)?.previewsArePopped ?? false))627 || ((window?.windowController as? SequencerWindowController)?.previewsArePopped ?? false))
...@@ -667,6 +675,21 @@ final class TransportBar: NSView {...@@ -667,6 +675,21 @@ final class TransportBar: NSView {
667 jobs.textColor = Theme.subtleLabel675 jobs.textColor = Theme.subtleLabel
668 jobs.stringValue = "⏸ \(total) chunk\(total == 1 ? "" : "s")"676 jobs.stringValue = "⏸ \(total) chunk\(total == 1 ? "" : "s")"
669 jobs.toolTip = "When paused, clip optimization happens only during playback."677 jobs.toolTip = "When paused, clip optimization happens only during playback."
678 } else if chunks.budgetStarved {
679 // The cache is at its cap and the project is bigger than it: the
680 // builder is deliberately NOT trying to finish the queue — it
681 // maintains a working set around where you play and edit. Say so,
682 // instead of dangling a queue count that will never drain.
683 jobs.textColor = Theme.subtleLabel
684 jobs.stringValue = building > 0
685 ? "cache full — optimizing \(building) near playhead"
686 : "cache full — optimizing on demand"
687 let gb = Double(MediaPipeline.shared.maxCacheBytes) / 1e9
688 jobs.toolTip = String(format:
689 "The proxy cache is at its %.0f GB cap, so this project can't be "
690 + "fully optimized at once. Proxies are kept where you play, edit, "
691 + "and land on clips, and rebuilt on demand elsewhere. Raise the "
692 + "cap in Settings to fit more. Click to pause optimization.", gb)
670 } else if queued == 0 {693 } else if queued == 0 {
671 jobs.textColor = .systemOrange694 jobs.textColor = .systemOrange
672 jobs.stringValue = "optimizing \(building) chunk\(building == 1 ? "" : "s")"695 jobs.stringValue = "optimizing \(building) chunk\(building == 1 ? "" : "s")"
sequencer/Sources/Sequencer/UITest.swift+46-1
...@@ -37,7 +37,8 @@ func runUITest() {...@@ -37,7 +37,8 @@ func runUITest() {
3737
38 // Coordinate helpers mirroring the view's layout constants.38 // Coordinate helpers mirroring the view's layout constants.
39 func x(_ sec: Double) -> CGFloat { timeline.testXFor(sec) }39 func x(_ sec: Double) -> CGFloat { timeline.testXFor(sec) }
40 func laneY(_ row: Int) -> CGFloat { 26 + CGFloat(row) * (64 + 4) + 4 + 32 } // lane mid40 // lane mid: ruler (26) + optimization strip (5) + row offset
41 func laneY(_ row: Int) -> CGFloat { 26 + 5 + CGFloat(row) * (64 + 4) + 4 + 32 }
41 // NSEvent locationInWindow is bottom-left origin; view is flipped & fills window.42 // NSEvent locationInWindow is bottom-left origin; view is flipped & fills window.
42 func winPoint(_ vx: CGFloat, _ vy: CGFloat) -> NSPoint { NSPoint(x: vx, y: 400 - vy) }43 func winPoint(_ vx: CGFloat, _ vy: CGFloat) -> NSPoint { NSPoint(x: vx, y: 400 - vy) }
4344
...@@ -487,6 +488,16 @@ func runUITest() {...@@ -487,6 +488,16 @@ func runUITest() {
487 let top = lum(comp27, 0.5, 0.15), bottom = lum(comp27, 0.5, 0.85)488 let top = lum(comp27, 0.5, 0.15), bottom = lum(comp27, 0.5, 0.85)
488 check(top >= 0 && top < 0.5 && bottom > 0.9,489 check(top >= 0 && top < 0.5 && bottom > 0.9,
489 "stroke drawn at top STAYS at top (top=\(top), bottom=\(bottom))")490 "stroke drawn at top STAYS at top (top=\(top), bottom=\(bottom))")
491
492 // 27e. The stroke rides the SHARED undo timeline: ⌘Z clears the drawing,
493 // ⌘⇧Z brings it back — no separate raster-undo lane.
494 store.undo()
495 let undone = lum(DocumentContext.headless.boards.composite(for: board27), 0.5, 0.15)
496 store.redo()
497 let redone = lum(DocumentContext.headless.boards.composite(for: board27), 0.5, 0.15)
498 check(undone > 0.9 && redone < 0.5,
499 "raster stroke undoes and redoes on the shared undo timeline "
500 + "(drawn=\(top) undone=\(undone) redone=\(redone))")
490 DocumentContext.headless.boards.saveRaster(nil, boardId: board27.id)501 DocumentContext.headless.boards.saveRaster(nil, boardId: board27.id)
491502
492 // 28. Overlaps ignore audio (layering is allowed).503 // 28. Overlaps ignore audio (layering is allowed).
...@@ -597,6 +608,40 @@ func runUITest() {...@@ -597,6 +608,40 @@ func runUITest() {
597 check(lcs.count == 2 && abs(lcs[1].start - 20) < 1e-6 && abs(lcs[1].duration - 15) < 1e-6,608 check(lcs.count == 2 && abs(lcs[1].start - 20) < 1e-6 && abs(lcs[1].duration - 15) < 1e-6,
598 "⌥← ripple-trims the left side to the playhead")609 "⌥← ripple-trims the left side to the playhead")
599610
611 // 31d-ripple. ⌥-drag a clip's IN edge = ripple trim: the clip's start stays
612 // pinned, its in-point re-trims, and the follower shifts to stay gapless.
613 // Media out-point is fixed, so the source range only loses (or regains)
614 // frames at the head.
615 var mri = ProjectModel(); mri.fps = 30; mri.media = [media]
616 mri.tracks = [Track(hue: 0.6)]
617 let ra = Clip(mediaId: media.id, track: .video(0), start: 20, srcIn: 10, duration: 20)
618 let rb = Clip(mediaId: media.id, track: .video(0), start: 40, srcIn: 0, duration: 20)
619 mri.clips = [ra, rb]
620 store.replaceForTest(mri)
621 store.selection = []
622 DocumentContext.headless.session.snapping = false
623 timeline.zoomToFit()
624 // Drag ra's in edge right by 10s: trim the head, pull rb in with it.
625 drag(from: winPoint(x(20) + 3, laneY(0)), to: winPoint(x(30) + 3, laneY(0)), flags: [.option])
626 let ra1 = clip(ra.id)!, rb1 = clip(rb.id)!
627 check(abs(ra1.start - 20) < 1e-6 && abs(ra1.srcIn - 20) < 0.5
628 && abs(ra1.duration - 10) < 0.5 && abs(rb1.start - 30) < 0.5,
629 "⌥ in-edge ripple: start pinned, head trimmed, follower pulled in "
630 + "(got start \(ra1.start), srcIn \(ra1.srcIn), dur \(ra1.duration), rb \(rb1.start))")
631 check(abs((ra1.srcIn + ra1.duration) - (ra.srcIn + ra.duration)) < 0.5,
632 "⌥ in-edge ripple keeps the source out-point fixed")
633 store.undo()
634 check(abs(clip(ra.id)!.srcIn - 10) < 1e-6 && abs(clip(rb.id)!.start - 40) < 1e-6,
635 "⌥ in-edge ripple undoes as one step")
636 // Drag left by 10s: extend the head, push the follower out.
637 drag(from: winPoint(x(20) + 3, laneY(0)), to: winPoint(x(10) + 3, laneY(0)), flags: [.option])
638 let ra2 = clip(ra.id)!, rb2 = clip(rb.id)!
639 check(abs(ra2.start - 20) < 1e-6 && abs(ra2.srcIn - 0) < 0.5
640 && abs(ra2.duration - 30) < 0.5 && abs(rb2.start - 50) < 0.5,
641 "⌥ in-edge ripple (drag left): head extended, follower pushed out "
642 + "(got start \(ra2.start), srcIn \(ra2.srcIn), dur \(ra2.duration), rb \(rb2.start))")
643 store.undo()
644
600 // 31e. Delete-the-space closes a blank gap at the playhead.645 // 31e. Delete-the-space closes a blank gap at the playhead.
601 var mb = ProjectModel(); mb.fps = 30; mb.media = [media]646 var mb = ProjectModel(); mb.fps = 30; mb.media = [media]
602 mb.tracks = [Track(hue: 0.4)]647 mb.tracks = [Track(hue: 0.4)]
sequencer/Sources/Sequencer/ViewerGridView.swift+121-15
...@@ -20,6 +20,7 @@ final class ViewerGridView: NSView {...@@ -20,6 +20,7 @@ final class ViewerGridView: NSView {
20 // and render nothing but the black cell background.20 // and render nothing but the black cell background.
21 for c in cells.values { c.ctx = ctx }21 for c in cells.values { c.ctx = ctx }
22 fusionCell?.ctx = ctx22 fusionCell?.ctx = ctx
23 subtitles.ctx = ctx
23 }24 }
24 }25 }
25 private var store: Store { ctx.store }26 private var store: Store { ctx.store }
...@@ -46,6 +47,11 @@ final class ViewerGridView: NSView {...@@ -46,6 +47,11 @@ final class ViewerGridView: NSView {
46 /// one-click way back). Only shown when there are no panes at all.47 /// one-click way back). Only shown when there are no panes at all.
47 private let placeholder = ViewerPlaceholder()48 private let placeholder = ViewerPlaceholder()
4849
50 /// Synchronized clover-transcript captions, drawn on top of the cells.
51 /// Positions itself (via `preferredFrame`) but hides itself unless a
52 /// transcript-bearing clip is under the playhead and captions are enabled.
53 private let subtitles = SubtitleOverlay()
54
49 override init(frame: NSRect) {55 override init(frame: NSRect) {
50 super.init(frame: frame)56 super.init(frame: frame)
51 wantsLayer = true57 wantsLayer = true
...@@ -76,6 +82,11 @@ final class ViewerGridView: NSView {...@@ -76,6 +82,11 @@ final class ViewerGridView: NSView {
76 placeholder.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 16),82 placeholder.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 16),
77 placeholder.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -16),83 placeholder.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -16),
78 ])84 ])
85 // Captions ride on top of the cells. Added last so later cells (inserted
86 // `.below`) stay under it; positioned in `layout()`.
87 subtitles.ctx = ctx
88 addSubview(subtitles)
89
79 // Dropping media anywhere in the empty viewer imports it (delegating to90 // Dropping media anywhere in the empty viewer imports it (delegating to
80 // the timeline's importer), so the "Drag files…" prompt is real.91 // the timeline's importer), so the "Drag files…" prompt is real.
81 registerForDraggedTypes([.fileURL])92 registerForDraggedTypes([.fileURL])
...@@ -304,6 +315,12 @@ final class ViewerGridView: NSView {...@@ -304,6 +315,12 @@ final class ViewerGridView: NSView {
304 override func layout() {315 override func layout() {
305 super.layout()316 super.layout()
306 applyFrames(animated: false, appearing: [])317 applyFrames(animated: false, appearing: [])
318 // Keep the caption box centred near the bottom and above every cell.
319 subtitles.frame = subtitles.preferredFrame(in: bounds)
320 if subviews.last !== subtitles {
321 subtitles.removeFromSuperview()
322 addSubview(subtitles)
323 }
307 }324 }
308325
309 /// Lay out the panes and animate them into place. Normally a justified-rows326 /// Lay out the panes and animate them into place. Normally a justified-rows
...@@ -967,11 +984,33 @@ final class ViewerCell: ViewerCellBase {...@@ -967,11 +984,33 @@ final class ViewerCell: ViewerCellBase {
967 /// those flash; instead we wait out a short grace period and only reveal it984 /// those flash; instead we wait out a short grace period and only reveal it
968 /// if the cell is still empty. Any resolved frame cancels it.985 /// if the cell is still empty. Any resolved frame cancels it.
969 private var pendingSpinner: DispatchWorkItem?986 private var pendingSpinner: DispatchWorkItem?
970 private func scheduleLoadingOverlay() {987 private var overlayText: String?
971 guard !overlayVisible, pendingSpinner == nil else { return }988
989 // Miss metrics: every spinner that actually shows is one recorded "miss",
990 // logged with a diagnosis of WHY the frame wasn't ready (see
991 // ChunkManager.missDiagnosis) and, on recovery, how long it lasted —
992 // so "I saw the loading screen" is answerable from the log.
993 private static var missCount = 0
994 private static var missSeconds = 0.0
995 private var missStart: Date?
996
997 private func scheduleLoadingOverlay(media: MediaItem?, clip: Clip?) {
998 guard overlayText == nil, pendingSpinner == nil else { return }
972 let work = DispatchWorkItem { [weak self] in999 let work = DispatchWorkItem { [weak self] in
973 self?.pendingSpinner = nil1000 guard let self else { return }
974 self?.showLoadingOverlay(true)1001 self.pendingSpinner = nil
1002 self.showOverlay("Loading Media…", spinning: true)
1003 self.missStart = Date()
1004 Self.missCount += 1
1005 if let media, let clip {
1006 let src = max(0, clip.sourceTime(at: self.playback.playhead))
1007 SeqLog.log("[miss] spinner ON %@ src=%.1f %@ (miss #%d)",
1008 media.displayName, src,
1009 self.chunks.missDiagnosis(media: media, sourceTime: src),
1010 Self.missCount)
1011 } else {
1012 SeqLog.log("[miss] spinner ON (no clip resolved) (miss #%d)", Self.missCount)
1013 }
975 }1014 }
976 pendingSpinner = work1015 pendingSpinner = work
977 DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: work)1016 DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: work)
...@@ -979,15 +1018,38 @@ final class ViewerCell: ViewerCellBase {...@@ -979,15 +1018,38 @@ final class ViewerCell: ViewerCellBase {
979 private func cancelLoadingOverlay() {1018 private func cancelLoadingOverlay() {
980 pendingSpinner?.cancel()1019 pendingSpinner?.cancel()
981 pendingSpinner = nil1020 pendingSpinner = nil
982 showLoadingOverlay(false)1021 if let start = missStart {
983 }1022 missStart = nil
9841023 let d = Date().timeIntervalSince(start)
985 private func showLoadingOverlay(_ show: Bool) {1024 Self.missSeconds += d
986 if show { layoutOverlay(in: bounds) }1025 SeqLog.log("[miss] spinner OFF after %.1fs (session: %d misses, %.0fs total)",
987 guard overlayVisible != show else { return }1026 d, Self.missCount, Self.missSeconds)
988 overlayVisible = show1027 }
989 overlayBg.isHidden = !show1028 hideOverlay()
990 if show {1029 }
1030
1031 /// A persistent (non-transient) overlay — media offline / failed to decode.
1032 /// No debounce and no spinner: it's a steady state, not a "wait a moment."
1033 private func showStateOverlay(_ text: String) {
1034 pendingSpinner?.cancel(); pendingSpinner = nil
1035 if let start = missStart { // spinner resolved INTO a steady state
1036 missStart = nil
1037 let d = Date().timeIntervalSince(start)
1038 Self.missSeconds += d
1039 SeqLog.log("[miss] spinner OFF after %.1fs → %@", d, text)
1040 }
1041 showOverlay(text, spinning: false)
1042 }
1043
1044 private func showOverlay(_ text: String, spinning: Bool) {
1045 guard overlayText != text else { return } // already showing this
1046 overlayText = text
1047 overlayVisible = true
1048 layoutOverlay(in: bounds)
1049 loadingText.string = text
1050 spinnerLayer.isHidden = !spinning
1051 overlayBg.isHidden = false
1052 if spinning {
991 if spinnerLayer.animation(forKey: "spin") == nil {1053 if spinnerLayer.animation(forKey: "spin") == nil {
992 let a = CABasicAnimation(keyPath: "transform.rotation.z")1054 let a = CABasicAnimation(keyPath: "transform.rotation.z")
993 a.fromValue = 01055 a.fromValue = 0
...@@ -1000,6 +1062,13 @@ final class ViewerCell: ViewerCellBase {...@@ -1000,6 +1062,13 @@ final class ViewerCell: ViewerCellBase {
1000 spinnerLayer.removeAnimation(forKey: "spin")1062 spinnerLayer.removeAnimation(forKey: "spin")
1001 }1063 }
1002 }1064 }
1065 private func hideOverlay() {
1066 guard overlayText != nil else { return }
1067 overlayText = nil
1068 overlayVisible = false
1069 overlayBg.isHidden = true
1070 spinnerLayer.removeAnimation(forKey: "spin")
1071 }
10031072
1004 private var currentClipId: UUID?1073 private var currentClipId: UUID?
10051074
...@@ -1285,17 +1354,54 @@ final class ViewerCell: ViewerCellBase {...@@ -1285,17 +1354,54 @@ final class ViewerCell: ViewerCellBase {
1285 && frontLayer.isReadyForDisplay1354 && frontLayer.isReadyForDisplay
1286 && onTime1355 && onTime
1287 var status = covered ? "" : "processing…"1356 var status = covered ? "" : "processing…"
1357 if !itemOK {
1358 // The player can't show this moment yet: start decoding the real
1359 // frame into the RAM cache now (from a chunk, rescue slice, or
1360 // playable original) so the next update can stand in with it —
1361 // exact where the filmstrip is a 240px thumb. Self-deduping.
1362 FrameCache.shared.warm(mediaKey: media.cacheKey, at: src,
1363 source: chunks.frameSource(media: media, sourceTime: src))
1364 }
1288 if itemOK {1365 if itemOK {
1289 frontLayer.isHidden = false1366 frontLayer.isHidden = false
1290 imageLayer.isHidden = true1367 imageLayer.isHidden = true
1291 cancelLoadingOverlay()1368 cancelLoadingOverlay()
1369 } else if let frame = FrameCache.shared.image(media: media, at: src) {
1370 // The RAM frame cache has the exact (full-quality) frame for this
1371 // moment — a warmed cut boundary, or a stand-in decoded on demand
1372 // below. Better than the 240px filmstrip thumb, and it makes the
1373 // hold across an item swap invisible.
1374 frontLayer.isHidden = true
1375 imageLayer.isHidden = false
1376 imageLayer.contents = frame
1377 cancelLoadingOverlay()
1378 if MediaPipeline.shared.isOffline(media) { status = "offline" }
1292 } else if let strip = MediaPipeline.shared.filmstripImage(for: media, at: src) {1379 } else if let strip = MediaPipeline.shared.filmstripImage(for: media, at: src) {
1293 // A filmstrip is a real frame for this moment: stand in with it so1380 // A filmstrip is a real frame for this moment: stand in with it so
1294 // the transition is filmstrip → video, never black.1381 // the transition is filmstrip → video, never black. If the original
1382 // is offline the cached strip still previews, but flag it in the
1383 // corner so it's clear playback/export won't work until it's back.
1295 frontLayer.isHidden = true1384 frontLayer.isHidden = true
1296 imageLayer.isHidden = false1385 imageLayer.isHidden = false
1297 imageLayer.contents = strip1386 imageLayer.contents = strip
1298 cancelLoadingOverlay()1387 cancelLoadingOverlay()
1388 if MediaPipeline.shared.isOffline(media) { status = "offline" }
1389 } else if MediaPipeline.shared.isOffline(media) {
1390 // The original file is gone (unmounted NAS / moved). Don't imply it's
1391 // loading — say so plainly, and don't spin forever.
1392 frontLayer.isHidden = true
1393 imageLayer.isHidden = true
1394 imageLayer.contents = nil
1395 showStateOverlay("Media Offline")
1396 status = "offline"
1397 } else if chunks.buildFailed(media: media, sourceTime: src) {
1398 // The proxy chunk hard-failed and the original isn't playable — this
1399 // frame genuinely can't be shown; surface it instead of spinning.
1400 frontLayer.isHidden = true
1401 imageLayer.isHidden = true
1402 imageLayer.contents = nil
1403 showStateOverlay("Can't Decode")
1404 status = "failed"
1299 } else {1405 } else {
1300 // No live video AND no stand-in. This is usually just a transient1406 // No live video AND no stand-in. This is usually just a transient
1301 // (an item swap or a seek that lands within a few frames), so do1407 // (an item swap or a seek that lands within a few frames), so do
...@@ -1303,7 +1409,7 @@ final class ViewerCell: ViewerCellBase {...@@ -1303,7 +1409,7 @@ final class ViewerCell: ViewerCellBase {
1303 // on screen and only escalate to the framed "Loading Media…"1409 // on screen and only escalate to the framed "Loading Media…"
1304 // overlay if the empty state actually persists (see1410 // overlay if the empty state actually persists (see
1305 // scheduleLoadingOverlay). That kills the paused spinner flashes.1411 // scheduleLoadingOverlay). That kills the paused spinner flashes.
1306 scheduleLoadingOverlay()1412 scheduleLoadingOverlay(media: media, clip: clip)
1307 status = ""1413 status = ""
1308 }1414 }
1309 setStatus(status)1415 setStatus(status)
sequencer/Sources/Sequencer/WindowController.swift+16-6
...@@ -167,11 +167,23 @@ final class SequencerWindowController: NSWindowController, NSWindowDelegate,...@@ -167,11 +167,23 @@ final class SequencerWindowController: NSWindowController, NSWindowDelegate,
167 }167 }
168 }168 }
169169
170 /// This project became frontmost — now (and only now) kick the whole-project
171 /// proxy pre-build so scrubbing anywhere is smooth. Deferring the fill to here
172 /// rather than document load is what stops state-restored *background* projects
173 /// from all transcoding their full proxy sets at once and overflowing the
174 /// shared cache (see DocumentContext.startServices). `ensure` is idempotent —
175 /// already-built chunks are skipped — so re-focusing a project is essentially
176 /// free. Ignore the popout window becoming main (it shares this ctx).
177 func windowDidBecomeMain(_ notification: Notification) {
178 guard (notification.object as? NSWindow) === window else { return }
179 ctx.chunks.ensure(for: ctx.store.project)
180 }
181
170 // MARK: - Menu validation (per-document items)182 // MARK: - Menu validation (per-document items)
171183
172 func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {184 func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
173 switch menuItem.action {185 switch menuItem.action {
174 case #selector(undo): return store.canUndo || StoryboardEditor.shared.canUndoRaster186 case #selector(undo): return store.canUndo
175 case #selector(redo): return store.canRedo187 case #selector(redo): return store.canRedo
176 case #selector(deleteSelected), #selector(rippleDeleteSelected):188 case #selector(deleteSelected), #selector(rippleDeleteSelected):
177 return !store.selection.isEmpty189 return !store.selection.isEmpty
...@@ -211,11 +223,9 @@ final class SequencerWindowController: NSWindowController, NSWindowDelegate,...@@ -211,11 +223,9 @@ final class SequencerWindowController: NSWindowController, NSWindowDelegate,
211223
212 // MARK: - Edit / Clip actions224 // MARK: - Edit / Clip actions
213225
214 @objc func undo() {226 // Drawing edits now live on the same undo timeline as model edits, so ⌘Z /
215 if StoryboardEditor.shared.undoRasterIfKey() { return }227 // ⌘⇧Z route through the Store regardless of which window is focused.
216 if session.mainTool.isDraw, ctx.boards.undoLastStroke() { return }228 @objc func undo() { store.undo() }
217 store.undo()
218 }
219 @objc func redo() { store.redo() }229 @objc func redo() { store.redo() }
220 @objc func deselectAll() {230 @objc func deselectAll() {
221 store.selection = []231 store.selection = []
sequencer/Sources/Sequencer/main.swift+43
...@@ -6,16 +6,59 @@ if CommandLine.arguments.count >= 3, CommandLine.arguments[1] == "--selftest" {...@@ -6,16 +6,59 @@ if CommandLine.arguments.count >= 3, CommandLine.arguments[1] == "--selftest" {
6 exit(0)6 exit(0)
7}7}
88
9// Headless cache-budget test: sequencer --cachetest <file.sq> [capGB]
10if CommandLine.arguments.count >= 3, CommandLine.arguments[1] == "--cachetest" {
11 let cap = CommandLine.arguments.count >= 4 ? Int(CommandLine.arguments[3]) ?? 0 : 0
12 runCacheTest(path: CommandLine.arguments[2], capGB: cap)
13 exit(0)
14}
15
9if CommandLine.arguments.contains("--uitest") {16if CommandLine.arguments.contains("--uitest") {
10 _ = NSApplication.shared // AppKit needs an app instance for views/windows17 _ = NSApplication.shared // AppKit needs an app instance for views/windows
11 MainActor.assumeIsolated { runUITest() }18 MainActor.assumeIsolated { runUITest() }
12}19}
1320
21// Diagnostic: bare AVPlayerLayer window on a stitched composition, using the
22// app's own bundle/signing — isolates layer rendering from the app machinery.
23if CommandLine.arguments.count >= 3, CommandLine.arguments[1] == "--layertest" {
24 let app = NSApplication.shared
25 app.setActivationPolicy(.regular)
26 MainActor.assumeIsolated {
27 runLayerTest(specs: Array(CommandLine.arguments.dropFirst(2)))
28 }
29 app.run()
30}
31
14if CommandLine.arguments.count >= 3, CommandLine.arguments[1] == "--perftest" {32if CommandLine.arguments.count >= 3, CommandLine.arguments[1] == "--perftest" {
15 _ = NSApplication.shared33 _ = NSApplication.shared
16 MainActor.assumeIsolated { runPerfTest(path: CommandLine.arguments[2]) }34 MainActor.assumeIsolated { runPerfTest(path: CommandLine.arguments[2]) }
17}35}
1836
37// pkill/SIGTERM (run.sh does one per rebuild) must also take the ffmpeg
38// children down — the default handler kills only this process, minting the
39// orphan encoders that exhaust VideoToolbox and black out the next launch.
40// A background queue, NOT main: a wedged main thread must not make the app
41// unkillable (pkill looked ignored while main was frozen).
42signal(SIGTERM, SIG_IGN)
43signal(SIGINT, SIG_IGN)
44let signalQueue = DispatchQueue(label: "sequencer.signals", qos: .userInteractive)
45let termSource = DispatchSource.makeSignalSource(signal: SIGTERM, queue: signalQueue)
46let intSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: signalQueue)
47for src in [termSource, intSource] {
48 src.setEventHandler {
49 MediaPipeline.terminateChildren()
50 exit(0)
51 }
52 src.resume()
53}
54
55HangMonitor.start()
56// SEQ_HANGTEST=1: deliberately stall the main thread once, to verify the
57// watchdog end-to-end (a [hang] line with this frame should hit the log).
58if ProcessInfo.processInfo.environment["SEQ_HANGTEST"] != nil {
59 DispatchQueue.main.asyncAfter(deadline: .now() + 2) { hangTestStall() }
60}
61
19let app = SeqApplication.shared62let app = SeqApplication.shared
20let delegate = AppDelegate()63let delegate = AppDelegate()
21app.delegate = delegate64app.delegate = delegate
sequencer/build.sh+3-3
...@@ -81,7 +81,7 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'...@@ -81,7 +81,7 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'
81 <key>CFBundleExecutable</key>81 <key>CFBundleExecutable</key>
82 <string>Sequencer</string>82 <string>Sequencer</string>
83 <key>CFBundleIdentifier</key>83 <key>CFBundleIdentifier</key>
84 <string>com.clover.Sequencer</string>84 <string>net.paperclover.Sequencer</string>
85 <key>CFBundlePackageType</key>85 <key>CFBundlePackageType</key>
86 <string>APPL</string>86 <string>APPL</string>
87 <key>CFBundleShortVersionString</key>87 <key>CFBundleShortVersionString</key>
...@@ -109,7 +109,7 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'...@@ -109,7 +109,7 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'
109 <string>Sequencer.ProjectDocument</string>109 <string>Sequencer.ProjectDocument</string>
110 <key>LSItemContentTypes</key>110 <key>LSItemContentTypes</key>
111 <array>111 <array>
112 <string>com.clover.sequencer.project</string>112 <string>net.paperclover.sequencer.project</string>
113 </array>113 </array>
114 <key>CFBundleTypeExtensions</key>114 <key>CFBundleTypeExtensions</key>
115 <array>115 <array>
...@@ -121,7 +121,7 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'...@@ -121,7 +121,7 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'
121 <array>121 <array>
122 <dict>122 <dict>
123 <key>UTTypeIdentifier</key>123 <key>UTTypeIdentifier</key>
124 <string>com.clover.sequencer.project</string>124 <string>net.paperclover.sequencer.project</string>
125 <key>UTTypeDescription</key>125 <key>UTTypeDescription</key>
126 <string>Sequencer Project</string>126 <string>Sequencer Project</string>
127 <key>UTTypeConformsTo</key>127 <key>UTTypeConformsTo</key>
sequencer/run.sh+13-2
...@@ -1,8 +1,19 @@...@@ -1,8 +1,19 @@
1#!/bin/sh1#!/bin/sh
2# Build and relaunch the app. Ships the RELEASE binary — the timeline is
3# measurably 4× slower under -Onone, so the app people actually run must be
4# optimized. Pass --debug to ship the debug build (e.g. when chasing a crash
5# with full assertions/symbols).
2set -e6set -e
3cd "$(dirname "$0")"7cd "$(dirname "$0")"
4swift build8CONF=release
9[ "$1" = "--debug" ] && CONF=debug
10swift build -c "$CONF"
5pkill -x Sequencer 2>/dev/null || true11pkill -x Sequencer 2>/dev/null || true
6cp .build/debug/Sequencer Sequencer.app/Contents/MacOS/Sequencer12# The app terminates its ffmpeg children on SIGTERM, but sweep any strays from
13# older builds anyway — orphaned encoders exhaust the shared VideoToolbox
14# session pool and the next launch's players silently render black.
15sleep 1
16pkill -9 -f "Library/Caches/Sequencer" 2>/dev/null || true
17cp ".build/$CONF/Sequencer" Sequencer.app/Contents/MacOS/Sequencer
7codesign --force --deep --sign "Sequencer Dev" Sequencer.app18codesign --force --deep --sign "Sequencer Dev" Sequencer.app
8open Sequencer.app19open Sequencer.app