Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions App/RecordScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ final class RecordScreenModel {
let store = drive.store
self.drive = drive
recorder = Recorder(driveFolder: {
store.beginAccess().map { access in DestinationAccess(folder: access.folder, release: { access.end() }) }
store.beginAccess()
}, preRollSeconds: PreRollSetting.load())
mixerAddress = UserDefaults.standard.string(forKey: Self.mixerAddressKey) ?? ""
devices = Self.availableDevices()
Expand All @@ -648,7 +648,7 @@ final class RecordScreenModel {
}

var takeTitle: String {
String(format: "Take %02d", recorder.currentShow?.takeCount ?? 0)
TakeMetadata.name(take: recorder.currentShow?.takeCount ?? 0)
}

func record() {
Expand Down
2 changes: 1 addition & 1 deletion App/ShowDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ struct ShowDetailView: View {
}
} header: {
HStack {
Text(String(format: "Take %02d", take.number))
Text(TakeMetadata.name(take: take.number))
Spacer()
Button("Delete…", role: .destructive) { deletingTake = take.number }
.buttonStyle(.borderless)
Expand Down
13 changes: 11 additions & 2 deletions Packages/ShowRecorderKit/Sources/Destinations/Destinations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ public enum DestinationKind: String, Codable, Sendable, Hashable, CaseIterable {
public init(index: Int) {
self = index == 0 ? .device : .drive
}

/// "Device" or "Drive", as the operator reads it.
public var name: String {
switch self {
case .device: "Device"
case .drive: "Drive"
}
}
}

/// Where the Drive folder's bookmark is kept between launches. One per device.
Expand Down Expand Up @@ -197,12 +205,13 @@ public struct DriveFolderStore: Sendable {
}
}

/// The Drive folder opened for one Take. Call `end()` when the Take is finalized.
/// The Drive folder opened for one Take, held until the Take is finalized and Repaired. `end()` lets it go
/// (for a bookmarked folder, its security scope).
public struct DriveAccess: Sendable {
public let folder: URL
private let stop: @Sendable () -> Void

init(folder: URL, stop: @escaping @Sendable () -> Void) {
public init(folder: URL, stop: @escaping @Sendable () -> Void = {}) {
self.folder = folder
self.stop = stop
}
Expand Down
4 changes: 2 additions & 2 deletions Packages/ShowRecorderKit/Sources/Recording/CopyCheck.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ public enum CopyCheck {
let deviceTakes = ShowList.takeMetadata(in: deviceShow).sorted { $0.take.take < $1.take.take }
if deviceTakes.isEmpty { return .failed(["The Device copy has no Takes to compare."]) }
for (deviceFolder, deviceTake) in deviceTakes {
let name = String(format: "Take %02d", deviceTake.take)
let name = TakeMetadata.name(take: deviceTake.take)
let driveFolder = driveShow.appending(path: deviceFolder.lastPathComponent, directoryHint: .isDirectory)
guard let driveTake = try? TakeMetadata.read(fromTakeFolder: driveFolder) else {
problems.append("\(name) is missing on the Drive.")
continue
}
let outcome = driveTake.outcome(ofCopy: .drive)
if outcome == .hasGaps || outcome == .repairFailed {
problems.append("\(name) on the Drive still has Gaps.")
problems.append("\(name) on the \(DestinationKind.drive.name) still \(CopyOutcome.hasGaps.word).")
}
for channel in deviceTake.usbChannels {
guard let match = driveTake.usbChannels.first(where: { $0.usbChannel == channel.usbChannel }) else {
Expand Down
12 changes: 2 additions & 10 deletions Packages/ShowRecorderKit/Sources/Recording/CopySummary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,8 @@ public struct CopySummary: Equatable, Sendable {
public let isProblem: Bool

public init(outcomes: [DestinationKind: CopyOutcome], dropoutCount: Int) {
func word(_ outcome: CopyOutcome) -> String {
switch outcome {
case .complete: "complete"
case .hasGaps: "has Gaps"
case .repaired: "Repaired"
case .repairFailed: "Repair failed"
}
}
var parts = [(DestinationKind.device, "Device"), (.drive, "Drive")].compactMap { kind, name in
outcomes[kind].map { "\(name) copy: \(word($0))" }
var parts = DestinationKind.allCases.compactMap { kind in
outcomes[kind].map { "\(kind.name) copy: \($0.word)" }
}
if outcomes.count == 1 {
parts[0] += outcomes[.device] == nil ? " (no device copy)" : " (no Drive)"
Expand Down
60 changes: 24 additions & 36 deletions Packages/ShowRecorderKit/Sources/Recording/Recorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public final class Recorder {
}

@ObservationIgnored private let deviceFolder: URL
@ObservationIgnored private let driveFolder: () -> DestinationAccess?
@ObservationIgnored private let driveFolder: () -> DriveAccess?
@ObservationIgnored private let now: () -> Date
@ObservationIgnored private let freeSpace: (URL) -> Int64
/// How often a running Take's Destinations are checked (`checkDestinations()`). Nil leaves it to the caller.
Expand All @@ -84,15 +84,15 @@ public final class Recorder {
@ObservationIgnored private var session: TakeSession?
@ObservationIgnored private var lastTake: LastTake?
/// Repairs ended Takes; its result is published as `lastTakeOutcomes` and `isRepairing`.
@ObservationIgnored private let repairQueue: RepairQueue
@ObservationIgnored private let repair: TakeRepair
@ObservationIgnored private var finishedCopies: [DestinationKind: CopyStatus] = [:]

/// - Parameters:
/// - deviceFolder: where the Device Copy of every Show goes.
/// - driveFolder: asked at each record press for the Drive folder; nil writes to the Device only.
public init(
deviceFolder: URL = Recorder.defaultDeviceFolder,
driveFolder: @escaping () -> DestinationAccess? = { nil },
driveFolder: @escaping () -> DriveAccess? = { nil },
now: @escaping () -> Date = Date.init,
freeSpace: @escaping (URL) -> Int64 = { DriveFolderStore.availableBytes(at: $0) },
preRollSeconds: Double = 0,
Expand All @@ -105,16 +105,16 @@ public final class Recorder {
savedChannelNames = SavedChannelNames.load(from: savedNames)
self.freeSpace = freeSpace
self.preRollSeconds = preRollSeconds
repairQueue = RepairQueue(freeSpace: freeSpace)
repair = TakeRepair(freeSpace: freeSpace)
self.makeStem = makeStem
self.deviceFolder = deviceFolder
self.driveFolder = driveFolder
self.now = now
repairQueue.onChange = { [unowned self] in
lastTakeOutcomes = repairQueue.outcomes
isRepairing = repairQueue.isRunning
repair.onChange = { [unowned self] in
lastTakeOutcomes = repair.outcomes
isRepairing = repair.isRunning
}
repairQueue.jobDone = { [unowned self] in regenerateReports() }
repair.jobDone = { [unowned self] in regenerateReports() }
// A Show left open by a crash or an earlier launch is the open Show again.
currentShow = Show.findOpen(in: deviceFolder)
channelNames = currentShow?.channelNames ?? [:]
Expand Down Expand Up @@ -323,14 +323,14 @@ public final class Recorder {
let kinds = copies.sorted { $0.index < $1.index }
guard !isRecording else { return .init(deleted: [], failed: kinds.map { .init(copy: $0, reason: "A Take is running.") }) }
let access = copies.contains(.drive) ? driveFolder() : nil
defer { access?.release() }
defer { access?.end() }
return ShowDeleter.delete(show: name, from: copies, device: deviceFolder, drive: access?.folder, openShow: currentShow?.name)
}

/// Checks that the Drive Copy of Show `show` is whole (see `CopyCheck`). Reads every Stem, off the main thread.
public func checkDriveCopy(of show: String) async -> CopyCheck.Result {
guard let access = driveFolder() else { return .failed(["The Drive isn't available."]) }
defer { access.release() }
defer { access.end() }
let device = deviceFolder, drive = access.folder
return await Task.detached { CopyCheck.verify(show: show, device: device, drive: drive) }.value
}
Expand All @@ -355,7 +355,7 @@ public final class Recorder {
let kinds = copies.sorted { $0.index < $1.index }
guard !isRecording else { return .init(deleted: [], failed: kinds.map { .init(copy: $0, reason: "A Take is running.") }) }
let access = copies.contains(.drive) ? driveFolder() : nil
defer { access?.release() }
defer { access?.end() }
return TakeDeleter.delete(take: take, ofShow: show, from: copies, device: deviceFolder, drive: access?.folder, openShow: currentShow?.name)
}

Expand All @@ -365,7 +365,7 @@ public final class Recorder {
let kinds = copies.sorted { $0.index < $1.index }
guard !isRecording else { return .init(deleted: [], failed: kinds.map { .init(copy: $0, reason: "A Take is running.") }) }
let access = copies.contains(.drive) ? driveFolder() : nil
defer { access?.release() }
defer { access?.end() }
return StemDeleter.delete(channel: channel, inTake: take, ofShow: show, from: copies, device: deviceFolder, drive: access?.folder, openShow: currentShow?.name)
}

Expand Down Expand Up @@ -393,7 +393,7 @@ public final class Recorder {
nextShow = nil
}
} catch {
drive?.release()
drive?.end()
throw error
}
if show.channelNames != channelNames { show.setChannelNames(channelNames) }
Expand Down Expand Up @@ -431,7 +431,7 @@ public final class Recorder {
show: show, folders: takeFolders, drive: takeFolders.count > 1 ? drive : nil, metadata: metadata, stems: specs,
info: info, capture: capture, preRollFrames: preRollFrames, makeStem: makeStem, freeSpace: freeSpace)
session.onCopyFailed = { [weak self, weak session] in self?.copyFailed(in: session) }
if takeFolders.count == 1 { drive?.release() }
if takeFolders.count == 1 { drive?.end() }
self.session = session
finishedCopies = [:]
takeMarkers = []
Expand Down Expand Up @@ -514,15 +514,15 @@ public final class Recorder {
/// The Drive is reached again for the rename and let go after. The outcome names any Copy that wasn't
/// updated. Waits for Repair first, which would otherwise write the old name back.
public func renameLastTakeMarker(at index: Int, to name: String) async -> MarkerRenamer.Outcome {
await repairQueue.wait()
await repair.wait()
guard !isRecording, let take = lastTake else { return MarkerRenamer.Outcome(result: .notRecording, renamed: [], failed: []) }
var copies: [DestinationKind: URL] = [.device: take.deviceFolder]
var access: DestinationAccess?
var access: DriveAccess?
if take.hadDrive {
access = driveFolder()
if let access { copies[.drive] = access.folder.appending(path: take.showName).appending(path: String(format: "Take %02d", take.takeNumber)) }
if let access { copies[.drive] = access.folder.appending(path: take.showName).appending(path: TakeMetadata.name(take: take.takeNumber)) }
}
defer { access?.release() }
defer { access?.end() }
var outcome = MarkerRenamer.rename(markerAt: index, to: name, in: copies)
if take.hadDrive, access == nil {
outcome.failed.append(.init(copy: .drive, reason: "The Drive isn't available, so its Copy still has the old name."))
Expand Down Expand Up @@ -612,7 +612,7 @@ public final class Recorder {
currentShow?.noteTakeEnded(at: now())
// The Takes are safe either way; the report and project are regenerated next time.
regenerateReports()
repairQueue.enqueue(finished.metadata, folders: finished.folders, holding: finished.access)
repair.enqueue(finished.metadata, folders: finished.folders, holding: finished.access)
nameChannels(of: finished.metadata)
}

Expand All @@ -630,14 +630,14 @@ public final class Recorder {
let previous = namingTask
namingTask = Task { @MainActor in
await previous?.value
await repairQueue.wait()
await repair.wait()
var copies: [DestinationKind: URL] = [.device: take.deviceFolder]
var access: DestinationAccess?
var access: DriveAccess?
if take.hadDrive {
access = driveFolder()
if let access { copies[.drive] = access.folder.appending(path: take.showName).appending(path: String(format: "Take %02d", take.takeNumber)) }
if let access { copies[.drive] = access.folder.appending(path: take.showName).appending(path: TakeMetadata.name(take: take.takeNumber)) }
}
defer { access?.release() }
defer { access?.end() }
_ = ChannelRenamer.rename(names: names, lateChannels: Set(late.keys), in: copies)
}
}
Expand All @@ -654,7 +654,7 @@ public final class Recorder {

/// Waits for Repair of the last Take to finish.
public func waitForRepair() async {
await repairQueue.wait()
await repair.wait()
await namingTask?.value
}
}
Expand All @@ -674,18 +674,6 @@ public enum RecorderError: Error, Equatable {
case takeRunning
}

/// Access to a Destination folder for the length of a Take. `release` ends it (for a Drive folder,
/// the security scope).
public struct DestinationAccess {
public let folder: URL
public let release: @MainActor () -> Void

public init(folder: URL, release: @escaping @MainActor () -> Void = {}) {
self.folder = folder
self.release = release
}
}

/// Everything the real-time callback touches, allocated when the recorder is Armed.
final class Capture: Sendable {
let meters: PeakMeters
Expand Down
85 changes: 0 additions & 85 deletions Packages/ShowRecorderKit/Sources/Recording/RepairQueue.swift

This file was deleted.

4 changes: 2 additions & 2 deletions Packages/ShowRecorderKit/Sources/Recording/Show.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,15 +174,15 @@ public struct Show: Sendable, Equatable {
/// the same name) and a folder for the current Take. Returns that Take folder.
mutating func joinDrive(_ parent: URL) throws -> URL {
try useDrive(parent)
let takeFolder = driveFolder!.appending(path: String(format: "Take %02d", takeCount), directoryHint: .isDirectory)
let takeFolder = driveFolder!.appending(path: TakeMetadata.name(take: takeCount), directoryHint: .isDirectory)
try FileManager.default.createDirectory(at: takeFolder, withIntermediateDirectories: false)
return takeFolder
}

/// Creates "Take NN" for the next Take in every Copy and returns the folders, the Device first.
mutating func createNextTakeFolder() throws -> [URL] {
takeCount += 1
let takeName = String(format: "Take %02d", takeCount)
let takeName = TakeMetadata.name(take: takeCount)
return try ([folder] + (driveFolder.map { [$0] } ?? [])).map { showFolder in
let takeFolder = showFolder.appending(path: takeName, directoryHint: .isDirectory)
try FileManager.default.createDirectory(at: takeFolder, withIntermediateDirectories: false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public enum ShowDetail {
public static func renameMarker(
at index: Int, to name: String, inTake take: Int, ofShow show: String, device: URL, drive: URL?
) -> MarkerRenamer.Outcome {
let takeFolder = String(format: "Take %02d", take)
let takeFolder = TakeMetadata.name(take: take)
var copies: [DestinationKind: URL] = [.device: device.appending(path: show).appending(path: takeFolder)]
if let drive {
let folder = drive.appending(path: show).appending(path: takeFolder)
Expand Down
Loading
Loading