App Stops Receiving onKeyEvent Inputs

EDIT: Added another file

Hello, I am fairly new to Roku development and am running into an incredibly frustrating issue left behind by a previous dev. I have an app that lets you navigate between a list of fullscreen videos and photos. Going between them mostly works fine, however when I transition from a video into a photo, the app completely stops accepting any input from my remote. I have monitored all onKeyEvent triggers and can confirm that none of them throughout the app trigger once this state is made. The app doesn’t crash and it doesn’t give any errors in the debug console. Backing out of the fullscreen mode is impossible once this happens as well. Thankfully, the home button still works so I can exit the app but that’s it. There is no further functionality within the app. I’m at a complete loss as to what this issue may be. Below I share some of the core functions used in this functionality, but please let me know if there’s anything more specific I may be able to pass along that could help us to figure out how to solve this issue.

sub _onCurrentMediaIndexChanged(event)
    print "onCurrentMediaIndexChanged"
    index = event.getData()
    currentItemNode = m.top.mediaPlaylistNode.getChild(index)
    print "onCurrentMediaIndexChanged", currentItemNode
    _showMedia(currentItemNode)
end sub

sub _addImageViewForPreviewMediaItems(previewImages)
    ' print "addImageViewForPreviewMediaItems", m._slideShow
    if IsInvalid(m._slideShow)
        m._slideShow = m.top.createChild("ImageSlideshow")
    end if
    m._slideShow.update({
        id: "slideShow"
        uri: previewImages[0][ToString(displaySize().width)]
        loadDisplayMode: "limitSize"
        loadWidth: 1920
        loadHeight: 1080
        animationDuration: 3
        content: previewImages
        interval: 7
        control: "start"
        opacity: 0.5
    })
end sub

sub _showMedia(mediaItemNode as object)
    print "showMedia"
    m.itemMediaType = getValueForPath(mediaItemNode, "dataItem.elementType")

    if m._lastMediaType <> m.itemMediaType
        _cleanUpSubviews()
    end if

    ' print mediaItemNode
    ' print mediaItemNode.dataItem
    ' print mediaItemNode.dataItem.elementType
    if mediaItemNode.dataItem.elementType = "photo"
        ' m.continuesPlaytimer.control = "stop"
        print "ADD PHOTO"
        _addPhotoViewForMediaItem(mediaItemNode.dataItem)
    else if mediaItemNode.dataItem.elementType = "video"
        _addVideoViewForMediaItem(mediaItemNode.dataItem)
    end if
    m._lastMediaType = m.itemMediaType
end sub

sub _addPhotoViewForMediaItem(item as object)
    print "addPhotoViewForMediaItem"
    if m.top.isContinuousPlayEnabled then
        m.continuesPlaytimer.control = "stop"
        m.continuesPlaytimer.control = "start"
    end if

    if IsInvalid(m._photoView)
        m._photoView = m.top.createChild("FadingBackground")
    end if

    m._photoView.update({
        id: "photoView"
        uri: getValueForPath(item, "srcMediaUrl")
        width: 1920
        height: 1080
        loadDisplayMode: "scaleToFit"
        loadWidth: 1920
        loadHeight: 1080
        animationDuration: 0.5
    })
end sub

sub _addVideoComponent()
    ' print "addVideoComponent"
    if IsInvalid(m._videoView)
        m._videoView = m.top.createChild("Video")
        m._videoView.observeField("state", "_onVideoStateChanged")
    end if
end sub

sub _addCustomPlayer()
    ' print "addCustomPlayer"
    if IsInvalid(m._customPlayer)
        m._customPlayer = m.top.createChild("PlayerScreen")
        m._customPlayer.observeField("showPrevious", "_showPrevious")
        m._customPlayer.observeField("showNext", "_showNext")
        m._customPlayer.observeField("state", "_onVideoPlayerStateChanged")
    end if
end sub

sub _addVideoViewForPreviewMediaItems(items as object)
    ' print "addVideoViewForPreviewMediaItems"
    _addVideoComponent()
    m._videoView.update({
        id: "videoView"
        content: _getPreviewVideoContentNodeFor(items)
        width: 1920
        height: 1080
        contentIsPlaylist: true
        contentIndex: 0
        loop: true
        mute: true
        enableUI: false
    })
    m._videoView.control = PlaybackAction().play
    m._videoPrewievMask = m.top.createChild("Poster")
    m._videoPrewievMask.update({
        uri: "pkg:/images/videoPrewievMask.png"
        width: 1920
        height: 1080
    })
end sub

sub _addVideoViewForMediaItem(item as object)
    print "addVideoViewForMediaItem"
    _addCustomPlayer()
    m._customPlayer.update({
        id: "customPlayer"
        content: _getVideoContentNodeFor(item)
        width: 1920
        height: 1080
        control: PlaybackAction().play
    })
    m._customPlayer.setFocus(true)
end sub

sub _onVideoStateChanged(event)
    ' print "onVideoStateChanged"
    state = event.getData()
    errorMessage = m._videoView
    if state = PlaybackState().finished and m.top.isPreview
        showed = _showNext()
        if not _showNext()
            m.top.currentMediaIndex = 0
        end if
    end if
end sub

sub _cleanUpSubviews()
    print "cleanUpSubviews"
    if IsValid(m._photoView)
        m.top.removeChild(m._photoView)
        m._photoView = invalid
    end if
    if IsValid(m._videoView)
        m.top.removeChild(m._videoView)
        m._videoView = invalid
    end if
    if IsValid(m._customPlayer)
        m.top.removeChild(m._customPlayer)
        m._customPlayer = invalid
    end if
    if IsValid(m._slideShow)
        m.top.removeChild(m._slideShow)
        m._slideShow = invalid
    end if
    if IsValid(m._videoPrewievMask)
        m.top.removeChild(m._videoPrewievMask)
        m._videoPrewievMask = invalid
    end if
end sub

function _getVideoContentNodeFor(item) as object
    ' print "getVideoContentNodeFor"
    webCQUrl = getValueForPath(item, "editor_streams.web_cq.url")
    hlsUrl = getValueForPath(item, "editor_streams.hls.url", webCQUrl)

    content = CreateObject("roSGnode", "ContentNode")
    content.url = defaultValueIfInvalid(getValue(item, "srcMediaUrl"), defaultValueIfInvalid(webCQUrl, hlsUrl))
    if item.isEdited
        content.url = defaultValueIfInvalid(defaultValueIfInvalid(webCQUrl, hlsUrl), getValue(item, "srcMediaUrl"))
    end if
    return content
end function

function _getPreviewVideoContentNodeFor(items) as object
    ' print "getPreviewVideoContentNodeFor"
    content = CreateObject("roSGnode", "ContentNode")
    for each item in items
        webCQUrl = getValueForPath(item, "web_cq.url")
        hlsUrl = getValueForPath(item, "hls.url", webCQUrl)
        itemContent = content.createChild("ContentNode")
        itemContent.url = defaultValueIfInvalid(getValue(item, "srcMediaUrl"), defaultValueIfInvalid(webCQUrl, hlsUrl))
    end for
    return content
end function

sub _onVideoPlayerStateChanged(event as object)
    ' print "onVideoPlayerStateChanged"
    state = event.GetData()
    if state = PlaybackState().finished and m.top.isContinuousPlayEnabled then
        _showNext()
    end if
end sub

function _showNext() as boolean
    print "showNext", m.itemMediaType
    ' print m
    ' print m.top

    nextIndex = m.top.currentMediaIndex + 1
    print nextIndex
    if nextIndex < m.top.mediaPlaylistNode.getChildCount()
        m.top.currentMediaIndex = nextIndex
        return true
    end if
    return false
end function

function _showPrevious() as boolean
    ' print "showPrevious", m.itemMediaType
    if m.itemMediaType = "photo" then
        m.continuesPlaytimer.control = "stop"
    end if
    nextIndex = m.top.currentMediaIndex - 1
    if nextIndex >= 0
        m.top.currentMediaIndex = nextIndex
        return true
    end if
    return false
end function

function onKeyEvent(key as string, press as boolean) as boolean
    print "onKeyEvent FullscreenMediaPage", key, press
    result = false
    if not press then return result

    if key = RemoteKeys().right
        result = _showNext()
    else if key = RemoteKeys().Left
        result = _showPrevious()
    end if

    return result
end function

After sleuthing around more, I found another file that might be relevant to this issue. Funnily enough, this has a comment that is aware of the issue and has a “fix” commented out. However, when trying to implement the fix, it breaks far more than it fixes and the onKeyEvent trigger still won’t fire anyway.

sub init()
    _initSubviews()
    _initVars()
    _setObservers()
end sub

sub _initSubviews()
    m._clickTimer = m.top.findNode("clickTimer")
    m._longPressObserveTimer = m.top.findNode("longPressObserveTimer")
    m._forwardTimer = m.top.findNode("forwardTimer")
    m._rewindTimer = m.top.findNode("rewindTimer")
end sub

sub _initVars()
    m.top.notificationInterval = 1
    m._stateVariable = PlaybackState().buffering
    m._clickTimer.duration = 0.5
    m._rightClickCount = 0
    m._leftClickCount = 0
    m._secondsToShowNext = 5
end sub

sub _setObservers()
    m.top.observeField("state", "_onStateChanged")
    m._clickTimer.observeField("fire", "_onClickTimer")
end sub

sub _onStateChanged(event)
    m._stateVariable = event.getData()

end sub

sub _onClickTimer()
    m._rightClickCount = 0
    m._leftClickCount = 0
end sub
' issue is that when display first image after last video its not navigate to FullscreenMediaPage but it should
' because the right click key event and showNext() are in FullscreenMediaPage so to navigate it....
' i made this function to navigate to FullscreenMediaPage after click the double click when video is playing

' function showFullscreenMediaPage() as void
'     print "showFullscreenMediaPage", m.top
'     if m.top <> invalid then
'         ' Navigate directly to FullscreenMediaPage
'         fullscreenMediaPage = CreateObject("roSGNode", "FullscreenMediaPage")

'         if fullscreenMediaPage <> invalid then
'             m.top.appendChild(fullscreenMediaPage)
'             fullscreenMediaPage.setFocus(true)
'         else
'             print "Error: Failed to create FullscreenMediaPage scene."
'         end if
'     else
'         print "Error: m.top is invalid."
'     end if
' end function

sub _onRightButton()
    print "onRightButton"
    m._rightClickCount += 1
    if m._stateVariable <> PlaybackState().buffering
        if (m._rightClickCount = 1 and m.top.position >= (m.top.duration - m._secondsToShowNext)) or m._rightClickCount = 2
            m.top.control = "stop"
            m.top.showNext = true
            m._rightClickCount = 0
            m._stateVariable = PlaybackState().buffering
        ' end if
        else  ' call this showFullscreenMediaPage after a certon condition .. not in else
        '   showFullscreenMediaPage()
        end if
    end if

end sub

sub _onLeftButton()
    m._leftClickCount += 1
    if m._stateVariable <> PlaybackState().buffering
        if m._leftClickCount = 2
            m.top.showPrevious = true
            m._leftClickCount = 0
            m._stateVariable = PlaybackState().buffering
        end if
    end if
end sub

function onKeyEvent(key, press) as boolean
    print "onKeyEvent PlayerScreen"
    handled = false
    if press = true and (key = RemoteKeys().left or key = RemoteKeys().right)
        m._clickTimer.control = ControlAction().start
        if key = RemoteKeys().left
            _onLeftButton()
        else if key = RemoteKeys().right
            _onRightButton()
        end if
        handled = true
    end if
    return handled
end function

Focus can be a real pain (at least that’s been my experience.) Do you have an onKeyEvent function in your main scene’s code? It might help to know how the app is structured. In general, I use one scene with multiple screens and each screen as well as the main scene has its own onKeyEvent function.

It looks like there’s just one scene, the main one, and there is no onKeyEvent in the file. It sounds like this app is structured how you describe, just without that initial onKeyEvent. I’ve just added another file to the main post. Here’s the main.brs code if it helps at all:

' ********** Copyright 2016 Roku Corp.  All Rights Reserved. **********
sub Main(args as object)
    showChannelSGScreen(args)
end sub

sub showChannelSGScreen(args as object)
    screen = CreateObject("roSGScreen")
    m.port = CreateObject("roMessagePort")
    screen.setMessagePort(m.port)
    scene = screen.CreateScene("MainScene")
    m.global = screen.getGlobalNode()

    deeplink = getDeepLinks(args)
    if deeplink <> invalid
        m.global.addField("deeplink", "assocarray", false)
        m.global.deeplink = deeplink
    end if

    screen.show() ' vscode_rale_tracker_entry
    ' vscode_rdb_on_device_component_entry
    scene.callFunc("show", args)
    scene.observeField("exitApp", m.port)
    while(true)
        msg = wait(0, m.port)
        msgType = type(msg)
        if msgType = "roSGScreenEvent"
            if msg.isScreenClosed() then return
        else if msgType = "roSGNodeEvent" then
            field = msg.getField()
            if field = "exitApp" then
                return
            end if
        end if
    end while
end sub

function getDeepLinks(args) as object
    deeplink = invalid
    if args.contentid <> invalid and args.mediaType <> invalid
        deeplink = {
            contentID: args.contentId
            mediaType: args.mediaType
        }
    end if
    return deeplink
end function

@CDMem , did you get it figured out? This thread seemed to disappear for a few days and then came back.

If you haven’t figured it out, add an onKeyEvent function to the main scene’s file and print out what it gets and you may find the missing button presses. If they show up there, then you can try to figure out how to assign focus to the proper screen.

@renojim I have no idea what happened to the thread, but no I haven’t quite solved it yet.

I have definitely learned a lot throughout the week but I am also more confused about many things. I have added this function to my main.brs file but the print isn’t returning in my terminal at all:

function onKeyEvent(key as string, press as boolean) as boolean
    print "onKeyEvent main", key
    return true
end function

In general I’ve been having a hard time adding new onKeyEvent functions to the project as none of them seem to trigger for some reason. This function in near identical to other onKeyEvent functions in the same project so I don’t know why this shouldn’t be working.

That being said, the cleanUpSubviews function seems to be my main issue. All the data gets passed into the function how I would expect it to, but for some reason upon destroying the videoView, everything falls apart. This is especially frustrating because if I do some kooky stuff to kind of get around the issue, the videoView that hangs around seems to be preventing my future inputs from working correctly and I’m worried about creating a memory leak if I do keep it around. To be more specific, the videoView creates an instance of a PlayerScreen, so when that is active, I get logs for onKeyEvent presses from the PlayerScreen and the FullscreenMediaPage, which is the first file I shared in my original post. But once the PlayerScreen is destroyed, I don’t get onKeyEvents from the FullscreenMediaPage anymore, even though that’s how the fullscreen images were always being handled.

Suffice to say, I am definitely still stumped. I feel like I’m on the edge of getting this figured out but there’s an invisible wall blocking my progress to the end. If there’s any help you could provide, I would greatly appreciate it and if there’s anything else I can provide to you, please let me know.

EDIT: One last thing I want to add is that in my onKeyEvents, I tend to get both true and false values of the press variable, as would be expected. After I go from a video to an image, the press seems to always send back false, no matter how long I press the button. I cut a bunch of code so I can technically get it working and the main issue I run into by cutting out that code, is that my onKeyEvents stop getting a true value for press and multiple onKeyEvents seem to trigger from a single press so it skips over media, which of course is not acceptable.

You have to add the onKeyEvent function to the main scene’s source. It should be in the components directory and I would think should have at least an init function. I feel like tracking down where the missing button presses are going may shed some light on what’s happening.

I should state that I wouldn’t call myself an expert in scene graph, so take what I say with that in mind. The way I do things may not be the best/correct way of doing things. One of the strengths of SG is that there’s multiple ways of doing things, but that also makes it harder to debug (especially when looking at someone else’s code).

As I said before, I have one main scene and I usually have its code in home_scene.brs and home_scene.xml in the components directory. The XML file has all of the various screens as children. I never destroy anything, but use the visibility property of the various screens (obviously, only one is visible at a time). Getting the focus to the right component has always been a pain.

This project does things differently (not that it’s right or wrong), so it’s harder for me to follow and SG is already hard enough to follow. :slight_smile: My interest in the main scene’s onKeyEvent function is partially just so I know if my admittedly limited knowledge of SG is correct. I feel like the focus is being passed to the main scene, but why that is I couldn’t say just from glancing at the code.

Looks like I have a main.brs, BaseScreen.brs, and MainScene.brs. And once I get the issue to trigger, I don’t get onKeyEvent calls from any of them! Very very peculiar… I do get the MainScene.brs onKeyEvents triggering when I use inputs before the problem occurs, so I think that is the file you wanted me to find and try out. But it looks like the focus is leaving the entire scene somehow… I have no clue that to make of that. For what it’s worth. the XML for these files don’t seem to indicate their connection to any other files, unlike what you were describing. Here’s my MainScene.xml as an example:

<component name="MainScene" extends="Scene">

    <interface>
        <field id="exitApp" type="boolean" value="false" />

        <function name="show" />

        <function name="pop" />
        <function name="popToRoot" />
        <function name="popToPreviousFrom" />
        <function name="showScreen" />
        <function name="removeScreen" />
        <function name="replaceAllStackWith" />
        <function name="getScreensCount" />

        <function name="hideActivityIndicator" />
        <function name="showActivityIndicator" />
    </interface>

    <children>
        <NavigationStack id="navigationStack" />
    </children>

    <script type="text/brightscript" uri="MainScene.brs" />
    <script type="text/brightscript" uri="DeepLinkingLogic.brs" />
    <script type="text/brightscript" uri="../../Classes/Utils/Enums/API.brs" />
    <script type="text/brightscript" uri="../../Classes/Utils/util.brs" />
    <script type="text/brightscript" uri="../../Classes/Utils/valueUtil.brs" />
    <script type="text/brightscript" uri="../../Classes/Utils/Enums/ControlEnums.brs" />
    <script type="text/brightscript" uri="../../Classes/Utils/Enums/SubscriptionEnums.brs" />
    <script type="text/brightscript" uri="../../Classes/Utils/appBeaconUtil.brs" />
</component>

You mention that you don’t destroy anything. I wonder if just cutting out that cleanup function and working around whatever gaps it leaves would just be a fine enough solution. It’s not cleaning up after each video or anything so I’m wondering if it’s worth destroying at all, at least not until the user navigates away from the FullscreenPage to begin with. That will probably be an approach I take since I know if I remove the cleanup function, the inputs do keep coming in, there’s just other bugs that arise in its place.

As an aside, you mention that you’re not an expert in SceneGraph. I am very new to Roku development, as my first message likely said. Officially, I’ve only been working in it for about two weeks; this is my first bug to fix! What other ways can you work in it?

Yep, that’s the file. So much for that theory. I thought the main scene would always receive the button presses if no other component claimed them. I’m not sure where you should go from here. You might try just using the visible property to go from page to page (or screen to screen), but I don’t know how much work that would involve.

SG is Roku’s second version of an SDK. v1.0 was much simpler and much easier to learn, but also much less capable than SG. There’s also roScreen that is mostly used for games and graphics intensive stuff. I have a few of those in the Channel Store. I’m not fond of SG (I never liked the event driven model), but I have to admit that it’s very powerful - you can do pretty much anything. However, the learning curve is pretty steep. I’ve only learned enough to get by.

What is the visible property? Is it just .visible? I swear I tried using that before but the IDE won’t list it as a viable property and searching for it online has been shockingly annoying.

That being said, I did come up with a solution! I stopped deleting the video player, which allowed for events to keep coming in but things wouldn’t move. What I do now is I destroy the image view instead and recreate it whenever I need. That allows it to gain priority and display itself over the video player and let is sit in the background instead of the other way around. Seems to be working great for me right now! The mystery of why deleting the video player killed all input will have to remain unsolved for now.

Thank you for the breakdown on the SDKs!! I really appreciate it; you may have been the most helpful person I’ve ever talked to online about anything software engineering related LOL I’ll definitely be looking more into this stuff now that I know what to research.

I found this for visible:

https://developer.roku.com/docs/developer-program/core-concepts/scenegraph-xml/other-inherited-properties.md

It’s a property of most (if not all) objects that you just set to true or false. My code would have something like this:

m.video_screen.visible = false

I find it helpful to print out objects in the console to see what properties they have. Since they inherit from other objects, there’s always something that gets forgotten since it doesn’t show up in the documentation for the object if it’s inherited from something else.

print m.video_screen

I’m glad you found something that works! Good luck with your project. It’s always tough taking over something from someone else.