Merge p1-w3-minors: streaming minors findings 15-22
This commit is contained in:
@@ -112,8 +112,11 @@ public class WavOffsetService
|
||||
if (chunkSize < 0)
|
||||
return null;
|
||||
|
||||
if (chunkId == "fmt ")
|
||||
if (chunkId == "fmt " && !foundFmt)
|
||||
{
|
||||
// Use the first fmt chunk encountered — that is the WAV-spec-authoritative
|
||||
// chunk. Subsequent fmt chunks in a malformed file are ignored, matching
|
||||
// AudioProcessor.FindChunk which also returns the first match.
|
||||
if (chunkSize < 16)
|
||||
return null;
|
||||
|
||||
|
||||
@@ -125,6 +125,16 @@ public class TrackController : ControllerBase
|
||||
public async Task<ActionResult> PutTrack(string trackId, [FromBody] AudioBinaryDto track)
|
||||
{
|
||||
_logger.LogInformation("PutTrack called with trackId: {TrackId}", trackId);
|
||||
|
||||
// Reject unknown MIME types up front rather than silently storing the binary
|
||||
// with a ".bin" extension. GetExtension returns ".bin" for any unrecognised
|
||||
// MIME, so treat that as the sentinel for an unsupported type.
|
||||
if (MimeTypeExtensions.GetExtension(track.Mime) == ".bin")
|
||||
{
|
||||
_logger.LogWarning("PutTrack rejected: unsupported MIME type '{Mime}' for track {TrackId}", track.Mime, trackId);
|
||||
return BadRequest($"Unsupported MIME type: {track.Mime}");
|
||||
}
|
||||
|
||||
var audioBinary = AudioBinary.From(track);
|
||||
// Direct FileDatabase write: this endpoint receives an already-processed AudioBinaryDto,
|
||||
// not a WAV file, so TrackService.AddTrackFromWavAsync does not apply. See constructor comment.
|
||||
|
||||
@@ -10,15 +10,18 @@ public static class StreamingErrorHandler
|
||||
|
||||
return lowerError switch
|
||||
{
|
||||
_ when lowerError.Contains("network") || lowerError.Contains("connection") || lowerError.Contains("timeout") =>
|
||||
_ when lowerError.Contains("network") || lowerError.Contains("connection") || lowerError.Contains("timeout") =>
|
||||
"Unable to load audio. Please check your connection and try again.",
|
||||
|
||||
_ when lowerError.Contains("audio") || lowerError.Contains("decode") || lowerError.Contains("format") =>
|
||||
|
||||
_ when lowerError.Contains("header") || lowerError.Contains("wav") || lowerError.Contains("invalid wav") =>
|
||||
"This file format is not supported. Only WAV files can be played.",
|
||||
|
||||
_ when lowerError.Contains("audio") || lowerError.Contains("decode") || lowerError.Contains("format") =>
|
||||
"This audio file may be corrupted or in an unsupported format.",
|
||||
|
||||
_ when lowerError.Contains("cancel") || lowerError.Contains("abort") =>
|
||||
|
||||
_ when lowerError.Contains("cancel") || lowerError.Contains("abort") =>
|
||||
"Audio loading was cancelled.",
|
||||
|
||||
|
||||
_ => "Unable to play audio. Please try again."
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,7 +64,10 @@ export class PlaybackScheduler {
|
||||
* Get current playback position in seconds (includes playbackOffset for seek-beyond-buffer)
|
||||
*/
|
||||
getCurrentPosition(): number {
|
||||
if (this.playbackAnchorTime === 0) {
|
||||
// Use isActive_ as the sentinel for "playback is running", not playbackAnchorTime == 0.
|
||||
// AudioContext.currentTime can legitimately be 0 at context creation, so comparing
|
||||
// against 0 would incorrectly treat an active stream started at t=0 as paused.
|
||||
if (!this.isActive_) {
|
||||
return this.playbackAnchorPosition + this.playbackOffset;
|
||||
}
|
||||
const elapsed = this.contextManager.currentTime - this.playbackAnchorTime;
|
||||
@@ -143,8 +146,11 @@ export class PlaybackScheduler {
|
||||
return; // No new buffers
|
||||
}
|
||||
|
||||
if (this.nextScheduleTime === 0) {
|
||||
this.nextScheduleTime = this.contextManager.currentTime + 0.01;
|
||||
// Use isActive_ as the sentinel for "playback is running", not nextScheduleTime === 0.
|
||||
// AudioContext.currentTime can legitimately be 0 at context creation, which would cause
|
||||
// nextScheduleTime === 0 to incorrectly reset a value already set by playFromPosition.
|
||||
if (!this.isActive_) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scheduleBuffersFrom(this.nextBufferIndex, 0);
|
||||
|
||||
@@ -51,6 +51,9 @@ export class StreamDecoder {
|
||||
private contextManager: AudioContextManager;
|
||||
private wavHeader: WavHeader | null = null;
|
||||
private rawChunks: Uint8Array[] = [];
|
||||
// totalRawBytes and processedBytes are JS number (IEEE 754 double), which can
|
||||
// represent integers exactly up to 2^53 bytes (~8 PB). WAV files are bounded
|
||||
// at 4 GB by the 32-bit RIFF size field, so overflow is not a practical concern.
|
||||
private totalRawBytes: number = 0;
|
||||
private processedBytes: number = 0;
|
||||
private totalStreamLength: number = 0;
|
||||
|
||||
@@ -22,11 +22,16 @@ class WavUtils {
|
||||
// Need a DataView that spans the entire buffer for chunk searching
|
||||
const view = new DataView(concatenated.buffer);
|
||||
|
||||
// Allocate TextDecoder once for the entire parse pass. Constructing it
|
||||
// inside the chunk-walk loop would create a new instance per iteration,
|
||||
// which is non-trivial and unnecessary — a single instance is reusable.
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
// Check RIFF header
|
||||
const riff = new TextDecoder().decode(concatenated.slice(0, 4));
|
||||
const riff = decoder.decode(concatenated.slice(0, 4));
|
||||
if (riff !== 'RIFF') return null;
|
||||
|
||||
const wave = new TextDecoder().decode(concatenated.slice(8, 12));
|
||||
const wave = decoder.decode(concatenated.slice(8, 12));
|
||||
if (wave !== 'WAVE') return null;
|
||||
|
||||
// Variables to store parsed header info
|
||||
@@ -43,7 +48,7 @@ class WavUtils {
|
||||
// Find fmt and data chunks
|
||||
let chunkOffset = 12;
|
||||
while (chunkOffset < totalSize - 8) {
|
||||
const chunkId = new TextDecoder().decode(concatenated.slice(chunkOffset, chunkOffset + 4));
|
||||
const chunkId = decoder.decode(concatenated.slice(chunkOffset, chunkOffset + 4));
|
||||
const chunkSize = view.getUint32(chunkOffset + 4, true);
|
||||
|
||||
if (chunkId === 'fmt ') {
|
||||
|
||||
Reference in New Issue
Block a user