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

Low latency dash support #3

Merged
Merged
Show file tree
Hide file tree
Changes from 13 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
14 changes: 11 additions & 3 deletions run_end_to_end_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,17 @@ def dashStreamsReady(manifest_path):
pattern = re.compile(r'<Representation.*?((\n).*?)*?Representation>')
with open(manifest_path) as manifest_file:
for representation in pattern.finditer(manifest_file.read()):
if not re.search(r'<S t', representation.group()):
# This Representation has no segments.
return False
if controller.is_low_latency_dash_mode():
# LL-DASH manifests do not contain the segment reference tag <S>.
# Check for the availabilityTimeOffset attribute instead.
if not re.search(r'availabilityTimeOffset', representation.group()):
# This Representation does not have a availabilityTimeOffset yet,
# meaning the first chunk is not yet ready for playout.
return False
else:
if not re.search(r'<S t', representation.group()):
# This Representation has no segments.
return False

return True

Expand Down
21 changes: 20 additions & 1 deletion streamer/controller_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from streamer.node_base import NodeBase, ProcessStatus
from streamer.output_stream import AudioOutputStream, OutputStream, TextOutputStream, VideoOutputStream
from streamer.packager_node import PackagerNode
from streamer.pipeline_configuration import PipelineConfig, StreamingMode
from streamer.pipeline_configuration import ManifestFormat, PipelineConfig, StreamingMode
from streamer.transcoder_node import TranscoderNode
from streamer.periodconcat_node import PeriodConcatNode
import streamer.subprocessWindowsPatch # side-effects only
Expand Down Expand Up @@ -143,6 +143,17 @@ def start(self, output_location: str,
raise RuntimeError(
'Multiperiod input list support is incompatible with HTTP outputs.')

if self._pipeline_config.low_latency_dash_mode:
# Check some restrictions on LL-DASH packaging.
if ManifestFormat.DASH not in self._pipeline_config.manifest_format:
raise RuntimeError(
'low_latency_dash_mode is only compatible with DASH ouputs. ' +
'manifest_format must include DASH')

if not self._pipeline_config.utc_timings:
raise RuntimeError(
'For low_latency_dash_mode, the utc_timings must be set.')

# Note that we remove the trailing slash from the output location, because
# otherwise GCS would create a subdirectory whose name is "".
output_location = output_location.rstrip('/')
Expand Down Expand Up @@ -289,6 +300,14 @@ def is_vod(self) -> bool:

return self._pipeline_config.streaming_mode == StreamingMode.VOD

def is_low_latency_dash_mode(self) -> bool:
"""Returns True if the pipeline is running in LL-DASH mode.

:rtype: bool
"""

return self._pipeline_config.low_latency_dash_mode

class VersionError(Exception):
"""A version error for one of Shaka Streamer's external dependencies.

Expand Down
10 changes: 10 additions & 0 deletions streamer/packager_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,16 @@ def _setup_stream(self, stream: OutputStream) -> str:
def _setup_manifest_format(self) -> List[str]:
args: List[str] = []
if ManifestFormat.DASH in self._pipeline_config.manifest_format:
if self._pipeline_config.utc_timings:
args += [
'--utc_timings',
','.join(timing.scheme_id_uri + '=' +
timing.value for timing in self._pipeline_config.utc_timings)
]
if self._pipeline_config.low_latency_dash_mode:
args += [
'--is_low_latency_dash=true',
]
if self._pipeline_config.streaming_mode == StreamingMode.VOD:
args += [
'--generate_static_live_mpd',
Expand Down
24 changes: 24 additions & 0 deletions streamer/pipeline_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ class EncryptionMode(enum.Enum):
RAW = 'raw'
"""Raw key mode"""

class UtcTimingPair(configuration.Base):
"""An object containing the attributes for a DASH MPD UTCTiming
element"""

# TODO: Use an enum for scheme_id_uri to simplify the config input
scheme_id_uri = configuration.Field(str).cast()
"""SchemeIdUri attribute to be used for the UTCTiming element"""

value = configuration.Field(str).cast()
"""Value attribute to be used for the UTCTiming element"""

class RawKeyConfig(configuration.Base):
"""An object representing a list of keys for Raw key encryption"""

Expand Down Expand Up @@ -309,6 +320,19 @@ class PipelineConfig(configuration.Base):
default=EncryptionConfig({})).cast()
"""Encryption settings."""

# TODO: Generalize this to low_latency_mode once LL-HLS is supported by Packager
low_latency_dash_mode = configuration.Field(bool, default=False).cast()
"""If true, stream in low latency mode for DASH."""

utc_timings = configuration.Field(List[UtcTimingPair]).cast()
"""UTCTiming schemeIdUri and value pairs for the DASH MPD.

If multiple UTCTiming pairs are provided for redundancy,
list the pairs in the order of preference.

Must be set for LL-DASH streaming.
"""


def __init__(self, *args) -> None:

Expand Down
74 changes: 74 additions & 0 deletions tests/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ describe('Shaka Streamer', () => {
muxedTextTests(dashManifestUrl, '(dash)');

multiPeriodTests(dashManifestUrl, '(dash)');

lowLatencyDashTests(dashManifestUrl, '(dash)');
});

function errorTests() {
Expand Down Expand Up @@ -378,6 +380,39 @@ function errorTests() {
error_type: 'RuntimeError',
}));
});

it('fails when utc_timing is not set for low_latency_dash_mode', async () => {
const inputConfig = getBasicInputConfig();
const pipelineConfig = {
low_latency_dash_mode: true,
streaming_mode: 'live',
};

await expectAsync(startStreamer(inputConfig, pipelineConfig))
.toBeRejectedWith(jasmine.objectContaining({
error_type: 'RuntimeError',
}));
});

it('fails when low_latency_dash_mode is set without a DASH manifest', async () => {
const inputConfig = getBasicInputConfig();
const pipelineConfig = {
low_latency_dash_mode: true,
manifest_format: ['hls'],
streaming_mode: 'live',
utc_timings: [
{
scheme_id_uri:'urn:mpeg:dash:utc:http-xsdate:2014',
value:'https://time.akamai.com/?.iso'
},
],
};

await expectAsync(startStreamer(inputConfig, pipelineConfig))
.toBeRejectedWith(jasmine.objectContaining({
error_type: 'RuntimeError',
}));
});
}

function resolutionTests(manifestUrl, format) {
Expand Down Expand Up @@ -1396,4 +1431,43 @@ function multiPeriodTests(manifestUrl, format) {
// Be more tolerant with float comparison, (D > 1.9 * length) instead of (D == 2 * length).
expect(video.duration).toBeGreaterThan(1.9);
});
}

function lowLatencyDashTests(manifestUrl, format) {
CaitlinOCallaghan marked this conversation as resolved.
Show resolved Hide resolved
it('can process LL-DASH streaming ' + format, async() => {
const inputConfigDict = {
'inputs': [
{
'name': TEST_DIR + 'BigBuckBunny.1080p.mp4',
'media_type': 'video',
// Keep this test short by only encoding 4s of content.
'end_time': '0:04',
}
],
};
const pipelineConfigDict = {
'streaming_mode': 'live',
'resolutions': ['144p'],
'video_codecs': ['h264'],
'manifest_format': ['dash'],
'segment_size': 2,
'low_latency_dash_mode': true,
'utc_timings': [
CaitlinOCallaghan marked this conversation as resolved.
Show resolved Hide resolved
{
'scheme_id_uri':'urn:mpeg:dash:utc:http-xsdate:2014',

Choose a reason for hiding this comment

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

I think I might prefer replacing the scheme ID with a more readable enum, in the name of making the config files easier to write, but I don't think it's worth holding up this PR. Something to consider for later, though, if you have time.

I think we would restrict that enum to values equivalent to the "xsdate" and "head" schemes.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Ohhh, I like the idea of using an enum - I'll make sure to add it later.

The UTCTiming information is funky to format and very easy to mess up. The standard lists the following as the possible options.

https://dashif-documents.azurewebsites.net/DASH-IF-IOP/master/DASH-IF-IOP.html
image

Choose a reason for hiding this comment

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

True, but we wouldn't have to offer all of those. I think xsdate and head are the simplest and easiest to explain in docs, and should cover most people's needs. For Streamer, I would tend to err on the side of "easy" and add power later if people request it.

'value':'https://time.akamai.com/?.iso'
},
],
};
await startStreamer(inputConfigDict, pipelineConfigDict);
// TODO(CaitlinO'Callaghan): fix so player loads and test passes
player.configure({
CaitlinOCallaghan marked this conversation as resolved.
Show resolved Hide resolved
streaming: {
lowLatencyMode: true,
inaccurateManifestTolerance: 0,
rebufferingGoal: 0.01,
}
});
await player.load(manifestUrl);
Copy link
Owner Author

Choose a reason for hiding this comment

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

This await is never resolved causing the test to fail :(

Choose a reason for hiding this comment

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

You can debug in the http server in the python script that runs the tests to see what requests the player is making. That might give us a clue as to what's wrong.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Thank you for this insight! I will check it out.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Ah, I figured out the issue! The run_end_to_end_tests.py process will sleep until the DASH or HLS stream is ready for playout. The readiness for the DASH case is determined by the function dashStreamsReady(). This function uses regex to check the MPD for the presence of a segment tag <S>, which indicates that a segment is available. LL-DASH MPDs do not contain <S>, so the LL-DASH stream was never able to trigger the ready state.

To fix this, I added a check for the availableTimeOffset attribute in the LL-DASH case. This attribute is equal to the segment duration minus the chunk duration. The availableTimeOffset value indicates to the player the earliest time that a segment can be played out, since, in the LL-DASH case, the segment can be downloaded and played before it is fully written.

Choose a reason for hiding this comment

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

Fix looks good. Nice work!

});
}