Пример #1
0
async def test_stream_timeout_after_stop(hass, hass_client,
                                         stream_worker_sync):
    """Test hls stream timeout after the stream has been stopped already."""
    await async_setup_component(hass, "stream", {"stream": {}})

    stream_worker_sync.pause()

    # Setup demo HLS track
    source = generate_h264_video()
    stream = create_stream(hass, source)

    # Request stream
    stream.add_provider("hls")
    stream.start()

    stream_worker_sync.resume()
    stream.stop()

    # Wait 5 minutes and fire callback.  Stream should already have been
    # stopped so this is a no-op.
    future = dt_util.utcnow() + timedelta(minutes=5)
    async_fire_time_changed(hass, future)
    await hass.async_block_till_done()
Пример #2
0
async def test_get_image(hass, record_worker_sync):
    """Test that the has_keyframe metadata matches the media."""
    await async_setup_component(hass, "stream", {"stream": {}})

    source = generate_h264_video()

    # Since libjpeg-turbo is not installed on the CI runner, we use a mock
    with patch("homeassistant.components.camera.img_util.TurboJPEGSingleton"
               ) as mock_turbo_jpeg_singleton:
        mock_turbo_jpeg_singleton.instance.return_value = mock_turbo_jpeg()
        stream = create_stream(hass, source, {})

    # use record_worker_sync to grab output segments
    with patch.object(hass.config, "is_allowed_path", return_value=True):
        await stream.async_record("/example/path")

    assert stream._keyframe_converter._image is None

    await record_worker_sync.join()

    assert await stream.async_get_image() == EMPTY_8_6_JPEG

    stream.stop()
Пример #3
0
async def test_hls_max_segments_discontinuity(hass, hls_stream,
                                              stream_worker_sync):
    """Test a discontinuity with more segments than the segment deque can hold."""
    await async_setup_component(hass, "stream", {"stream": {}})

    stream = create_stream(hass, STREAM_SOURCE)
    stream_worker_sync.pause()
    hls = stream.add_provider("hls")

    hls_client = await hls_stream(stream)

    hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0))

    # Produce enough segments to overfill the output buffer by one
    for sequence in range(1, MAX_SEGMENTS + 2):
        hls.put(
            Segment(sequence, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=1))
    await hass.async_block_till_done()

    resp = await hls_client.get("/playlist.m3u8")
    assert resp.status == 200

    # Only NUM_PLAYLIST_SEGMENTS are returned in the playlist causing the
    # EXT-X-DISCONTINUITY tag to be omitted and EXT-X-DISCONTINUITY-SEQUENCE
    # returned instead.
    start = MAX_SEGMENTS + 2 - NUM_PLAYLIST_SEGMENTS
    segments = []
    for sequence in range(start, MAX_SEGMENTS + 2):
        segments.append(make_segment(sequence))
    assert await resp.text() == make_playlist(
        sequence=start,
        discontinuity_sequence=1,
        segments=segments,
    )

    stream_worker_sync.resume()
    stream.stop()
Пример #4
0
async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync):
    """Test rendering the hls playlist with 1 and 2 output segments."""
    await async_setup_component(hass, "stream", {"stream": {}})

    stream = create_stream(hass, STREAM_SOURCE, {})
    stream_worker_sync.pause()
    hls = stream.add_provider(HLS_PROVIDER)
    for i in range(2):
        segment = Segment(sequence=i,
                          duration=SEGMENT_DURATION,
                          start_time=FAKE_TIME)
        hls.put(segment)
    await hass.async_block_till_done()

    hls_client = await hls_stream(stream)

    resp = await hls_client.get("/playlist.m3u8")
    assert resp.status == 200
    assert await resp.text() == make_playlist(
        sequence=0, segments=[make_segment(0),
                              make_segment(1)])

    segment = Segment(sequence=2,
                      duration=SEGMENT_DURATION,
                      start_time=FAKE_TIME)
    hls.put(segment)
    await hass.async_block_till_done()
    resp = await hls_client.get("/playlist.m3u8")
    assert resp.status == 200
    assert await resp.text() == make_playlist(
        sequence=0,
        segments=[make_segment(0),
                  make_segment(1),
                  make_segment(2)])

    stream_worker_sync.resume()
    stream.stop()
Пример #5
0
async def test_durations(hass, record_worker_sync):
    """Test that the duration metadata matches the media."""
    await async_setup_component(hass, "stream", {"stream": {}})

    source = generate_h264_video()
    stream = create_stream(hass, source, {})

    # use record_worker_sync to grab output segments
    with patch.object(hass.config, "is_allowed_path", return_value=True):
        await stream.async_record("/example/path")

    complete_segments = list(await record_worker_sync.get_segments())[:-1]
    assert len(complete_segments) >= 1

    # check that the Part duration metadata matches the durations in the media
    running_metadata_duration = 0
    for segment in complete_segments:
        for part in segment.parts:
            av_part = av.open(io.BytesIO(segment.init + part.data))
            running_metadata_duration += part.duration
            # av_part.duration will just return the largest dts in av_part.
            # When we normalize by av.time_base this should equal the running duration
            assert math.isclose(
                running_metadata_duration,
                av_part.duration / av.time_base,
                abs_tol=1e-6,
            )
            av_part.close()
    # check that the Part durations are consistent with the Segment durations
    for segment in complete_segments:
        assert math.isclose(sum(part.duration for part in segment.parts),
                            segment.duration,
                            abs_tol=1e-6)

    await record_worker_sync.join()

    stream.stop()
Пример #6
0
async def test_hls_playlist_view_discontinuity(hass, hls_stream,
                                               stream_worker_sync):
    """Test a discontinuity across segments in the stream with 3 segments."""
    await async_setup_component(hass, "stream", {"stream": {}})

    stream = create_stream(hass, STREAM_SOURCE, {})
    stream_worker_sync.pause()
    hls = stream.add_provider(HLS_PROVIDER)

    segment = Segment(sequence=0, stream_id=0, duration=SEGMENT_DURATION)
    hls.put(segment)
    segment = Segment(sequence=1, stream_id=0, duration=SEGMENT_DURATION)
    hls.put(segment)
    segment = Segment(
        sequence=2,
        stream_id=1,
        duration=SEGMENT_DURATION,
    )
    hls.put(segment)
    await hass.async_block_till_done()

    hls_client = await hls_stream(stream)

    resp = await hls_client.get("/playlist.m3u8")
    assert resp.status == HTTPStatus.OK
    assert await resp.text() == make_playlist(
        sequence=0,
        segments=[
            make_segment(0),
            make_segment(1),
            make_segment(2, discontinuity=True),
        ],
    )

    stream_worker_sync.resume()
    stream.stop()
Пример #7
0
async def test_stream_keepalive(hass):
    """Test hls stream retries the stream when keepalive=True."""
    await async_setup_component(hass, "stream", {"stream": {}})

    # Setup demo HLS track
    source = "test_stream_keepalive_source"
    stream = create_stream(hass, source, {})
    assert stream.available
    track = stream.add_provider(HLS_PROVIDER)
    track.num_segments = 2

    cur_time = 0

    def time_side_effect():
        nonlocal cur_time
        if cur_time >= 80:
            stream.keepalive = False  # Thread should exit and be joinable.
        cur_time += 40
        return cur_time

    with patch("av.open") as av_open, patch(
            "homeassistant.components.stream.time") as mock_time, patch(
                "homeassistant.components.stream.STREAM_RESTART_INCREMENT", 0):
        av_open.side_effect = av.error.InvalidDataError(-2, "error")
        mock_time.time.side_effect = time_side_effect
        # Request stream
        stream.keepalive = True
        stream.start()
        stream._thread.join()
        stream._thread = None
        assert av_open.call_count == 2
        assert not stream.available

    # Stop stream, if it hasn't quit already
    stream.stop()
    assert not stream.available
Пример #8
0
async def test_record_stream(hass, hass_client, record_worker_sync, h264_video):
    """
    Test record stream.

    Tests full integration with the stream component, and captures the
    stream worker and save worker to allow for clean shutdown of background
    threads.  The actual save logic is tested in test_recorder_save below.
    """
    await async_setup_component(hass, "stream", {"stream": {}})

    # Setup demo track
    stream = create_stream(hass, h264_video, {})
    with patch.object(hass.config, "is_allowed_path", return_value=True):
        await stream.async_record("/example/path")

    # After stream decoding finishes, the record worker thread starts
    segments = await record_worker_sync.get_segments()
    assert len(segments) >= 1

    # Verify that the save worker was invoked, then block until its
    # thread completes and is shutdown completely to avoid thread leaks.
    await record_worker_sync.join()

    stream.stop()
async def test_get_part_segments(hass, hls_stream, stream_worker_sync,
                                 hls_sync):
    """Test requests for part segments and hinted parts."""
    await async_setup_component(
        hass,
        "stream",
        {
            "stream": {
                CONF_LL_HLS: True,
                CONF_SEGMENT_DURATION: SEGMENT_DURATION,
                CONF_PART_DURATION: TEST_PART_DURATION,
            }
        },
    )

    stream = create_stream(hass, STREAM_SOURCE, {})
    stream_worker_sync.pause()

    hls = stream.add_provider(HLS_PROVIDER)

    hls_client = await hls_stream(stream)

    # Seed hls with 1 complete segment and 1 in process segment
    segment = create_segment(sequence=0)
    hls.put(segment)
    for part in create_parts(SEQUENCE_BYTES):
        segment.async_add_part(part, 0)
        hls.part_put()
    complete_segment(segment)

    segment = create_segment(sequence=1)
    hls.put(segment)
    remaining_parts = create_parts(SEQUENCE_BYTES)
    num_completed_parts = len(remaining_parts) // 2
    for _ in range(num_completed_parts):
        segment.async_add_part(remaining_parts.pop(0), 0)

    # Make requests for all the existing part segments
    # These should succeed
    requests = asyncio.gather(*(hls_client.get(f"/segment/1.{part}.m4s")
                                for part in range(num_completed_parts)))
    responses = await requests
    assert all(response.status == HTTPStatus.OK for response in responses)
    assert all([
        await responses[i].read() == segment.parts[i].data
        for i in range(len(responses))
    ])

    # Request for next segment which has not yet been hinted (we will only hint
    # for this segment after segment 1 is complete).
    # This should fail, but it will hold for one more part_put before failing.
    hls_sync.reset_request_pool(1)
    request = asyncio.create_task(hls_client.get("/segment/2.0.m4s"))
    await hls_sync.wait_for_handler()
    hls.part_put()
    response = await request
    assert response.status == HTTPStatus.NOT_FOUND

    # Put the remaining parts and complete the segment
    while remaining_parts:
        await hls_sync.wait_for_handler()
        # Put one more part segment
        segment.async_add_part(remaining_parts.pop(0), 0)
        hls.part_put()
    complete_segment(segment)

    # Now the hint should have moved to segment 2
    # The request for segment 2 which failed before should work now
    hls_sync.reset_request_pool(1)
    request = asyncio.create_task(hls_client.get("/segment/2.0.m4s"))
    # Put an entire segment and its parts.
    segment = create_segment(sequence=2)
    hls.put(segment)
    remaining_parts = create_parts(ALT_SEQUENCE_BYTES)
    for part in remaining_parts:
        await hls_sync.wait_for_handler()
        segment.async_add_part(part, 0)
        hls.part_put()
    complete_segment(segment)
    # Check the response
    response = await request
    assert response.status == HTTPStatus.OK
    assert (await response.read() ==
            ALT_SEQUENCE_BYTES[:len(hls.get_segment(2).parts[0].data)])

    stream_worker_sync.resume()
Пример #10
0
async def test_ll_hls_playlist_msn_part(hass, hls_stream, stream_worker_sync,
                                        hls_sync):
    """Test that requests using _HLS_msn and _HLS_part get held and returned."""

    await async_setup_component(
        hass,
        "stream",
        {
            "stream": {
                CONF_LL_HLS: True,
                CONF_SEGMENT_DURATION: SEGMENT_DURATION,
                CONF_PART_DURATION: TEST_PART_DURATION,
            }
        },
    )

    stream = create_stream(hass, STREAM_SOURCE, {})
    stream_worker_sync.pause()

    hls = stream.add_provider(HLS_PROVIDER)

    hls_client = await hls_stream(stream)

    # Seed hls with 1 complete segment and 1 in process segment
    segment = create_segment(sequence=0)
    hls.put(segment)
    for part in create_parts(SEQUENCE_BYTES):
        segment.async_add_part(part, 0)
        hls.part_put()
    complete_segment(segment)

    segment = create_segment(sequence=1)
    hls.put(segment)
    remaining_parts = create_parts(SEQUENCE_BYTES)
    num_completed_parts = len(remaining_parts) // 2
    for part in remaining_parts[:num_completed_parts]:
        segment.async_add_part(part, 0)
    del remaining_parts[:num_completed_parts]

    # Make requests for all the part segments up to n+ADVANCE_PART_LIMIT
    hls_sync.reset_request_pool(
        num_completed_parts +
        int(-(-hass.data[DOMAIN][ATTR_SETTINGS].hls_advance_part_limit // 1)))
    msn_requests = asyncio.gather(*(
        hls_client.get(f"/playlist.m3u8?_HLS_msn=1&_HLS_part={i}")
        for i in range(num_completed_parts + int(-(
            -hass.data[DOMAIN][ATTR_SETTINGS].hls_advance_part_limit // 1)))))

    while remaining_parts:
        await hls_sync.wait_for_handler()
        segment.async_add_part(remaining_parts.pop(0), 0)
        hls.part_put()

    msn_responses = await msn_requests

    # All the responses should succeed except the last one which fails
    assert all(response.status == HTTPStatus.OK
               for response in msn_responses[:-1])
    assert msn_responses[-1].status == HTTPStatus.BAD_REQUEST

    stream_worker_sync.resume()
Пример #11
0
async def test_ll_hls_playlist_rollover_part(hass, hls_stream,
                                             stream_worker_sync, hls_sync):
    """Test playlist request rollover."""

    await async_setup_component(
        hass,
        "stream",
        {
            "stream": {
                CONF_LL_HLS: True,
                CONF_SEGMENT_DURATION: SEGMENT_DURATION,
                CONF_PART_DURATION: TEST_PART_DURATION,
            }
        },
    )

    stream = create_stream(hass, STREAM_SOURCE, {})
    stream_worker_sync.pause()

    hls = stream.add_provider(HLS_PROVIDER)

    hls_client = await hls_stream(stream)

    # Seed hls with 1 complete segment and 1 in process segment
    for sequence in range(2):
        segment = create_segment(sequence=sequence)
        hls.put(segment)

        for part in create_parts(SEQUENCE_BYTES):
            segment.async_add_part(part, 0)
            hls.part_put()
        complete_segment(segment)

    await hass.async_block_till_done()

    hls_sync.reset_request_pool(4)
    segment = hls.get_segment(1)
    # the first request corresponds to the last part of segment 1
    # the remaining requests correspond to part 0 of segment 2
    requests = asyncio.gather(*([
        hls_client.get(
            f"/playlist.m3u8?_HLS_msn=1&_HLS_part={len(segment.parts)-1}"),
        hls_client.get(
            f"/playlist.m3u8?_HLS_msn=1&_HLS_part={len(segment.parts)}"),
        hls_client.get(
            f"/playlist.m3u8?_HLS_msn=1&_HLS_part={len(segment.parts)+1}"),
        hls_client.get("/playlist.m3u8?_HLS_msn=2&_HLS_part=0"),
    ]))

    await hls_sync.wait_for_handler()

    segment = create_segment(sequence=2)
    hls.put(segment)
    await hass.async_block_till_done()

    remaining_parts = create_parts(SEQUENCE_BYTES)
    segment.async_add_part(remaining_parts.pop(0), 0)
    hls.part_put()

    await hls_sync.wait_for_handler()

    different_response, *same_responses = await requests

    assert different_response.status == HTTPStatus.OK
    assert all(response.status == HTTPStatus.OK for response in same_responses)
    different_playlist = await different_response.read()
    same_playlists = [await response.read() for response in same_responses]
    assert different_playlist != same_playlists[0]
    assert all(playlist == same_playlists[0]
               for playlist in same_playlists[1:])

    stream_worker_sync.resume()
Пример #12
0
async def test_ll_hls_playlist_view(hass, hls_stream, stream_worker_sync):
    """Test rendering the hls playlist with 1 and 2 output segments."""
    await async_setup_component(
        hass,
        "stream",
        {
            "stream": {
                CONF_LL_HLS: True,
                CONF_SEGMENT_DURATION: SEGMENT_DURATION,
                CONF_PART_DURATION: TEST_PART_DURATION,
            }
        },
    )

    stream = create_stream(hass, STREAM_SOURCE, {})
    stream_worker_sync.pause()
    hls = stream.add_provider(HLS_PROVIDER)

    # Add 2 complete segments to output
    for sequence in range(2):
        segment = create_segment(sequence=sequence)
        hls.put(segment)
        for part in create_parts(SEQUENCE_BYTES):
            segment.async_add_part(part, 0)
            hls.part_put()
        complete_segment(segment)
    await hass.async_block_till_done()

    hls_client = await hls_stream(stream)

    resp = await hls_client.get("/playlist.m3u8")
    assert resp.status == HTTPStatus.OK
    assert await resp.text() == make_playlist(
        sequence=0,
        segments=[
            make_segment_with_parts(i, len(segment.parts),
                                    PART_INDEPENDENT_PERIOD) for i in range(2)
        ],
        hint=make_hint(2, 0),
        segment_duration=SEGMENT_DURATION,
        part_target_duration=hls.stream_settings.part_target_duration,
    )

    # add one more segment
    segment = create_segment(sequence=2)
    hls.put(segment)
    for part in create_parts(SEQUENCE_BYTES):
        segment.async_add_part(part, 0)
        hls.part_put()
    complete_segment(segment)

    await hass.async_block_till_done()
    resp = await hls_client.get("/playlist.m3u8")
    assert resp.status == HTTPStatus.OK
    assert await resp.text() == make_playlist(
        sequence=0,
        segments=[
            make_segment_with_parts(i, len(segment.parts),
                                    PART_INDEPENDENT_PERIOD) for i in range(3)
        ],
        hint=make_hint(3, 0),
        segment_duration=SEGMENT_DURATION,
        part_target_duration=hls.stream_settings.part_target_duration,
    )

    stream_worker_sync.resume()
    stream.stop()
Пример #13
0
async def test_ll_hls_stream(hass, hls_stream, stream_worker_sync):
    """
    Test hls stream.

    Purposefully not mocking anything here to test full
    integration with the stream component.
    """
    await async_setup_component(
        hass,
        "stream",
        {
            "stream": {
                CONF_LL_HLS: True,
                CONF_SEGMENT_DURATION: SEGMENT_DURATION,
                # Use a slight mismatch in PART_DURATION to mimic
                # misalignments with source DTSs
                CONF_PART_DURATION: TEST_PART_DURATION - 0.01,
            }
        },
    )

    stream_worker_sync.pause()

    num_playlist_segments = 3
    # Setup demo HLS track
    source = generate_h264_video(
        duration=num_playlist_segments * SEGMENT_DURATION + 2)
    stream = create_stream(hass, source, {})

    # Request stream
    stream.add_provider(HLS_PROVIDER)
    stream.start()

    hls_client = await hls_stream(stream)

    # Fetch playlist
    master_playlist_response = await hls_client.get()
    assert master_playlist_response.status == HTTPStatus.OK

    # Fetch init
    master_playlist = await master_playlist_response.text()
    init_response = await hls_client.get("/init.mp4")
    assert init_response.status == HTTPStatus.OK

    # Fetch playlist
    playlist_url = "/" + master_playlist.splitlines()[-1]
    playlist_response = await hls_client.get(
        playlist_url + f"?_HLS_msn={num_playlist_segments-1}")
    assert playlist_response.status == HTTPStatus.OK

    # Fetch segments
    playlist = await playlist_response.text()
    segment_re = re.compile(r"^(?P<segment_url>./segment/\d+\.m4s)")
    for line in playlist.splitlines():
        match = segment_re.match(line)
        if match:
            segment_url = "/" + match.group("segment_url")
            segment_response = await hls_client.get(segment_url)
            assert segment_response.status == HTTPStatus.OK

    def check_part_is_moof_mdat(data: bytes):
        if len(data) < 8 or data[4:8] != b"moof":
            return False
        moof_length = int.from_bytes(data[0:4], byteorder="big")
        if (len(data) < moof_length + 8
                or data[moof_length + 4:moof_length + 8] != b"mdat"):
            return False
        mdat_length = int.from_bytes(data[moof_length:moof_length + 4],
                                     byteorder="big")
        if mdat_length + moof_length != len(data):
            return False
        return True

    # Parse playlist
    part_re = re.compile(
        r'#EXT-X-PART:DURATION=(?P<part_duration>[0-9]{1,}.[0-9]{3,}),URI="(?P<part_url>.+?)"(,INDEPENDENT=YES)?'
    )
    datetime_re = re.compile(r"#EXT-X-PROGRAM-DATE-TIME:(?P<datetime>.+)")
    inf_re = re.compile(r"#EXTINF:(?P<segment_duration>[0-9]{1,}.[0-9]{3,}),")
    # keep track of which tests were done (indexed by re)
    tested = {regex: False for regex in (part_re, datetime_re, inf_re)}
    # keep track of times and durations along playlist for checking consistency
    part_durations = []
    segment_duration = 0
    datetimes = deque()
    for line in playlist.splitlines():
        match = part_re.match(line)
        if match:
            # Fetch all completed part segments
            part_durations.append(float(match.group("part_duration")))
            part_segment_url = "/" + match.group("part_url")
            part_segment_response = await hls_client.get(part_segment_url, )
            assert part_segment_response.status == HTTPStatus.OK
            assert check_part_is_moof_mdat(await part_segment_response.read())
            tested[part_re] = True
            continue
        match = datetime_re.match(line)
        if match:
            datetimes.append(parser.parse(match.group("datetime")))
            # Check that segment durations are consistent with PROGRAM-DATE-TIME
            if len(datetimes) > 1:
                datetime_duration = (datetimes[-1] -
                                     datetimes.popleft()).total_seconds()
                if segment_duration:
                    assert math.isclose(datetime_duration,
                                        segment_duration,
                                        rel_tol=1e-3)
                    tested[datetime_re] = True
            continue
        match = inf_re.match(line)
        if match:
            segment_duration = float(match.group("segment_duration"))
            # Check that segment durations are consistent with part durations
            if len(part_durations) > 1:
                assert math.isclose(sum(part_durations),
                                    segment_duration,
                                    rel_tol=1e-3)
                tested[inf_re] = True
                part_durations.clear()
    # make sure all playlist tests were performed
    assert all(tested.values())

    stream_worker_sync.resume()

    # Stop stream, if it hasn't quit already
    stream.stop()

    # Ensure playlist not accessible after stream ends
    fail_response = await hls_client.get()
    assert fail_response.status == HTTPStatus.NOT_FOUND
Пример #14
0
async def test_durations(hass, record_worker_sync):
    """Test that the duration metadata matches the media."""

    # Use a target part duration which has a slight mismatch
    # with the incoming frame rate to better expose problems.
    target_part_duration = TEST_PART_DURATION - 0.01
    await async_setup_component(
        hass,
        "stream",
        {
            "stream": {
                CONF_LL_HLS: True,
                CONF_SEGMENT_DURATION: SEGMENT_DURATION,
                CONF_PART_DURATION: target_part_duration,
            }
        },
    )

    source = generate_h264_video(duration=SEGMENT_DURATION + 1)
    stream = create_stream(hass, source, {})

    # use record_worker_sync to grab output segments
    with patch.object(hass.config, "is_allowed_path", return_value=True):
        await stream.async_record("/example/path")

    complete_segments = list(await record_worker_sync.get_segments())[:-1]
    assert len(complete_segments) >= 1

    # check that the Part duration metadata matches the durations in the media
    running_metadata_duration = 0
    for segment in complete_segments:
        av_segment = av.open(io.BytesIO(segment.init + segment.get_data()))
        av_segment.close()
        for part_num, part in enumerate(segment.parts):
            av_part = av.open(io.BytesIO(segment.init + part.data))
            running_metadata_duration += part.duration
            # av_part.duration actually returns the dts of the first packet of the next
            # av_part. When we normalize this by av.time_base we get the running
            # duration of the media.
            # The metadata duration may differ slightly from the media duration.
            # The worker has some flexibility of where to set each metadata boundary,
            # and when the media's duration is slightly too long or too short, the
            # metadata duration may be adjusted up or down.
            # We check here that the divergence between the metadata duration and the
            # media duration is not too large (2 frames seems reasonable here).
            assert math.isclose(
                (av_part.duration - av_part.start_time) / av.time_base,
                part.duration,
                abs_tol=2 / av_part.streams.video[0].rate + 1e-6,
            )
            # Also check that the sum of the durations so far matches the last dts
            # in the media.
            assert math.isclose(
                running_metadata_duration,
                av_part.duration / av.time_base,
                abs_tol=1e-6,
            )
            # And check that the metadata duration is between 0.85x and 1.0x of
            # the part target duration
            if not (part.has_keyframe or part_num == len(segment.parts) - 1):
                assert part.duration > 0.85 * target_part_duration - 1e-6
            assert part.duration < target_part_duration + 1e-6
            av_part.close()
    # check that the Part durations are consistent with the Segment durations
    for segment in complete_segments:
        assert math.isclose(
            sum(part.duration for part in segment.parts),
            segment.duration,
            abs_tol=1e-6,
        )

    await record_worker_sync.join()

    stream.stop()
Пример #15
0
async def test_ll_hls_playlist_bad_msn_part(hass, hls_stream,
                                            stream_worker_sync):
    """Test some playlist requests with invalid _HLS_msn/_HLS_part."""

    await async_setup_component(
        hass,
        "stream",
        {
            "stream": {
                CONF_LL_HLS: True,
                CONF_SEGMENT_DURATION: SEGMENT_DURATION,
                CONF_PART_DURATION: TEST_PART_DURATION,
            }
        },
    )

    stream = create_stream(hass, STREAM_SOURCE, {})
    stream_worker_sync.pause()

    hls = stream.add_provider(HLS_PROVIDER)

    hls_client = await hls_stream(stream)

    # If the Playlist URI contains an _HLS_part directive but no _HLS_msn
    # directive, the Server MUST return Bad Request, such as HTTP 400.

    assert (await hls_client.get("/playlist.m3u8?_HLS_part=1")
            ).status == HTTPStatus.BAD_REQUEST

    # Seed hls with 1 complete segment and 1 in process segment
    segment = create_segment(sequence=0)
    hls.put(segment)
    for part in create_parts(SEQUENCE_BYTES):
        segment.async_add_part(part, 0)
        hls.part_put()
    complete_segment(segment)

    segment = create_segment(sequence=1)
    hls.put(segment)
    remaining_parts = create_parts(SEQUENCE_BYTES)
    num_completed_parts = len(remaining_parts) // 2
    for part in remaining_parts[:num_completed_parts]:
        segment.async_add_part(part, 0)

    # If the _HLS_msn is greater than the Media Sequence Number of the last
    # Media Segment in the current Playlist plus two, or if the _HLS_part
    # exceeds the last Partial Segment in the current Playlist by the
    # Advance Part Limit, then the server SHOULD immediately return Bad
    # Request, such as HTTP 400.  The Advance Part Limit is three divided
    # by the Part Target Duration if the Part Target Duration is less than
    # one second, or three otherwise.

    # Current sequence number is 1 and part number is num_completed_parts-1
    # The following two tests should fail immediately:
    # - request with a _HLS_msn of 4
    # - request with a _HLS_msn of 1 and a _HLS_part of num_completed_parts-1+advance_part_limit
    assert (await hls_client.get("/playlist.m3u8?_HLS_msn=4")
            ).status == HTTPStatus.BAD_REQUEST
    assert (await hls_client.get(
        f"/playlist.m3u8?_HLS_msn=1&_HLS_part={num_completed_parts-1+hass.data[DOMAIN][ATTR_SETTINGS].hls_advance_part_limit}"
    )).status == HTTPStatus.BAD_REQUEST
    stream_worker_sync.resume()
Пример #16
0
        stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True

    try:
        url = yarl.URL(stream_source)
    except ValueError:
        return {CONF_STREAM_SOURCE: "malformed_url"}
    if not url.is_absolute():
        return {CONF_STREAM_SOURCE: "relative_url"}
    if not url.user and not url.password:
        username = info.get(CONF_USERNAME)
        password = info.get(CONF_PASSWORD)
        if username and password:
            url = url.with_user(username).with_password(password)
            stream_source = str(url)
    try:
        stream = create_stream(hass, stream_source, stream_options, "test_stream")
        hls_provider = stream.add_provider(HLS_PROVIDER)
        await stream.start()
        if not await hls_provider.part_recv(timeout=SOURCE_TIMEOUT):
            hass.async_create_task(stream.stop())
            return {CONF_STREAM_SOURCE: "timeout"}
        await stream.stop()
    except StreamWorkerError as err:
        return {CONF_STREAM_SOURCE: str(err)}
    except PermissionError:
        return {CONF_STREAM_SOURCE: "stream_not_permitted"}
    except OSError as err:
        if err.errno == EHOSTUNREACH:
            return {CONF_STREAM_SOURCE: "stream_no_route_to_host"}
        if err.errno == EIO:  # input/output error
            return {CONF_STREAM_SOURCE: "stream_io_error"}
Пример #17
0
async def test_ll_hls_stream(hass, hls_stream, stream_worker_sync):
    """
    Test hls stream.

    Purposefully not mocking anything here to test full
    integration with the stream component.
    """
    await async_setup_component(
        hass,
        "stream",
        {
            "stream": {
                CONF_LL_HLS: True,
                CONF_SEGMENT_DURATION: SEGMENT_DURATION,
                CONF_PART_DURATION: TEST_PART_DURATION,
            }
        },
    )

    stream_worker_sync.pause()

    # Setup demo HLS track
    source = generate_h264_video(duration=SEGMENT_DURATION + 1)
    stream = create_stream(hass, source, {})

    # Request stream
    stream.add_provider(HLS_PROVIDER)
    stream.start()

    hls_client = await hls_stream(stream)

    # Fetch playlist
    master_playlist_response = await hls_client.get()
    assert master_playlist_response.status == 200

    # Fetch init
    master_playlist = await master_playlist_response.text()
    init_response = await hls_client.get("/init.mp4")
    assert init_response.status == 200

    # Fetch playlist
    playlist_url = "/" + master_playlist.splitlines()[-1]
    playlist_response = await hls_client.get(playlist_url)
    assert playlist_response.status == 200

    # Fetch segments
    playlist = await playlist_response.text()
    segment_re = re.compile(r"^(?P<segment_url>./segment/\d+\.m4s)")
    for line in playlist.splitlines():
        match = segment_re.match(line)
        if match:
            segment_url = "/" + match.group("segment_url")
            segment_response = await hls_client.get(segment_url)
            assert segment_response.status == 200

    def check_part_is_moof_mdat(data: bytes):
        if len(data) < 8 or data[4:8] != b"moof":
            return False
        moof_length = int.from_bytes(data[0:4], byteorder="big")
        if (len(data) < moof_length + 8
                or data[moof_length + 4:moof_length + 8] != b"mdat"):
            return False
        mdat_length = int.from_bytes(data[moof_length:moof_length + 4],
                                     byteorder="big")
        if mdat_length + moof_length != len(data):
            return False
        return True

    # Fetch all completed part segments
    part_re = re.compile(
        r'#EXT-X-PART:DURATION=[0-9].[0-9]{5,5},URI="(?P<part_url>.+?)",BYTERANGE="(?P<byterange_length>[0-9]+?)@(?P<byterange_start>[0-9]+?)"(,INDEPENDENT=YES)?'
    )
    for line in playlist.splitlines():
        match = part_re.match(line)
        if match:
            part_segment_url = "/" + match.group("part_url")
            byterange_end = (int(match.group("byterange_length")) +
                             int(match.group("byterange_start")) - 1)
            part_segment_response = await hls_client.get(
                part_segment_url,
                headers={
                    "Range":
                    f'bytes={match.group("byterange_start")}-{byterange_end}'
                },
            )
            assert part_segment_response.status == 206
            assert check_part_is_moof_mdat(await part_segment_response.read())

    stream_worker_sync.resume()

    # Stop stream, if it hasn't quit already
    stream.stop()

    # Ensure playlist not accessible after stream ends
    fail_response = await hls_client.get()
    assert fail_response.status == HTTP_NOT_FOUND