Skip to content

Commit

Permalink
handle WebVTT captions in HLS manifests
Browse files Browse the repository at this point in the history
* add new helper methods to grab the URI for a WebVTT file included in a HLS manifest
* insert new <track/> tags specifically for HLS streams
* inject HLS.js configuration options to hide captions it renders

this will require setting the cupertinoVODCaptionsUseWebVTT property in all
wowza collections to true.

squashed refactors:

* refactor new hls vtt private methods in track extensions
* remove track tag for iOS as it is unnecessary
  • Loading branch information
anarchivist committed Jul 8, 2024
1 parent 921da6e commit cbb81e3
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 2 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ gem 'berkeley_library-docker', '~> 0.2.0'
gem 'berkeley_library-logging', '~> 0.2'
gem 'browser', '~> 4.2'
gem 'jbuilder', '~> 2.7'
gem 'm3u8', '~> 0.8.2'
gem 'non-stupid-digest-assets', '~> 1.0' # Allow static pages (e.g. 404.html) to link to compiled assets
gem 'omniauth-cas', '~> 2.0'
gem 'puma', '~> 5.3', '>= 5.3.1'
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ GEM
loofah (2.21.4)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
m3u8 (0.8.2)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
Expand Down Expand Up @@ -352,6 +353,7 @@ PLATFORMS
aarch64-linux
arm64-darwin-21
arm64-darwin-22
arm64-darwin-23
x86_64-darwin-19
x86_64-linux

Expand All @@ -368,6 +370,7 @@ DEPENDENCIES
dotenv-rails
jbuilder (~> 2.7)
listen (>= 3.0)
m3u8 (~> 0.8.2)
non-stupid-digest-assets (~> 1.0)
omniauth-cas (~> 2.0)
puma (~> 5.3, >= 5.3.1)
Expand Down
3 changes: 3 additions & 0 deletions app/views/player/_player_head_additional.erb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@

function initialize_players() {
$("audio,video").mediaelementplayer({
// workaround for mediaelement/mediaelement#2963
// (when the HLS stream contains WebVTT captions)
hls: { enableWebVTT: false },
iconSprite: "/assets/icons/mejs-controls.svg",
success: function (mediaElement, originalNode, instance) {
if (typeof dashjs !== "undefined") {
Expand Down
2 changes: 1 addition & 1 deletion app/views/player/_video.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
<video id="video-<%= index %>" width="100%" height="270" preload="<%= preload %>" controls crossorigin="anonymous">
<% if browser.platform.ios? || browser.device.ipad? %>
<source src="<%= track.hls_uri %>" type="<%= BerkeleyLibrary::AV::Track::SOURCE_TYPE_HLS %>"/>
<%# a separate track tag is not necessary here for captions as Wowza sends them as part of the HLS stream, and iOS will auto discover them %>
<% else %>
<source src="<%= track.mpeg_dash_uri %>" type="<%= BerkeleyLibrary::AV::Track::SOURCE_TYPE_MPEG_DASH %>"/>
<% if (dash_vtt_uri = track.dash_vtt_uri) %>
<%# TODO: should we also be adding a caption track for HLS streaming to iPhone/iPad? %>
<track kind="captions" srclang="en" src="<%= dash_vtt_uri %>"/>
<% end %>
<% end %>
Expand Down
29 changes: 28 additions & 1 deletion lib/av_player/track_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

module BerkeleyLibrary
module AV
class Track
class Track # rubocop:disable Metrics/ClassLength
include BerkeleyLibrary::Logging

COLLECTION_RE = %r{(^[^/]+)/}
Expand All @@ -22,6 +22,12 @@ def hls_uri
@hls_uri ||= build_hls_uri
end

def hls_vtt_uri
return @hls_vtt_uri if instance_variable_defined?(:@hls_vtt_uri)

@hls_vtt_uri ||= find_hls_vtt_uri
end

def mpeg_dash_uri
return @mpeg_dash_uri if instance_variable_defined?(:@mpeg_dash_uri)

Expand Down Expand Up @@ -70,6 +76,27 @@ def build_mpeg_dash_uri
log_invalid_uri(relative_path, e)
end

def find_hls_vtt_uri
return unless hls_uri_exists?
return unless (hls_manifest_uri = hls_uri)
return unless (hls_manifest = do_get(hls_manifest_uri, ignore_errors: true))
return unless (subtitle_list_uri = find_hls_subtitle_list_uri(hls_manifest))
return unless (hls_subtitle_list = do_get(subtitle_list_uri, ignore_errors: true))

vtt_playlist = M3u8::Playlist.read(hls_subtitle_list)
return unless (hls_vtt_path_relative = vtt_playlist.items.first.segment)

hls_uri.merge(hls_vtt_path_relative)
end

def find_hls_subtitle_list_uri(manifest)
return unless (playlist = M3u8::Playlist.read(manifest))
return unless (subtitle_list = playlist.items.find { |p| p.group_id == 'subs' })
return unless (subtitle_list_uri = subtitle_list.uri)

hls_uri.merge(subtitle_list_uri)
end

def find_dash_vtt_uri
return unless (dash_uri = mpeg_dash_uri)
return unless (dash_manifest = do_get(dash_uri, ignore_errors: true))
Expand Down
5 changes: 5 additions & 0 deletions spec/data/breslin1-playlist.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",FORCED=NO,AUTOSELECT=YES,URI="subtitlelist_leng_w289808588.m3u8",LANGUAGE="eng"
#EXT-X-STREAM-INF:BANDWIDTH=294961,CODECS="avc1.4d4028,mp4a.40.2",RESOLUTION=320x212,SUBTITLES="subs"
chunklist_w289808588.m3u8
7 changes: 7 additions & 0 deletions spec/data/subtitlelist_leng_w289808588.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:250
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:140.165,
subtitlechunk_leng_w289808588_0.webvtt
#EXT-X-ENDLIST
19 changes: 19 additions & 0 deletions spec/lib/av_player/track_extensions_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,25 @@ module AV
expected_uri = URI.join(AV::Config.wowza_base_uri, expected_vtt_path)
expect(track.dash_vtt_uri).to eq(expected_uri)
end

describe :hls_vtt_uri do
it 'returns the HLS VTT URI' do
track = Track.new(sort_order: 0, path: 'ROHOVideo/breslin1.mp4')
hls_uri = track.hls_uri

expected_manifest_path = '/ROHOVideo/mp4:breslin1.mp4/playlist.m3u8'
expect(hls_uri.path).to eq(expected_manifest_path) # just to be sure

stub_request(:head, hls_uri).to_return(status: 200)
stub_request(:get, hls_uri).to_return(body: File.read('spec/data/breslin1-playlist.m3u8'))
subtitle_list_uri = hls_uri.merge('subtitlelist_leng_w289808588.m3u8')
stub_request(:get, subtitle_list_uri).to_return(body: File.read('spec/data/subtitlelist_leng_w289808588.m3u8'))

expected_vtt_path = expected_manifest_path.sub(%r{[^/]+$}, 'subtitlechunk_leng_w289808588_0.webvtt')
expected_uri = URI.join(AV::Config.wowza_base_uri, expected_vtt_path)
expect(track.hls_vtt_uri).to eq(expected_uri)
end
end
end
end
end
Expand Down

0 comments on commit cbb81e3

Please sign in to comment.