Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SeekTo(...) #15

Merged
merged 8 commits into from
Mar 7, 2019
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
27 changes: 0 additions & 27 deletions Concentus.Oggfile/Concentus.OggFile.nuspec.OLD

This file was deleted.

2 changes: 1 addition & 1 deletion Concentus.Oggfile/Concentus.Oggfile.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard1.0;net4.5</TargetFrameworks>
<Id>Concentus.OggFile</Id>
<Version>1.0.4.0</Version>
<Version>1.0.5.0</Version>
<Title>Concentus.OggFile</Title>
<Authors>Logan Stromberg</Authors>
<Owners>Logan Stromberg</Owners>
Expand Down
211 changes: 154 additions & 57 deletions Concentus.Oggfile/OpusOggReadStream.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
using Concentus.Structs;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Concentus.Oggfile
{
Expand All @@ -14,13 +11,13 @@ namespace Concentus.Oggfile
/// </summary>
public class OpusOggReadStream
{
private Stream _inputStream;
private const double GranuleSampleRate = 48000.0; // Granule position is always expressed in units of 48000hz
private readonly Stream _stream;
private readonly OpusDecoder _decoder;

private byte[] _nextDataPacket;
private OpusDecoder _decoder;
private OpusTags _tags;
private IPacketProvider _packetProvider;
private bool _endOfStream;
private string _lastError;

/// <summary>
/// Builds an Ogg file reader that decodes Opus packets from the given input stream, using a
Expand All @@ -29,57 +26,75 @@ public class OpusOggReadStream
/// </summary>
/// <param name="decoder">An Opus decoder. If you are reusing an existing decoder, remember to call Reset() on it before
/// processing a new stream. The decoder is optional for cases where you may only be interested in the file tags</param>
/// <param name="oggFileInput">The input stream for an Ogg formatted .opus file. The stream will be read from immediately</param>
public OpusOggReadStream(OpusDecoder decoder, Stream oggFileInput)
/// <param name="stream">The input stream for an Ogg formatted .opus file. The stream will be read from immediately</param>
public OpusOggReadStream(OpusDecoder decoder, Stream stream)
{
if (oggFileInput == null)
if (decoder == null)
{
throw new ArgumentNullException("oggFileInput");
}
throw new ArgumentNullException(nameof(decoder));

_inputStream = oggFileInput;
_decoder = decoder;
_endOfStream = false;
if (!Initialize())
}
if (stream == null)
{
_endOfStream = true;
throw new ArgumentNullException(nameof(stream));
}

_stream = stream;
_decoder = decoder;
_endOfStream = !Initialize();
}

/// <summary>
/// Gets a value indicating whether the current stream supports seeking.
/// </summary>
public bool CanSeek => _stream.CanSeek;

/// <summary>
/// Gets the tags that were parsed from the OpusTags Ogg packet, or NULL if no such packet was found.
/// </summary>
public OpusTags Tags
{
get
{
return _tags;
}
}
public OpusTags Tags { get; private set; }

/// <summary>
/// Returns true if there is still another data packet to be decoded from the current Ogg stream.
/// Note that this decoder currently only assumes that the input has 1 elementary stream with no splices
/// or other fancy things.
/// </summary>
public bool HasNextPacket
{
get
{
return !_endOfStream;
}
}
public bool HasNextPacket => !_endOfStream;

/// <summary>
/// If an error happened either in stream initialization, reading, or decoding, the message will appear here.
/// </summary>
public string LastError
{
get
{
return _lastError;
}
}
public string LastError { get; private set; }

/// <summary>
/// Gets the position of the last granule in the page the packet is in.
/// </summary>
public long PageGranulePosition { get; private set; }

/// <summary>
/// Gets the current time in the stream.
/// </summary>
public TimeSpan CurrentTime => TimeSpan.FromSeconds(PageGranulePosition / GranuleSampleRate);

/// <summary>
/// Gets the total number of granules in this stream.
/// </summary>
public long GranuleCount { get; private set; }

/// <summary>
/// Gets the total time from the stream. Only available if the stream is seekable.
/// </summary>
public TimeSpan TotalTime => TimeSpan.FromSeconds(GranuleCount / GranuleSampleRate);

/// <summary>
/// Gets the current pages (or frame) position in this stream.
/// </summary>
public long PagePosition { get; private set; }

/// <summary>
/// Gets the total number of pages (or frames) this stream uses. Only available if the stream is seekable.
/// </summary>
public long PageCount { get; private set; }

/// <summary>
/// Reads the next packet from the Ogg stream and decodes it, returning the decoded PCM buffer.
Expand All @@ -97,10 +112,6 @@ public short[] DecodeNextPacket()
if (_nextDataPacket == null || _nextDataPacket.Length == 0)
{
_endOfStream = true;
}

if (_endOfStream)
{
return null;
}

Expand All @@ -109,12 +120,14 @@ public short[] DecodeNextPacket()
int numSamples = OpusPacketInfo.GetNumSamples(_nextDataPacket, 0, _nextDataPacket.Length, _decoder.SampleRate);
short[] output = new short[numSamples * _decoder.NumChannels];
_decoder.Decode(_nextDataPacket, 0, _nextDataPacket.Length, output, 0, numSamples, false);

QueueNextPacket();

return output;
}
catch (OpusException e)
{
_lastError = "Opus decoder threw exception: " + e.Message;
LastError = "Opus decoder threw exception: " + e.Message;
return null;
}
}
Expand All @@ -128,37 +141,118 @@ private bool Initialize()
{
try
{
OggContainerReader reader = new OggContainerReader(_inputStream, true);
if (!reader.Init())
var oggContainerReader = new OggContainerReader(_stream, true);
if (!oggContainerReader.Init())
{
_lastError = "Could not initialize stream";
LastError = "Could not initialize stream";
return false;
}

//if (!reader.FindNextStream())
//{
// _lastError = "Could not find elementary stream";
// return false;
//}
if (reader.StreamSerials.Length == 0)
if (oggContainerReader.StreamSerials.Length == 0)
{
_lastError = "Initialization failed: No elementary streams found in input file";
LastError = "Initialization failed: No elementary streams found in input file";
return false;
}

int streamSerial = reader.StreamSerials[0];
_packetProvider = reader.GetStream(streamSerial);
int firstStreamSerial = oggContainerReader.StreamSerials[0];
_packetProvider = oggContainerReader.GetStream(firstStreamSerial);

if (CanSeek)
{
GranuleCount = _packetProvider.GetGranuleCount();
PageCount = _packetProvider.GetTotalPageCount();
}

QueueNextPacket();

return true;
}
catch (Exception e)
{
_lastError = "Unknown initialization error: " + e.Message;
LastError = "Unknown initialization error: " + e.Message;
return false;
}
}

/// <summary>
/// Seeks the stream for a valid packet at the specified playbackTime. Note that this is the best approximated position.
/// </summary>
/// <param name="playbackTime">The playback time.</param>
public void SeekTo(TimeSpan playbackTime)
{
if (!CanSeek)
{
throw new InvalidOperationException("Stream is not seekable.");
}

if (playbackTime < TimeSpan.Zero || playbackTime > TotalTime)
{
throw new ArgumentOutOfRangeException(nameof(playbackTime));
}

long granulePosition = Convert.ToInt64(playbackTime.TotalSeconds * GranuleSampleRate);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why Convert.ToInt64 instead of just casting to long?

SeekToGranulePosition(granulePosition);
}

/// <summary>
/// Seeks the stream for a valid packet at the specified granule position.
/// </summary>
/// <param name="granulePosition">The granule position.</param>
private void SeekToGranulePosition(long granulePosition)
{
if (!CanSeek)
{
throw new InvalidOperationException("Stream is not seekable.");
}

if (granulePosition < 0 || granulePosition > GranuleCount)
{
throw new ArgumentOutOfRangeException(nameof(granulePosition));
}

// Find a packet based on offset and return 1 in the callback if the packet is valid
var foundPacket = _packetProvider.FindPacket(granulePosition, GetPacketLength);

// Check of the found packet is valid
if (foundPacket == null || foundPacket.IsEndOfStream)
{
_endOfStream = true;
_nextDataPacket = null;
return;
}

// Just seek to this found packet and get the previous packet (preRoll = 1)
_packetProvider.SeekToPacket(foundPacket, 1);
StefH marked this conversation as resolved.
Show resolved Hide resolved

// Update the PageGranulePosition to the position from this next packet which will be retrieved by the next QueueNextPacket call
PageGranulePosition = _packetProvider.PeekNextPacket().PageGranulePosition;
StefH marked this conversation as resolved.
Show resolved Hide resolved

// Reset the state from the decoder to start processing a fresh stream
_decoder.ResetState();
}

private int GetPacketLength(DataPacket curPacket, DataPacket lastPacket)
{
// if we don't have a previous packet, or we're re-syncing, this packet has no audio data to return
if (lastPacket == null || curPacket.IsResync)
{
return 0;
}

// make sure they are audio packets
if (curPacket.ReadBit())
{
return 0;
}
if (lastPacket.ReadBit())
{
return 0;
}

// Just return a value > 0
return 1;
}

/// <summary>
/// Looks for the next opus data packet in the Ogg stream and queues it up.
/// If the end of stream has been reached, this does nothing.
Expand All @@ -178,6 +272,9 @@ private void QueueNextPacket()
return;
}

PageGranulePosition = packet.PageGranulePosition;
PagePosition = packet.PageSequenceNumber;

byte[] buf = new byte[packet.Length];
packet.Read(buf, 0, packet.Length);
packet.Done();
Expand All @@ -188,7 +285,7 @@ private void QueueNextPacket()
}
else if (buf.Length > 8 && "OpusTags".Equals(Encoding.UTF8.GetString(buf, 0, 8)))
{
_tags = OpusTags.ParsePacket(buf, buf.Length);
Tags = OpusTags.ParsePacket(buf, buf.Length);
QueueNextPacket();
}
else
Expand Down