Skip to content

Commit

Permalink
Merge pull request #2 from membraneframework/support-for-byterange-tag
Browse files Browse the repository at this point in the history
Add support for byterange tag and nested segment tags
  • Loading branch information
Qizot authored Feb 20, 2024
2 parents 72b2611 + 16ee2c4 commit e68a227
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 10 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The package can be installed by adding `ex_m3u8` into your list of dependencies
```elixir
def deps do
[
{:ex_m3u8, "~> 0.9.0"}
{:ex_m3u8, "~> 0.10.0"}
]
end
```
Expand Down
57 changes: 49 additions & 8 deletions lib/ex_m3u8/deserializer/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ defmodule ExM3U8.Deserializer.Parser do
]
@timeline_tags [
:program_date_time,
:byterange,
:media_init,
:part,
:segment,
Expand Down Expand Up @@ -156,16 +157,10 @@ defmodule ExM3U8.Deserializer.Parser do
end
end

parse_raw "#EXTINF:" do
defp do_parse(["#EXTINF:" <> value | lines], acc, opts) do
case Float.parse(value) do
{duration, ","} ->
case lines do
[uri | lines] ->
{:ok, :segment, %ExM3U8.Tags.Segment{duration: duration, uri: uri}, lines}

[] ->
{:error, "segment uri missing"}
end
parse_segment(duration, lines, acc, opts)

:error ->
{:error, "invalid segment duration"}
Expand All @@ -182,6 +177,28 @@ defmodule ExM3U8.Deserializer.Parser do
end
end

parse_tag "BYTERANGE" do
case String.split(value, "@") do
[length] ->
case Integer.parse(length) do
{length, ""} -> {:ok, :byterange, %ExM3U8.Tags.ByteRange{length: length}}
:error -> {:error, "invalid byterange"}
end

[length, offset] ->
with {length, ""} <- Integer.parse(length),
{offset, ""} <- Integer.parse(offset) do
{:ok, :byterange, %ExM3U8.Tags.ByteRange{length: length, offset: offset}}
else
_other ->
{:error, "invalid byterange"}
end

_other ->
{:error, "invalid byterange"}
end
end

parse_tag "MAP" do
case AttributesList.parse(value) do
{:ok, %{"URI" => uri}} ->
Expand Down Expand Up @@ -380,4 +397,28 @@ defmodule ExM3U8.Deserializer.Parser do
{lines, acc} -> do_parse(lines, acc, opts)
end
end

defp parse_segment(duration, lines, acc, opts) do
{segment_tags, lines} = Enum.split_while(lines, &String.starts_with?(&1, "#"))

case do_parse(segment_tags, [], opts) do
{:error, _reason} = error ->
error

segment_tags ->
acc = Enum.reduce(segment_tags, acc, &[&1 | &2])

case lines do
[uri | lines] ->
do_parse(
lines,
[{:segment, %ExM3U8.Tags.Segment{duration: duration, uri: uri}} | acc],
opts
)

[] ->
{:error, "missing segment uri"}
end
end
end
end
29 changes: 29 additions & 0 deletions lib/ex_m3u8/tags/byte_range.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule ExM3U8.Tags.ByteRange do
@moduledoc """
Structure representing byte range of the following media segment/chunk.
"""
use TypedStruct

typedstruct enforce: true do
field :length, non_neg_integer()
field :offset, non_neg_integer() | nil, default: nil
end

defimpl ExM3U8.Serializer do
use ExM3U8.DSL

require ExM3U8.Helpers

@impl true
def serialize(%@for{length: length, offset: offset}) do
offset =
if offset do
"@#{offset}"
else
""
end

[ExM3U8.Helpers.tag_prefix(), "BYTERANGE:", "#{length}#{offset}"]
end
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule ExM3U8.MixProject do
use Mix.Project

@version "0.9.0"
@version "0.10.0"
@github_url "https://github.com/membraneframework/ex_m3u8"

def project do
Expand Down
24 changes: 24 additions & 0 deletions test/ex_m3u8/media_playlist_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,30 @@ defmodule ExM3U8.MediaPlaylistTest do
assert {:ok, ^playlist} = ExM3U8.Deserializer.Parser.parse_media_playlist(manifest)
end

test "deserialize segments with nested tags" do
manifest = """
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:6
#EXT-X-PART-INF:PART-TARGET=1.0
#EXTINF:3.0,
#EXT-X-BYTERANGE:426056@349786
#EXT-X-DISCONTINUITY
#EXT-X-PROGRAM-DATE-TIME:2077-12-12T12:00:00Z
segment1.m4s
"""

timeline = [
%ExM3U8.Tags.ByteRange{offset: 349_786, length: 426_056},
%ExM3U8.Tags.Discontinuity{},
%ExM3U8.Tags.ProgramDateTime{date: ~U[2077-12-12 12:00:00Z]},
%ExM3U8.Tags.Segment{uri: "segment1.m4s", duration: 3.0}
]

assert {:ok, playlist} = ExM3U8.Deserializer.Parser.parse_media_playlist(manifest)
assert playlist.timeline == timeline
end

defmodule CustomTag do
@moduledoc false

Expand Down
27 changes: 27 additions & 0 deletions test/ex_m3u8/tags/byte_range_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
defmodule ExM3U8.Tags.ByteRangeTest do
use ExUnit.Case

import ExM3U8.TestUtils, only: [serialize: 1]

test "serialize byte range" do
byte_range = %ExM3U8.Tags.ByteRange{
length: 5,
offset: nil
}

assert """
#EXT-X-BYTERANGE:5
"""
|> String.trim_trailing() == serialize(byte_range)

byte_range = %ExM3U8.Tags.ByteRange{
length: 5,
offset: 111
}

assert """
#EXT-X-BYTERANGE:5@111
"""
|> String.trim_trailing() == serialize(byte_range)
end
end

0 comments on commit e68a227

Please sign in to comment.