Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
dannykng committed Mar 29, 2017
1 parent 27bb171 commit 05071d3
Show file tree
Hide file tree
Showing 14 changed files with 389 additions and 2 deletions.
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,23 @@
# RAF4RSG-sample
sample demonstrating the Roku Advertising Framework in SceneGraph
# RAF4RSG Sample

This sample code demonstrates the use of Roku Advertising Framework in a Roku SceneGraph app. With v2 of RAF, the preferred way of use is by invoking the library from a Task node and passing a 3rd argument (`view`) to .ShowAds, which specifies where exactly in scene graph the ad content (video and interactive ad UI) should be attached.

## How does it work

main() creates an EntryScene (extends Scene), which only has a simplistic menu (LabelList). Selecting from there configures a VideoContent node with the video and ad information and starts playing it via a Player child node.

Player (extends Group) in turn creates a Video node child, sets the main content in it and delegates running the video and ad play to a PlayerTask.

PlayerTask (extends Task) in this sample is the only one "tasked" (pun intended) with playing the main content and the corresponding ads. It is also the only one that has to include the RAF library (`Library "Roku_Ads.brs"`). The runner function in PlayerTask - playContentWithAds() - makes the necessary library calls, the most notable being `.getAds()` to fetch ad pods and `.showAds()` to display them. Note that in RAF2 .showAds() has now a new, 3rd parameter - `view`, that is used to provide a renderable node in Roku Scene Graph, under which RAF should temporary attach its UI (incl. an ad video player node).

PlayContentWithAds() has an event loop, in which it keeps track of `position` events during playback of the main content, so that it could interrupt and insert mid-roll ads as appropriate (determined by repeat calls to .getAds()) - and when that is done, it resumes the main video. The event loop also handles other player state changes besides "position", for example it latches onto the video "finished" event for a chance to display post-roll ads.

## Pre-roll, mid-roll and post-roll ads

The code can handle pre-roll, mid-roll and post-roll ads on the same video content - subject to the ad server returning the pods configured that way during the .getAds() call.

Note that if invoked without a custom URL (as the sample does), only a single pre-roll ad will be shown, because that is the Roku ad server's default configuration. To change that behavior, .setAdUrl() pointing to customer's ad server has to be used (this falls under the "inventory split" model, see "Revenue Share" section in the documentation).

## Caveats

Notice this wrinkle - of PlayerTask having to manually set back the input focus after playing ads, since RAF UI has had the focus to be able to handle remote button presses.
78 changes: 78 additions & 0 deletions components/EntryScene.brs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'*********************************************************************
'** (c) 2016-2017 Roku, Inc. All content herein is protected by U.S.
'** copyright and other applicable intellectual property laws and may
'** not be copied without the express permission of Roku, Inc., which
'** reserves all rights. Reuse of any of this content for any purpose
'** without the permission of Roku, Inc. is strictly prohibited.
'*********************************************************************

sub init()
'we use a simple LabelList for a menu
m.list = m.top.FindNode("list")
m.list.observeField("itemSelected", "onItemSelected")
m.list.SetFocus(true)

'descriptor for the menu items
itemList = [
{
title: "Roku ad server (default server URL, single pre-roll)"
url: "" 'point to your own ad server if doing "inventory split" revenue share
}
]

' compile into a ContentNode structure
listNode = CreateObject("roSGNode", "ContentNode")
for each item in itemList:
nod = CreateObject("roSGNode", "ContentNode")
nod.setFields(item)
listNode.appendChild(nod)
next
m.list.content = listNode

end sub

sub onItemSelected()
m.list.SetFocus(false) ' un-set focus to avoid creating multiple players on user tapping twice
menuItem = m.list.content.getChild(m.list.itemSelected)

videoContent = {

streamFormat: "hls",
titleSeason: "Art21 Season 3",
title: "Place",
url: "http://ga.video.cdn.pbs.org/videos/art-21/fe42d468-e90f-489b-ade3-a242f196dba2/60600/hd-mezzanine-16x9/ARTF101_iPad-1240k.m3u8",

'used for raf.setContentGenre(). For ads provided by the Roku ad service, see docs on 'Roku Genre Tags'
categories: ["Documentary"]

'Roku mandates that all channels enable Nielsen DAR
nielsen_app_id: "P2871BBFF-1A28-44AA-AF68-C7DE4B148C32" 'required, put "P2871BBFF-1A28-44AA-AF68-C7DE4B148C32", Roku's default appId if not having ID from Nielsen
nielsen_genre: "DO" 'required, put "GV" if dynamic genre or special circumstances (e.g. games)
nielsen_program_id: "Art21" 'movie title or series name
length: 3220 'in seconds;

}
' compile into a VideoContent node
content = CreateObject("roSGNode", "VideoContent")
content.setFields(videoContent)
content.ad_url = menuItem.url

if m.Player = invalid:
m.Player = m.top.CreateChild("Player")
m.Player.observeField("state", "PlayerStateChanged")
end if

'start the player
m.Player.content = content
m.Player.visible = true
m.Player.control = "play"
end sub

sub PlayerStateChanged()
print "EntryScene: PlayerStateChanged(), state = "; m.Player.state
if m.Player.state = "done" or m.Player.state = "stop"
m.Player.visible = false
m.list.setFocus(true) 'NB. the player took the focus away, so get it back
end if
end sub

11 changes: 11 additions & 0 deletions components/EntryScene.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>

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

<script type="text/brightscript" uri="pkg:/components/EntryScene.brs" />

<children>
<LabelList id="list" itemSize="[800, 50]" translation="[100,50]" />
</children>

</component>
68 changes: 68 additions & 0 deletions components/Player.brs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'*********************************************************************
'** (c) 2016-2017 Roku, Inc. All content herein is protected by U.S.
'** copyright and other applicable intellectual property laws and may
'** not be copied without the express permission of Roku, Inc., which
'** reserves all rights. Reuse of any of this content for any purpose
'** without the permission of Roku, Inc. is strictly prohibited.
'*********************************************************************

' Player

sub init()
m.video = m.top.CreateChild("Video")
end sub

sub controlChanged()
'handle orders by the parent/owner
control = m.top.control
if control = "play" then
playContent()
else if control = "stop" then
exitPlayer()
end if
end sub

sub playContent()
content = m.top.content
if content <> invalid then
m.video.content = content
m.video.visible = false

m.PlayerTask = CreateObject("roSGNode", "PlayerTask")
m.PlayerTask.observeField("state", "taskStateChanged")
m.PlayerTask.video = m.video
m.PlayerTask.control = "RUN"
end if
end sub

sub exitPlayer()
print "Player: exitPlayer()"
m.video.control = "stop"
m.video.visible = false
m.PlayerTask = invalid

'signal upwards that we are done
m.top.state = "done"
end sub

sub taskStateChanged(event as Object)
print "Player: taskStateChanged(), id = "; event.getNode(); ", "; event.getField(); " = "; event.getData()
state = event.GetData()
if state = "done" or state = "stop"
exitPlayer()
end if
end sub

function onKeyEvent(key as String, press as Boolean) as Boolean
' since m.video is a child of `player` in the scene graph,
' pressing the Back button during play will "bubble up" for us to handle here
print "Player: keyevent = "; key

if press and key = "back" then
'handle Back button, by exiting play
exitPlayer()
return true
end if
return false
end function

17 changes: 17 additions & 0 deletions components/Player.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>

<!-- RAF player for playing ad supported content. -->
<component name="Player" extends="Group">
<interface>
<!-- video content descriptor, ContentNode-based; includes ad_url if non-default -->
<field id="content" type="node" />

<!-- state - Same as Task states. -->
<field id="state" type="string" alwaysNotify="true"/>

<!-- control - "play" starts content playback -->
<field id="control" type="string" alwaysNotify="true" onChange="controlChanged"/>
</interface>

<script type="text/brightscript" uri="pkg:/components/Player.brs" />
</component>
134 changes: 134 additions & 0 deletions components/PlayerTask.brs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
'*********************************************************************
'** (c) 2016-2017 Roku, Inc. All content herein is protected by U.S.
'** copyright and other applicable intellectual property laws and may
'** not be copied without the express permission of Roku, Inc., which
'** reserves all rights. Reuse of any of this content for any purpose
'** without the permission of Roku, Inc. is strictly prohibited.
'*********************************************************************

Library "Roku_Ads.brs"

sub init()
m.top.functionName = "playContentWithAds"
m.top.id = "PlayerTask"
end sub

sub playContentWithAds()

video = m.top.video
' `view` is the node under which RAF should display its UI (passed as 3rd argument of showAds())
view = video.getParent()

RAF = Roku_Ads()
'RAF.clearAdBufferScreenLayers() ' in case it was set earlier
'RAF.enableAdBufferMessaging(true, true) ' could have been cleared by custom screen
'RAF.setAdBufferScreenContent({})

content = video.content
RAF.setAdUrl(content.ad_url)
' for generic measurements api
RAF.setContentGenre(content.categories) 'if unset, ContentNode has it as []
' Nielsen DAR specific measurements
if content.nielsen_app_id <> invalid:
RAF.enableNielsenDAR(true)
RAF.setNielsenAppId(content.nielsen_app_id)
RAF.setNielsenGenre(content.nielsen_genre)
RAF.setNielsenProgramId(content.nielsen_program_id)
RAF.setContentLength(content.length)
end if

' log tracking events
' logObj = {
' log : Function(evtType = invalid as Dynamic, ctx = invalid as Dynamic)
' if GetInterface(evtType, "ifString") <> invalid
' print "*** tracking event " + evtType + " fired."
' if ctx.companion = true then
' print "***** companion = true"
' end if
' if ctx.errMsg <> invalid then print "***** Error message: " + ctx.errMsg
' if ctx.adIndex <> invalid then print "***** Ad Index: " + ctx.adIndex.ToStr()
' if ctx.ad <> invalid and ctx.ad.adTitle <> invalid then print "***** Ad Title: " + ctx.ad.adTitle
' else if ctx <> invalid and ctx.time <> invalid
' print "*** checking tracking events for ad progress: " + ctx.time.ToStr()
' end if
' End Function
' }
' logFunc = Function(obj = Invalid as Dynamic, evtType = invalid as Dynamic, ctx = invalid as Dynamic)
' obj.log(evtType, ctx)
' End Function
' RAF.setTrackingCallback(logFunc, logObj)

adPods = RAF.getAds() 'array of ad pods
keepPlaying = true 'gets set to `false` when showAds() was exited via Back button

' show the pre-roll ads, if any
if adPods <> invalid and adPods.count() > 0
keepPlaying = RAF.showAds(adPods, invalid, view)
end if

port = CreateObject("roMessagePort")
if keepPlaying then
video.observeField("position", port)
video.observeField("state", port)
video.visible = true
video.control = "play"
video.setFocus(true) 'so we can handle a Back key interruption
end if

curPos = 0
adPods = invalid
isPlayingPostroll = false
while keepPlaying
msg = wait(0, port)
if type(msg) = "roSGNodeEvent"
if msg.GetField() = "position" then
' keep track of where we reached in content
curPos = msg.GetData()
' check for mid-roll ads
adPods = RAF.getAds(msg)
if adPods <> invalid and adPods.count() > 0
print "PlayerTask: mid-roll ads, stopping video"
'ask the video to stop - the rest is handled in the state=stopped event below
video.control = "stop"
end if
else if msg.GetField() = "state" then
curState = msg.GetData()
print "PlayerTask: state = "; curState
if curState = "stopped" then
if adPods = invalid or adPods.count() = 0 then
exit while
end if

print "PlayerTask: playing midroll/postroll ads"
keepPlaying = RAF.showAds(adPods, invalid, view)
adPods = invalid
if isPlayingPostroll then
exit while
end if
if keepPlaying then
print "PlayerTask: mid-roll finished, seek to "; stri(curPos)
video.visible = true
video.seek = curPos
video.control = "play"
video.setFocus(true) 'important: take the focus back (RAF took it above)
end if

else if curState = "finished" then
print "PlayerTask: main content finished"
' render post-roll ads
adPods = RAF.getAds(msg)
if adPods = invalid or adPods.count() = 0 then
exit while
end if
print "PlayerTask: has postroll ads"
isPlayingPostroll = true
' stop the video, the post-roll would show when the state changes to "stopped" (above)
video.control = "stop"
end if
end if
end if
end while

print "PlayerTask: exiting playContentWithAds()"
end sub

12 changes: 12 additions & 0 deletions components/PlayerTask.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?rokuml version="1.0" encoding="utf-8" ?>

<!-- a Task handling the playback loop of both content abd ad -->
<component name="PlayerTask" extends="Task">

<script type="text/brightscript" uri="pkg:/components/PlayerTask.brs" />

<interface>
<field id="video" type="node" />
</interface>

</component>
11 changes: 11 additions & 0 deletions components/VideoContent.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<component name = "VideoContent" extends = "ContentNode" >

<!-- expanding ContentNode with additional fields, so can set them all with a setFields() -->
<interface>
<field id = "ad_url" type = "string" />
<field id = "nielsen_app_id" type = "string" />
<field id = "nielsen_genre" type = "string" />
<field id = "nielsen_program_id" type = "string" />
</interface>

</component>
Binary file added images/MainMenu_Icon_Center_HD.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/MainMenu_Icon_Center_SD43.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/splash_hd.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/splash_sd.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions manifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
title=Sample use of RAF in RSG
major_version=1
minor_version=1
build_version=1
bs_libs_required=roku_ads_lib
ui_resolutions=hd
mm_icon_focus_hd=pkg:/images/MainMenu_Icon_Center_HD.png
mm_icon_focus_sd=pkg:/images/MainMenu_Icon_Center_SD43.png
splash_screen_hd=pkg:/images/splash_hd.jpg
splash_screen_sd=pkg:/images/splash_sd.jpg
splash_color=#404040
Loading

0 comments on commit 05071d3

Please sign in to comment.