perf: coalesce superseded queued playback seeks
This commit is contained in:
@@ -200,7 +200,8 @@ struct CastPlaybackCheck {
|
|||||||
require(!model.canStepFrame && model.bufferedRange == nil, "local decoder-only controls are not exposed while casting")
|
require(!model.canStepFrame && model.bufferedRange == nil, "local decoder-only controls are not exposed while casting")
|
||||||
require(model.setPlayback(playing: true), "remote play accepted")
|
require(model.setPlayback(playing: true), "remote play accepted")
|
||||||
try await until(model, "receiver play acknowledged") { model.state.status == PLAYER_PLAYING && model.playbackIntent == nil }
|
try await until(model, "receiver play acknowledged") { model.state.status == PLAYER_PLAYING && model.playbackIntent == nil }
|
||||||
require(model.seekRelative(5) && model.seekRelative(10), "rapid remote skips accumulate before acknowledgement")
|
for _ in 0..<64 { require(model.seekRelative(0.125), "rapid remote skips accumulate beyond queue capacity") }
|
||||||
|
require(model.seekRelative(7), "final remote skip retains accumulated target")
|
||||||
try await until(model, "receiver seek acknowledged") { !model.state.seek_pending && abs(model.state.position - 24.25) < 0.15 }
|
try await until(model, "receiver seek acknowledged") { !model.state.seek_pending && abs(model.state.position - 24.25) < 0.15 }
|
||||||
require(model.setPlayback(playing: false), "remote pause accepted")
|
require(model.setPlayback(playing: false), "remote pause accepted")
|
||||||
try await until(model, "receiver pause acknowledged") { model.state.status == PLAYER_PAUSED && model.playbackIntent == nil }
|
try await until(model, "receiver pause acknowledged") { model.state.status == PLAYER_PAUSED && model.playbackIntent == nil }
|
||||||
|
|||||||
@@ -399,6 +399,39 @@ struct CastSessionCheck {
|
|||||||
language: "en", label: "English")])
|
language: "en", label: "English")])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func seekCoalescing(identity: SecIdentity) async throws {
|
||||||
|
let peer = try Receiver(identity: identity)
|
||||||
|
try await peer.start()
|
||||||
|
defer { peer.stop() }
|
||||||
|
let clock = CastCheckClock()
|
||||||
|
let session = CastSession(sleep: { try await clock.sleep($0) })
|
||||||
|
var status: CastStatus?
|
||||||
|
var errors: [String] = []
|
||||||
|
session.onStatus = { status = $0 }
|
||||||
|
session.onError = { errors.append($0) }
|
||||||
|
try await session.connect(to: peer.device)
|
||||||
|
try await session.load(media(), title: "Seek burst", position: 0, playing: false, subtitle: nil)
|
||||||
|
let before = peer.commands.count
|
||||||
|
for target in 1...64 { require(session.seek(Double(target)), "Seek burst exceeds ordinary queue capacity") }
|
||||||
|
require(session.setPlaying(true), "PLAY separates seek runs")
|
||||||
|
for target in 65...96 { require(session.seek(Double(target)), "Second seek run accepted") }
|
||||||
|
require(session.setPlaying(false), "PAUSE separates seek runs")
|
||||||
|
for target in (48...63).reversed() { require(session.seek(Double(target)), "Reverse seek run accepted") }
|
||||||
|
require(status?.seekPending == true, "Latest queued seek remains unacknowledged")
|
||||||
|
try await until("Final seek and playback acknowledged") {
|
||||||
|
status?.position == 48 && status?.seekPending == false &&
|
||||||
|
status?.playing == false && status?.playbackPending == false
|
||||||
|
}
|
||||||
|
let controls = peer.commands.dropFirst(before).filter { ["SEEK", "PLAY", "PAUSE"].contains($0.0) }
|
||||||
|
let targets = controls.filter { $0.0 == "SEEK" }.compactMap { $0.1["currentTime"] as? Double }
|
||||||
|
require(targets == [64, 96, 48] || targets == [1, 64, 96, 48],
|
||||||
|
"Only unsent adjacent seeks are replaced")
|
||||||
|
require(Array(controls.suffix(5)).map(\.0) == ["SEEK", "PLAY", "SEEK", "PAUSE", "SEEK"],
|
||||||
|
"Playback controls remain seek ordering barriers")
|
||||||
|
require(errors.isEmpty && peer.failure == nil, "Superseded requests do not fail or leak queue capacity")
|
||||||
|
session.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
static func behavior(identity: SecIdentity) async throws {
|
static func behavior(identity: SecIdentity) async throws {
|
||||||
let peer = try Receiver(identity: identity)
|
let peer = try Receiver(identity: identity)
|
||||||
peer.muted = true
|
peer.muted = true
|
||||||
@@ -756,10 +789,6 @@ struct CastSessionCheck {
|
|||||||
let load = peer.commands.first { $0.0 == "LOAD" }!.1
|
let load = peer.commands.first { $0.0 == "LOAD" }!.1
|
||||||
require(load["currentTime"] as? Double == 9.25 && load["autoplay"] as? Bool == false, "Acknowledged paused handoff")
|
require(load["currentTime"] as? Double == 9.25 && load["autoplay"] as? Bool == false, "Acknowledged paused handoff")
|
||||||
require(peer.relayVerified, "Receiver fetches real relay HLS and media bytes")
|
require(peer.relayVerified, "Receiver fetches real relay HLS and media bytes")
|
||||||
let controls = peer.commands.filter { ["PLAY", "SEEK", "PAUSE", "STOP"].contains($0.0) }
|
|
||||||
require(controls.map(\.0) == ["PLAY", "SEEK", "SEEK", "PAUSE", "STOP"], "Integrated controls ordered")
|
|
||||||
require(controls[1].1["currentTime"] as? Double == 14.25 &&
|
|
||||||
controls[2].1["currentTime"] as? Double == 24.25, "Rapid skips accumulate from the pending target")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func interruptedPlayback(identity: SecIdentity, executable: String, file: String) async throws {
|
static func interruptedPlayback(identity: SecIdentity, executable: String, file: String) async throws {
|
||||||
@@ -886,6 +915,7 @@ struct CastSessionCheck {
|
|||||||
print("PASS: interrupted Cast connect/LOAD, rejection, replacement/close, latest intent and acknowledged-position recovery")
|
print("PASS: interrupted Cast connect/LOAD, rejection, replacement/close, latest intent and acknowledged-position recovery")
|
||||||
} else {
|
} else {
|
||||||
try await behavior(identity: identity)
|
try await behavior(identity: identity)
|
||||||
|
try await seekCoalescing(identity: identity)
|
||||||
try await launchRegressions(identity: identity)
|
try await launchRegressions(identity: identity)
|
||||||
try await volumeRegressions(identity: identity)
|
try await volumeRegressions(identity: identity)
|
||||||
try await failures(identity: identity)
|
try await failures(identity: identity)
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// Include the implementation to inspect pending commands without adding a public test API.
|
||||||
|
#include "../Sources/Core/player.cpp"
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
static void require(bool condition, const char *message) {
|
||||||
|
if (!condition) { std::fprintf(stderr, "FAIL: %s\n", message); std::exit(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
Player player;
|
||||||
|
// Join before submitting anything: the queue exercise has no executing worker,
|
||||||
|
// media I/O, device clock or scheduling-dependent queue depth.
|
||||||
|
player.shutdown();
|
||||||
|
player.stopping.store(false);
|
||||||
|
auto reset = [&] {
|
||||||
|
player.commands.clear();
|
||||||
|
player.state = {};
|
||||||
|
player.state.status = PLAYER_PAUSED;
|
||||||
|
player.state.can_seek = true;
|
||||||
|
player.state.has_video = true;
|
||||||
|
player.publishedEpoch = player.requestedEpoch.load();
|
||||||
|
};
|
||||||
|
reset();
|
||||||
|
player.state.position = 7;
|
||||||
|
player.state.buffered_start = 6;
|
||||||
|
player.state.buffered_end = 9;
|
||||||
|
constexpr unsigned requests = 100000;
|
||||||
|
unsigned accepted = 0;
|
||||||
|
for (unsigned i = 0; i < requests; ++i)
|
||||||
|
accepted += player_seek(&player, double(i));
|
||||||
|
std::printf("Native pending seek burst: requested=%u accepted=%u queued=%zu latest=%.0f\n",
|
||||||
|
requests, accepted, player.commands.size(), player.commands.back().seconds);
|
||||||
|
if (argc == 2 && std::strcmp(argv[1], "--measure") == 0) return 0;
|
||||||
|
require(argc == 1, "only --measure is supported");
|
||||||
|
require(accepted == requests && player.commands.size() == 1 &&
|
||||||
|
player.commands.back().seconds == requests - 1,
|
||||||
|
"every superseding seek is accepted and only the latest target remains");
|
||||||
|
require(player.state.seek_pending && player.state.revision == requests &&
|
||||||
|
player.state.buffered_start == 0 && player.state.buffered_end == 0 &&
|
||||||
|
player.state.position == 7 && player.state.status == PLAYER_PAUSED,
|
||||||
|
"each accepted seek publishes pending and invalidates buffering, not acknowledged position");
|
||||||
|
|
||||||
|
reset();
|
||||||
|
for (std::size_t i = 0; i < commandLimit - 1; ++i)
|
||||||
|
require(player_pause(&player), "fill queue with command barriers");
|
||||||
|
require(player_seek(&player, 1) && player_seek(&player, 2),
|
||||||
|
"latest queued seek can be replaced even at capacity");
|
||||||
|
require(player.commands.size() == commandLimit && player.commands.back().seconds == 2 &&
|
||||||
|
!player_play(&player), "coalescing does not relax capacity for other commands");
|
||||||
|
|
||||||
|
for (Action barrier : {Action::Play, Action::Pause, Action::Step,
|
||||||
|
Action::Open, Action::Close, Action::Remote}) {
|
||||||
|
reset();
|
||||||
|
require(player_seek(&player, 1), "seek before barrier");
|
||||||
|
Command command{barrier, {}};
|
||||||
|
command.direction = 1;
|
||||||
|
require(player.enqueue(std::move(command)) && player_seek(&player, 2), "seek after barrier");
|
||||||
|
require(player.commands.size() == 3 && player.commands[0].action == Action::Seek &&
|
||||||
|
player.commands[0].seconds == 1 && player.commands[1].action == barrier &&
|
||||||
|
player.commands[2].action == Action::Seek && player.commands[2].seconds == 2,
|
||||||
|
"no seek replacement crosses another action");
|
||||||
|
if (barrier == Action::Open || barrier == Action::Close)
|
||||||
|
require(player.commands[0].epoch != player.commands[2].epoch,
|
||||||
|
"timeline replacement separates seek epochs");
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
require(player_seek(&player, 1), "seek before API error boundary");
|
||||||
|
player.apiError("queue check boundary");
|
||||||
|
require(player_seek(&player, 2) && player.commands.size() == 2 &&
|
||||||
|
player.commands[0].epoch != player.commands[1].epoch,
|
||||||
|
"adjacent seeks from different epochs remain distinct");
|
||||||
|
|
||||||
|
reset();
|
||||||
|
require(player_seek(&player, 1), "first seek queued");
|
||||||
|
auto executing = std::move(player.commands.front());
|
||||||
|
player.commands.pop_front(); // The worker removes a seek before executing it.
|
||||||
|
require(player_seek(&player, 2) && player_seek(&player, 3) &&
|
||||||
|
executing.seconds == 1 && player.commands.size() == 1 && player.commands.back().seconds == 3,
|
||||||
|
"replacement never changes a seek already removed for execution");
|
||||||
|
std::puts("PASS native seek burst/capacity/publication/action barriers/epochs/executing seek");
|
||||||
|
}
|
||||||
@@ -170,7 +170,7 @@ struct SystemPlaybackChecks {
|
|||||||
require(commands.nextTrackCommand.isEnabled,
|
require(commands.nextTrackCommand.isEnabled,
|
||||||
"seekability changes re-enable commands even when playback metadata is unchanged")
|
"seekability changes re-enable commands even when playback metadata is unchanged")
|
||||||
|
|
||||||
require(model.seekRelative(1), "relative seek accepted")
|
for _ in 0..<64 { require(model.seekRelative(1.0 / 64), "rapid native skips retain accumulated target") }
|
||||||
until("paused seek acknowledged") {
|
until("paused seek acknowledged") {
|
||||||
model.state.status == PLAYER_PAUSED && !model.state.seek_pending &&
|
model.state.status == PLAYER_PAUSED && !model.state.seek_pending &&
|
||||||
abs(model.state.position - settledPosition - 1) < 0.05
|
abs(model.state.position - settledPosition - 1) < 0.05
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ CLI := $(APP)/Contents/MacOS/terebi-cli
|
|||||||
BUNDLE_STAMP := $(BUILD_DIR)/bundle.stamp
|
BUNDLE_STAMP := $(BUILD_DIR)/bundle.stamp
|
||||||
SIGNING_IDENTITY ?= -
|
SIGNING_IDENTITY ?= -
|
||||||
|
|
||||||
.PHONY: all release install check-dependencies run check check-config check-fixtures check-audio check-window check-youtube check-quality check-adaptive check-network check-progress check-reporting check-cli check-runtime check-media check-startup check-bundle check-subtitles check-resolved check-cast
|
.PHONY: all release install check-dependencies run check check-seek-queue check-config check-fixtures check-audio check-window check-youtube check-quality check-adaptive check-network check-progress check-reporting check-cli check-runtime check-media check-startup check-bundle check-subtitles check-resolved check-cast
|
||||||
all: $(BUILD_DIR)/terebi
|
all: $(BUILD_DIR)/terebi
|
||||||
|
|
||||||
$(NATIVE_STAMP): Scripts/dependencies.py Dependencies.lock.json
|
$(NATIVE_STAMP): Scripts/dependencies.py Dependencies.lock.json
|
||||||
@@ -105,7 +105,13 @@ run: all
|
|||||||
$(BUILD_DIR)/playback-check: Checks/playback.mm $(OBJECTS) Makefile | check-dependencies
|
$(BUILD_DIR)/playback-check: Checks/playback.mm $(OBJECTS) Makefile | check-dependencies
|
||||||
$(CXX) $(CXXFLAGS) -fobjc-arc $< $(OBJECTS) $(FF_LIBS) $(FRAMEWORKS) -o $@
|
$(CXX) $(CXXFLAGS) -fobjc-arc $< $(OBJECTS) $(FF_LIBS) $(FRAMEWORKS) -o $@
|
||||||
|
|
||||||
check: check-dependencies $(BUILD_DIR)/playback-check check-fixtures check-audio check-window check-config check-youtube check-quality check-adaptive check-network check-progress check-reporting check-cli check-runtime check-media check-startup check-bundle check-subtitles check-resolved check-cast
|
$(BUILD_DIR)/seek-queue-check: Checks/seek-queue.cpp Sources/Core/player.cpp Sources/Core/player.h Sources/Core/player_macos.h Sources/Core/output.hpp Sources/Core/network_input.hpp Sources/Core/network_policy.hpp Sources/Core/progress_reporter.hpp $(filter-out $(BUILD_DIR)/player.o,$(OBJECTS)) Makefile | check-dependencies
|
||||||
|
$(CXX) $(CXXFLAGS) $< $(filter-out $(BUILD_DIR)/player.o,$(OBJECTS)) $(FF_LIBS) $(FRAMEWORKS) -o $@
|
||||||
|
|
||||||
|
check-seek-queue: $(BUILD_DIR)/seek-queue-check
|
||||||
|
$(BUILD_DIR)/seek-queue-check
|
||||||
|
|
||||||
|
check: check-dependencies $(BUILD_DIR)/playback-check check-seek-queue check-fixtures check-audio check-window check-config check-youtube check-quality check-adaptive check-network check-progress check-reporting check-cli check-runtime check-media check-startup check-bundle check-subtitles check-resolved check-cast
|
||||||
$(BUILD_DIR)/playback-check $(BUILD_DIR)/fixtures
|
$(BUILD_DIR)/playback-check $(BUILD_DIR)/fixtures
|
||||||
|
|
||||||
check-fixtures: | check-dependencies
|
check-fixtures: | check-dependencies
|
||||||
|
|||||||
@@ -285,6 +285,17 @@ In normal mode, the right-hand playback control group is captions, then compact
|
|||||||
|
|
||||||
Accepted relative seeks, including keyboard shortcuts, preserve control visibility instead of revealing hidden chrome. Seek failures still reveal the error; dragging the timeline continues to show the controls.
|
Accepted relative seeks, including keyboard shortcuts, preserve control visibility instead of revealing hidden chrome. Seek failures still reveal the error; dragging the timeline continues to show the controls.
|
||||||
|
|
||||||
|
Adjacent, not-yet-started seeks keep only the latest absolute target in the native worker queue and Cast socket-write queue. Relative skips still accumulate before submission. Executing/sent seeks are untouched; play/pause, frame steps, media changes and other queued commands remain ordering barriers. Superseded Cast requests release their deadlines and pending-request slots without reporting an error.
|
||||||
|
|
||||||
|
Measured with local, deterministic bursts (not provider/CDN latency):
|
||||||
|
|
||||||
|
| Scenario | Before | After |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 24 accepted Cast seeks in one actor turn, real TLS loopback receiver | 24 receiver SEEK commands | 2 receiver SEEK commands |
|
||||||
|
| 100,000 native seek submissions with the worker held stopped | 32 accepted; 32 queued; last accepted target 31 | 100,000 accepted; 1 queued; final target 99,999 |
|
||||||
|
|
||||||
|
The Cast burst acknowledged the same final position, 24 seconds, without errors in both runs. `make check-seek-queue` covers native queue capacity, command/timeline barriers and acknowledged-state preservation; Cast protocol and app-model checks cover burst coalescing, play/pause barriers and accumulated relative skips. These measurements establish reduced queued work, not cold-start, cached/uncached seek latency or sustained-playback gains; those benchmarks remain pending.
|
||||||
|
|
||||||
Normal and compact titlebars have no full-width glass strip or separator: content extends beneath AppKit’s transparent titlebar using `fullSizeContentView`, `titlebarAppearsTransparent`, and `titlebarSeparatorStyle = .none`. Titles retain their rounded HUD-glass backdrop. In normal mode, pin, Cast, and settings share a separate container matching the title's 24-point height, horizontal padding, vertical inset, tint, outline and corner radius. Cast sits between pin and settings; all three retain 24-point click targets. That button container is not shown in compact mode; only the compact title and adjacent progress occupy the top row with 8-point outer insets. Native traffic lights and window dragging are preserved.
|
Normal and compact titlebars have no full-width glass strip or separator: content extends beneath AppKit’s transparent titlebar using `fullSizeContentView`, `titlebarAppearsTransparent`, and `titlebarSeparatorStyle = .none`. Titles retain their rounded HUD-glass backdrop. In normal mode, pin, Cast, and settings share a separate container matching the title's 24-point height, horizontal padding, vertical inset, tint, outline and corner radius. Cast sits between pin and settings; all three retain 24-point click targets. That button container is not shown in compact mode; only the compact title and adjacent progress occupy the top row with 8-point outer insets. Native traffic lights and window dragging are preserved.
|
||||||
|
|
||||||
Compact titles default to `<title> · <position> / <duration> <unit>`, using acknowledged playback position rather than slider previews. Total duration selects the format: below one minute, `12 / 45 s`; from one minute to below one hour, `05:12 / 42:08 m`; from one hour onward, `01:23 / 02:10 h`. Values truncate to the displayed precision. Set `[ui].compact_title_progress = "percentage"` to append only a whole percentage number (`<title> · 59`, without a percent sign), or `"none"` for title-only display. Percentages truncate and stay within 0–100. Loading, failed, live, unknown-duration and closed media have no progress suffix. Normal-mode titles remain unchanged.
|
Compact titles default to `<title> · <position> / <duration> <unit>`, using acknowledged playback position rather than slider previews. Total duration selects the format: below one minute, `12 / 45 s`; from one minute to below one hour, `05:12 / 42:08 m`; from one hour onward, `01:23 / 02:10 h`. Values truncate to the displayed precision. Set `[ui].compact_title_progress = "percentage"` to append only a whole percentage number (`<title> · 59`, without a percent sign), or `"none"` for title-only display. Percentages truncate and stay within 0–100. Loading, failed, live, unknown-duration and closed media have no progress suffix. Normal-mode titles remain unchanged.
|
||||||
|
|||||||
@@ -255,6 +255,11 @@ final class CastSession {
|
|||||||
let deadline: Task<Void, Never>
|
let deadline: Task<Void, Never>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct Write {
|
||||||
|
let data: Data
|
||||||
|
let seek: Int?
|
||||||
|
}
|
||||||
|
|
||||||
private let makeConnection: (CastDevice) -> NWConnection
|
private let makeConnection: (CastDevice) -> NWConnection
|
||||||
private let requestTimeout: Duration
|
private let requestTimeout: Duration
|
||||||
private let loadTimeout: Duration
|
private let loadTimeout: Duration
|
||||||
@@ -270,7 +275,7 @@ final class CastSession {
|
|||||||
private var awaitingPong = false
|
private var awaitingPong = false
|
||||||
private var nextRequest = 0
|
private var nextRequest = 0
|
||||||
private var pending: [Int: Pending] = [:]
|
private var pending: [Int: Pending] = [:]
|
||||||
private var writes: [Data] = []
|
private var writes: [Write] = []
|
||||||
private var writing = false
|
private var writing = false
|
||||||
private var launchRequest: Int?
|
private var launchRequest: Int?
|
||||||
private var appTransport: String?
|
private var appTransport: String?
|
||||||
@@ -525,7 +530,17 @@ final class CastSession {
|
|||||||
operation: String, generation: UUID? = nil, timeout: Duration? = nil,
|
operation: String, generation: UUID? = nil, timeout: Duration? = nil,
|
||||||
id suppliedID: Int? = nil,
|
id suppliedID: Int? = nil,
|
||||||
completion: @escaping ([String: Any]?, CastError?) -> Void) throws -> Int {
|
completion: @escaping ([String: Any]?, CastError?) -> Void) throws -> Int {
|
||||||
guard pending.count < 32 else { throw CastError.queueFull }
|
// Only the tail is replaceable: it has not reached the socket, and every
|
||||||
|
// intervening control or protocol frame remains an ordering barrier.
|
||||||
|
let superseded: Int?
|
||||||
|
if command["type"] as? String == "SEEK", namespace == CastWire.media,
|
||||||
|
let id = writes.last?.seek, let previous = pending[id],
|
||||||
|
previous.generation == generation, previous.destination == destination {
|
||||||
|
superseded = id
|
||||||
|
} else {
|
||||||
|
superseded = nil
|
||||||
|
}
|
||||||
|
guard pending.count < 32 || superseded != nil else { throw CastError.queueFull }
|
||||||
let id = suppliedID ?? allocateRequest()
|
let id = suppliedID ?? allocateRequest()
|
||||||
var command = command
|
var command = command
|
||||||
command["requestId"] = id
|
command["requestId"] = id
|
||||||
@@ -537,8 +552,11 @@ final class CastSession {
|
|||||||
}
|
}
|
||||||
pending[id] = Pending(namespace: namespace, destination: destination, operation: operation,
|
pending[id] = Pending(namespace: namespace, destination: destination, operation: operation,
|
||||||
generation: generation, completion: completion, deadline: deadline)
|
generation: generation, completion: completion, deadline: deadline)
|
||||||
do { try send(command, namespace: namespace, destination: destination) }
|
do {
|
||||||
|
try send(command, namespace: namespace, destination: destination, replacing: superseded)
|
||||||
|
}
|
||||||
catch { pending.removeValue(forKey: id)?.deadline.cancel(); throw error }
|
catch { pending.removeValue(forKey: id)?.deadline.cancel(); throw error }
|
||||||
|
if let superseded { complete(superseded, json: nil, error: .cancelled) }
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,12 +571,17 @@ final class CastSession {
|
|||||||
for id in ids { complete(id, json: nil, error: .cancelled) }
|
for id in ids { complete(id, json: nil, error: .cancelled) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func send(_ json: [String: Any], namespace: String, destination: String) throws {
|
private func send(_ json: [String: Any], namespace: String, destination: String,
|
||||||
|
replacing: Int? = nil) throws {
|
||||||
guard connection != nil else { throw CastError.disconnected }
|
guard connection != nil else { throw CastError.disconnected }
|
||||||
guard writes.count < 32 else { throw CastError.queueFull }
|
guard writes.count < 32 || replacing != nil else { throw CastError.queueFull }
|
||||||
let payload = try JSONSerialization.data(withJSONObject: json)
|
let payload = try JSONSerialization.data(withJSONObject: json)
|
||||||
writes.append(try CastWire.frame(.init(source: source, destination: destination,
|
let write = Write(data: try CastWire.frame(.init(source: source, destination: destination,
|
||||||
namespace: namespace, payload: payload)))
|
namespace: namespace, payload: payload)),
|
||||||
|
seek: namespace == CastWire.media && json["type"] as? String == "SEEK" ?
|
||||||
|
json["requestId"] as? Int : nil)
|
||||||
|
if replacing != nil { writes[writes.count - 1] = write }
|
||||||
|
else { writes.append(write) }
|
||||||
flush()
|
flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,7 +590,7 @@ final class CastSession {
|
|||||||
writing = true
|
writing = true
|
||||||
let data = writes.removeFirst()
|
let data = writes.removeFirst()
|
||||||
let id = connectionID
|
let id = connectionID
|
||||||
socket.send(content: data, completion: .contentProcessed { [weak self] error in
|
socket.send(content: data.data, completion: .contentProcessed { [weak self] error in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
guard let self, self.connectionID == id else { return }
|
guard let self, self.connectionID == id else { return }
|
||||||
self.writing = false
|
self.writing = false
|
||||||
|
|||||||
@@ -234,8 +234,13 @@ struct Player {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (commands.size() >= commandLimit) return false;
|
if (command.action == Action::Seek && !commands.empty() &&
|
||||||
commands.push_back(std::move(command));
|
commands.back().action == Action::Seek && commands.back().epoch == command.epoch) {
|
||||||
|
commands.back().seconds = command.seconds;
|
||||||
|
} else {
|
||||||
|
if (commands.size() >= commandLimit) return false;
|
||||||
|
commands.push_back(std::move(command));
|
||||||
|
}
|
||||||
if (newTimeline) requestedEpoch.store(commands.back().epoch, std::memory_order_relaxed);
|
if (newTimeline) requestedEpoch.store(commands.back().epoch, std::memory_order_relaxed);
|
||||||
if (commands.back().action == Action::Open) {
|
if (commands.back().action == Action::Open) {
|
||||||
const auto revision = state.revision + 1;
|
const auto revision = state.revision + 1;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
- [ ] evaluate playback optimization opportunities for YouTube and Miruro — measure benefits before changing behavior
|
- [ ] evaluate playback optimization opportunities for YouTube and Miruro — measure benefits before changing behavior
|
||||||
- [ ] coalesce superseded, not-yet-started seeks in the player and Cast controls; preserve accumulated relative skips and play/pause ordering
|
- [x] coalesce superseded, not-yet-started seeks in the player and Cast controls; preserve accumulated relative skips and play/pause ordering
|
||||||
- [ ] expand Cast H.264/AAC remux coverage with keyframe-aligned segments; preserve seek accuracy, timestamps and receiver compatibility
|
- [ ] expand Cast H.264/AAC remux coverage with keyframe-aligned segments; preserve seek accuracy, timestamps and receiver compatibility
|
||||||
- [ ] reuse decoder/encoder state during sequential Cast playback; reset on random seeks and preserve independent keyframes and AAC continuity
|
- [ ] reuse decoder/encoder state during sequential Cast playback; reset on random seeks and preserve independent keyframes and AAC continuity
|
||||||
- [ ] evaluate VideoToolbox hardware video decoding for native and Cast playback; retain software fallback for unsupported formats
|
- [ ] evaluate VideoToolbox hardware video decoding for native and Cast playback; retain software fallback for unsupported formats
|
||||||
|
|||||||
Reference in New Issue
Block a user