Rework Subtitle Code

This commit is contained in:
Neil Burrows 2021-06-12 16:03:47 +01:00
parent 91036339f1
commit d8d1745720
5 changed files with 144 additions and 166 deletions

View File

@ -1,6 +1,8 @@
sub init()
m.top.observeField("state", "onState")
m.bufferPercentage = 0 ' Track whether content is being loaded
m.top.transcodeReasons = []
end sub

View File

@ -7,13 +7,19 @@
<field id="Subtitles" type="array" />
<field id="SelectedSubtitle" type="integer" />
<field id="captionMode" type="string" />
<field id="transcodeParams" type="assocarray" />
<field id="container" type="string" />
<field id="directPlaySupported" type="boolean" />
<field id="decodeAudioSupported" type="boolean" />
<field id="isTranscoded" type="boolean" />
<field id="systemOverlay" type="boolean" value="false" />
<field id="showID" type="string" />
<field id="transcodeParams" type="assocarray" />
<field id="isTranscoded" type="boolean" />
<field id="transcodeReasons" type="array" />
<field id="videoId" type="string" />
<field id="mediaSourceId" type="string" />
<field id="audioIndex" type="integer" />
</interface>
<script type="text/brightscript" uri="JFVideo.brs" />
<children>

View File

@ -1,9 +1,11 @@
function VideoPlayer(id, audio_stream_idx = 1)
function VideoPlayer(id, audio_stream_idx = 1, subtitle_idx = -1)
' Get video controls and UI
video = CreateObject("roSGNode", "JFVideo")
video.id = id
video = VideoContent(video, audio_stream_idx)
if video = invalid
AddVideoContent(video, audio_stream_idx, subtitle_idx)
if video.content = invalid
return invalid
end if
jellyfin_blue = "#00a4dcFF"
@ -14,178 +16,123 @@ function VideoPlayer(id, audio_stream_idx = 1)
return video
end function
function VideoContent(video, audio_stream_idx = 1) as object
' Get video stream
sub AddVideoContent(video, audio_stream_idx = 1, subtitle_idx = -1, playbackPosition = -1)
video.content = createObject("RoSGNode", "ContentNode")
params = {}
meta = ItemMetaData(video.id)
if meta = invalid return invalid
if meta = invalid then
video.content = invalid
return
end if
video.content.title = meta.title
video.showID = meta.showID
' If there is a last playback positon, ask user if they want to resume
position = meta.json.UserData.PlaybackPositionTicks
if position > 0 then
dialogResult = startPlayBackOver(position)
'Dialog returns -1 when back pressed, 0 for resume, and 1 for start over
if dialogResult = -1 then
'User pressed back, return invalid and don't load video
return invalid
else if dialogResult = 1 then
'Start Over selected, change position to 0
position = 0
else if dialogResult = 2 then
'Mark this item as watched, refresh the page, and return invalid so we don't load the video
MarkItemWatched(video.id)
video.content.watched = not video.content.watched
group = m.scene.focusedChild
group.timeLastRefresh = CreateObject("roDateTime").AsSeconds()
group.callFunc("refresh")
return invalid
if playbackPosition = -1 then
playbackPosition = meta.json.UserData.PlaybackPositionTicks
if playbackPosition > 0 then
dialogResult = startPlayBackOver(playbackPosition)
'Dialog returns -1 when back pressed, 0 for resume, and 1 for start over
if dialogResult = -1 then
'User pressed back, return invalid and don't load video
video.content = invalid
return
else if dialogResult = 1 then
'Start Over selected, change position to 0
playbackPosition = 0
else if dialogResult = 2 then
'Mark this item as watched, refresh the page, and return invalid so we don't load the video
MarkItemWatched(video.id)
video.content.watched = not video.content.watched
group = m.scene.focusedChild
group.timeLastRefresh = CreateObject("roDateTime").AsSeconds()
group.callFunc("refresh")
video.content = invalid
return
end if
end if
end if
video.content.PlayStart = int(position/10000000)
video.content.PlayStart = int(playbackPosition / 10000000)
playbackInfo = ItemPostPlaybackInfo(video.id, position)
' Call PlayInfo from server
mediaSourceId = video.id
if meta.live then mediaSourceId = "" ' Don't send mediaSourceId for Live media
playbackInfo = ItemPostPlaybackInfo(video.id, mediaSourceId, audio_stream_idx, subtitle_idx, playbackPosition)
video.videoId = video.id
video.mediaSourceId = video.id
video.audioIndex = audio_stream_idx
if playbackInfo = invalid then
return invalid
video.content = invalid
return
end if
params = {}
video.PlaySessionId = playbackInfo.PlaySessionId
if meta.live then
video.content.live = true
video.content.StreamFormat = "hls"
'Original MediaSource seems to be a placeholder and real stream data is available
'after POSTing to PlaybackInfo
json = meta.json
json.AddReplace("MediaSources", playbackInfo.MediaSources)
json.AddReplace("MediaStreams", playbackInfo.MediaSources[0].MediaStreams)
meta.json = json
end if
container = getContainerType(meta)
video.container = container
video.container = getContainerType(meta)
transcodeParams = getTranscodeParameters(meta, audio_stream_idx)
transcodeParams.append({"PlaySessionId": video.PlaySessionId})
subtitles = sortSubtitles(meta.id, playbackInfo.MediaSources[0].MediaStreams)
video.Subtitles = subtitles["all"]
if meta.live then
_livestream_params = {
video.transcodeParams = {
"MediaSourceId": playbackInfo.MediaSources[0].Id,
"LiveStreamId": playbackInfo.MediaSources[0].LiveStreamId,
"MinSegments": 2 'This is a guess about initial buffer size, segments are 3s each
"PlaySessionId": video.PlaySessionId
}
params.append(_livestream_params)
transcodeParams.append(_livestream_params)
end if
subtitles = sortSubtitles(meta.id,meta.json.MediaStreams)
video.Subtitles = subtitles["all"]
video.content.SubtitleTracks = subtitles["text"]
'TODO: allow user selection of subtitle track before playback initiated, for now set to first track
if video.Subtitles.count() then
video.SelectedSubtitle = 0
else
video.SelectedSubtitle = -1
end if
' 'TODO: allow user selection of subtitle track before playback initiated, for now set to no subtitles
video.SelectedSubtitle = -1
if video.SelectedSubtitle <> -1 and displaySubtitlesByUserConfig(video.Subtitles[video.SelectedSubtitle], meta.json.MediaStreams[audio_stream_idx]) then
if video.Subtitles[0].IsTextSubtitleStream then
video.subtitleTrack = video.availableSubtitleTracks[video.Subtitles[0].TextIndex].TrackName
video.suppressCaptions = false
else
video.suppressCaptions = true
'Watch to see if system overlay opened/closed to change transcoding if caption mode changed
m.device.EnableAppFocusEvent(True)
video.captionMode = video.globalCaptionMode
if video.globalCaptionMode = "On" or (video.globalCaptionMode = "When mute" and m.mute = true) then
'Only transcode if subtitles are turned on
transcodeParams.append({"SubtitleStreamIndex" : video.Subtitles[0].index })
end if
end if
else
video.suppressCaptions = true
video.SelectedSubtitle = -1
end if
video.directPlaySupported = playbackInfo.MediaSources[0].SupportsDirectPlay
video.directPlaySupported = directPlaySupported(meta)
video.decodeAudioSupported = decodeAudioSupported(meta, audio_stream_idx)
video.transcodeParams = transcodeParams
if video.directPlaySupported and video.decodeAudioSupported and transcodeParams.SubtitleStreamIndex = invalid then
if video.directPlaySupported then
params.append({
"Static": "true",
"Container": container,
"Container": video.container,
"PlaySessionId": video.PlaySessionId,
"AudioStreamIndex": audio_stream_idx
})
video.content.url = buildURL(Substitute("Videos/{0}/stream", video.id), params)
video.content.streamformat = container
video.content.switchingStrategy = ""
video.isTranscode = False
video.audioTrack = audio_stream_idx + 1 ' Tell Roku what Audio Track to play (convert from 0 based index for roku)
video.isTranscoded = false
else
video.content.url = buildURL(Substitute("Videos/{0}/master.m3u8", video.id), transcodeParams)
' Get transcoding reason
video.transcodeReasons = getTranscodeReasons(playbackInfo.MediaSources[0].TranscodingUrl)
video.content.url = buildURL(playbackInfo.MediaSources[0].TranscodingUrl)
video.isTranscoded = true
end if
video.content = authorize_request(video.content)
' todo - audioFormat is read only
video.content.audioFormat = getAudioFormat(meta)
video.content.setCertificatesFile("common:/certs/ca-bundle.crt")
return video
end function
end sub
function getTranscodeParameters(meta as object, audio_stream_idx = 1)
'
' Extract array of Transcode Reasons from the content URL
' @returns Array of Strings
function getTranscodeReasons(url as string) as object
params = {"AudioStreamIndex": audio_stream_idx}
if decodeAudioSupported(meta, audio_stream_idx) and meta.json.MediaStreams[audio_stream_idx] <> invalid and meta.json.MediaStreams[audio_stream_idx].Type = "Audio" then
audioCodec = meta.json.MediaStreams[audio_stream_idx].codec
audioChannels = meta.json.MediaStreams[audio_stream_idx].channels
else
params.Append({"AudioCodec": "aac"})
regex = CreateObject("roRegex", "&TranscodeReasons=([^&]*)", "")
match = regex.Match(url)
' If 5.1 Audio Output is connected then allow transcoding to 5.1
di = CreateObject("roDeviceInfo")
if di.GetAudioOutputChannel() = "5.1 surround" and di.CanDecodeAudio({ Codec: "aac", ChCnt: 6 }).result then
params.Append({"MaxAudioChannels": "6"})
else
params.Append({"MaxAudioChannels": "2"})
end if
if match.count() > 1
return match[1].Split(",")
end if
streamInfo = {}
if meta.json.MediaStreams[0] <> invalid and meta.json.MediaStreams[0].codec <> invalid then
streamInfo.Codec = meta.json.MediaStreams[0].codec
end if
if meta.json.MediaStreams[0] <> invalid and meta.json.MediaStreams[0].Profile <> invalid and meta.json.MediaStreams[0].Profile.len() > 0 then
streamInfo.Profile = LCase(meta.json.MediaStreams[0].Profile)
end if
if meta.json.MediaSources[0] <> invalid and meta.json.MediaSources[0].container <> invalid and meta.json.MediaSources[0].container.len() > 0 then
streamInfo.Container = meta.json.MediaSources[0].container
end if
devinfo = CreateObject("roDeviceInfo")
res = devinfo.CanDecodeVideo(streamInfo)
if res = invalid or res.result = invalid or res.result = false then
params.Append({"VideoCodec": "h264"})
streamInfo.Profile = "h264"
streamInfo.Container = "ts"
end if
params.Append({"MediaSourceId": meta.id})
params.Append({"DeviceId": devinfo.getChannelClientID()})
return params
return []
end function
'Checks available subtitle tracks and puts subtitles in forced, default, and non-default/forced but preferred language at the top
@ -206,9 +153,10 @@ function sortSubtitles(id as string, MediaStreams)
"Track": { "Language" : stream.language, "Description": stream.displaytitle , "TrackName": url },
"IsTextSubtitleStream": stream.IsTextSubtitleStream,
"Index": stream.index,
"TextIndex": -1,
"IsDefault": stream.IsDefault,
"IsForced": stream.IsForced
"IsForced": stream.IsForced,
"IsExternal": stream.IsExternal
"IsEncoded": stream.DeliveryMethod = "Encode"
}
if stream.isForced then
trackType = "forced"
@ -224,12 +172,16 @@ function sortSubtitles(id as string, MediaStreams)
end if
end if
end for
tracks["default"].append(tracks["normal"])
tracks["forced"].append(tracks["default"])
textTracks = []
for i = 0 to tracks["forced"].count() - 1
if tracks["forced"][i].IsTextSubtitleStream then tracks["forced"][i].TextIndex = textTracks.count()
textTracks.push(tracks["forced"][i].Track)
if tracks["forced"][i].IsTextSubtitleStream then
tracks["forced"][i].TextIndex = textTracks.count()
textTracks.push(tracks["forced"][i].Track)
end if
end for
return { "all" : tracks["forced"], "text": textTracks }
end function
@ -321,7 +273,7 @@ end function
function ReportPlayback(video, state = "update" as string)
if video = invalid or video.position = invalid then return
if video = invalid or video.position = invalid then return invalid
params = {
"PlaySessionId": video.PlaySessionId,

View File

@ -10,7 +10,7 @@ function ItemGetPlaybackInfo(id as string, StartTimeTicks = 0 as longinteger)
return getJson(resp)
end function
function ItemPostPlaybackInfo(id as string, StartTimeTicks = 0 as longinteger)
function ItemPostPlaybackInfo(id as string, mediaSourceId = "" as string , audioTrackIndex = -1 as integer, subtitleTrackIndex = -1 as integer, startTimeTicks = 0 as longinteger)
body = {
"DeviceProfile": getDeviceProfile()
}
@ -19,8 +19,14 @@ function ItemPostPlaybackInfo(id as string, StartTimeTicks = 0 as longinteger)
"StartTimeTicks": StartTimeTicks,
"IsPlayback": true,
"AutoOpenLiveStream": true,
"MaxStreamingBitrate": "140000000"
"MaxStreamingBitrate": "140000000",
"SubtitleStreamIndex": subtitleTrackIndex
}
if mediaSourceId <> "" then params.MediaSourceId = mediaSourceId
if audioTrackIndex > -1 then params.AudioStreamIndex = audioTrackIndex
req = APIRequest(Substitute("Items/{0}/PlaybackInfo", id), params)
req.SetRequest("POST")
return postJson(req, FormatJson(body))

View File

@ -1,13 +1,15 @@
function selectSubtitleTrack(tracks, current = -1)
function selectSubtitleTrack(tracks, current = -1) as integer
video = m.scene.focusedChild
trackSelected = selectSubtitleTrackDialog(video.Subtitles, video.SelectedSubtitle)
if trackSelected = -1 then
if trackSelected = invalid or trackSelected = -1 then
return invalid
else
return trackSelected - 1
end if
end function
' Present Dialog to user to select subtitle track
function selectSubtitleTrackDialog(tracks, currentTrack = -1)
iso6392 = getSubtitleLanguages()
options = ["None"]
@ -28,50 +30,60 @@ function selectSubtitleTrackDialog(tracks, currentTrack = -1)
end function
sub changeSubtitleDuringPlayback(newid)
if newid = invalid then return
if newid = -1 then
' If no subtitles set
if newid = invalid or newid = -1 then
turnoffSubtitles()
return
end if
video = m.scene.focusedChild
oldTrack = video.Subtitles[video.SelectedSubtitle]
newTrack = video.Subtitles[newid]
video.captionMode = video.globalCaptionMode
m.device.EnableAppFocusEvent(not newTrack.IsTextSubtitleStream)
video.SelectedSubtitle = newid
' If no change of subtitle track, return
if newId = video.SelectedSubtitle then return
if newTrack.IsTextSubtitleStream then
if video.content.PlayStart > video.position
'User has rewinded to before playback was initiated. The Roku never loaded this portion of the text subtitle
'Changing the track will cause plaback to jump to initial bookmark position.
video.suppressCaptions = true
rebuildURL(false)
end if
video.subtitleTrack = video.availableSubtitleTracks[newTrack.TextIndex].TrackName
video.suppressCaptions = false
currentSubtitles = video.Subtitles[video.SelectedSubtitle]
newSubtitles = video.Subtitles[newid]
if newSubtitles.IsEncoded then
' Switching to Encoded Subtitle stream
video.control = "stop"
AddVideoContent(video, video.audioIndex, newSubtitles.Index, video.position * 10000000)
video.control = "play"
video.globalCaptionMode = "Off" ' Using encoded subtitles - so turn off text subtitles
else if (currentSubtitles <> invalid AND currentSubtitles.IsEncoded) then
' Switching from an Encoded stream to a text stream
video.control = "stop"
AddVideoContent(video, video.audioIndex, -1, video.position * 10000000)
video.control = "play"
video.globalCaptionMode = "On"
video.subtitleTrack = video.availableSubtitleTracks[newSubtitles.TextIndex].TrackName
else
video.suppressCaptions = true
' Switch to Text Subtitle Track
video.globalCaptionMode = "On"
video.subtitleTrack = video.availableSubtitleTracks[newSubtitles.TextIndex].TrackName
end if
'Rebuild URL if subtitle track is video or if changed from video subtitle to text subtitle.
if not newTrack.IsTextSubtitleStream then
rebuildURL(true)
else if oldTrack <> invalid and not oldTrack.IsTextSubtitleStream then
rebuildURL(false)
if newTrack.TextIndex > 0 then video.subtitleTrack = video.availableSubtitleTracks[newTrack.TextIndex].TrackName
end if
video.SelectedSubtitle = newId
end sub
function turnoffSubtitles()
video = m.scene.focusedChild
current = video.SelectedSubtitle
video.SelectedSubtitle = -1
video.suppressCaptions = true
video.globalCaptionMode = "Off"
m.device.EnableAppFocusEvent(false)
if current > -1 and not video.Subtitles[current].IsTextSubtitleStream then
rebuildURL(false)
' Check if Enoded subtitles are being displayed, and turn off
if current > -1 and video.Subtitles[current].IsEncoded then
video.control = "stop"
AddVideoContent(video, video.audioIndex, -1, video.position * 10000000)
video.control = "play"
end if
end function