mirror of
https://github.com/jellyfin/jellyfin-roku.git
synced 2024-11-23 14:19:40 +00:00
Rework Subtitle Code
This commit is contained in:
parent
91036339f1
commit
d8d1745720
@ -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
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user