시작하기

Roku용 프로그래매틱 액세스 라이브러리(PAL) SDK를 사용하면 직접 VAST 호출(DVC) 승인을 받은 게시자가 DVC 기반 Roku 애플리케이션으로 수익을 창출할 수 있습니다. PAL SDK를 사용하면 암호화된 문자열인 nonce를 Google에 요청하여 DVC 요청에 서명할 수 있습니다. 각 새 스트림 요청에는 새로 생성된 nonce가 포함되어야 합니다. 그러나 동일한 스트림 내의 여러 광고 요청에 동일한 nonce를 재사용할 수 있습니다.

이 가이드에서는 PAL SDK를 Roku 애플리케이션에 통합하고, nonce를 요청하고, 광고 노출을 등록하는 방법의 예를 설명합니다.

기본 요건

이 가이드를 시작하기 전에 다음을 실행해야 합니다.

  • Roku 개발 환경: 자세한 내용은 Roku 개발자 환경 설정 가이드를 참고하세요.
  • 다음과 같은 구조의 프로젝트 폴더

    ./
      components/
        MainScene.xml
        PALInterface.xml
        SampleVideoPlayer.xml
      images/
        icon_focus_hd.png
        icon_focus_sd.png
        icon_side_hd.png
        icon_side_sd.png
        splash_fhd.png
        splash_hd.png
        splash_sd.png
      source/
        main.brs
      manifest
    

프로젝트 설정

PAL SDK를 통합하기 전에 프로젝트 파일을 구성해야 합니다.

매니페스트

title=PAL for Roku Sample
subtitle=As seen in the PAL for Roku Get Started Guide
major_version=1
minor_version=0
build_version=00001

mm_icon_focus_hd=pkg:/images/icon_focus_hd.png
mm_icon_side_hd=pkg:/images/icon_side_hd.png
mm_icon_focus_sd=pkg:/images/icon_focus_sd.png
mm_icon_side_sd=pkg:/images/icon_side_sd.png

splash_screen_sd=pkg:/images/splash_sd.jpg
splash_screen_hd=pkg:/images/splash_hd.jpg
splash_screen_fhd=pkg:/images/splash_fhd.jpg
splash_color=#000000
splash_min_time=1000
ui_resolutions=hd

source/main.brs

sub Main()
    showChannelSGScreen()
end sub

sub showChannelSGScreen()
  screen = CreateObject("roSGScreen")
  m.port = CreateObject("roMessagePort")

  screen.setMessagePort(m.port)
  m.scene = screen.CreateScene("MainScene")
  screen.show()

  while(true)
    msg = wait(0, m.port)
    msgType = type(msg)
    if msgType = "roSGScreenEvent"
      if msg.isScreenClosed() then return
    end if
  end while
end sub

샘플 동영상 플레이어 만들기

SampleVideoPlayer 구성요소는 단순히 동영상 구성요소를 래핑하여 리모컨 누르기를 캡처합니다. 리모컨의 포커스가 동영상/광고 플레이어로 전송되면 추가 키 누르기 (위, 아래, 왼쪽, 오른쪽, 클릭 등)가 캡처되어 PAL로 버블링되도록 onKeyEvent를 재정의합니다.

components/SampleVideoPlayer.xml

<?xml version="1.0" encoding="utf-8" ?>

<component name="SampleVideoPlayer" extends="Video">
  <interface>
    <field id="pressedKey" type="String" />
  </interface>
  <script type="text/brightscript">
    <![CDATA[

      Function onKeyEvent(key as String, press as Boolean) as Boolean
        If press
          m.top.pressedKey = key
        End If
        return True
      End Function

    ]]>
  </script>

  <children>
    <Label text="VIDEO" color="0xFFFFFFFF" font="font:MediumBoldSystemFont" horizAlign="center" vertAlign="center" width="720" height="480" />
  </children>

</component>

테스트 인터페이스 만들기

다음을 실행하는 버튼이 있는 장면을 구현합니다.

  • nonce를 요청합니다.
  • 광고 클릭을 전송합니다.
  • 재생 시작 이벤트를 전송합니다.
  • 재생 종료 이벤트를 전송합니다.
  • 동영상 버튼으로 포커스를 전송합니다.

components/MainScene.xml

<?xml version="1.0" encoding="utf-8" ?>
<component name="MainScene" extends="Scene" initialFocus="requestNonceButton">
<children>
  <ButtonGroup>
    <button text="Request Nonce" id="requestNonceButton" />
    <button text="Send Ad Click" id="sendAdClickButton" />
    <button text="Send Playback Start" id="sendPlaybackStartButton" />
    <button text="Send Playback End" id="sendPlaybackEndButton" />
    <button text="Transfer Focus to Video" id="transferFocusToVideoButton" />
  </ButtonGroup>
  <SampleVideoPlayer id="YourVideoPlayer" width="720" height="480" focusable="true" />
</children>
</component>

SDK 인터페이스 구성요소 만들기

기본 장면과 PAL SDK 간에 통신하려면 비동기 코드가 포함된 구성요소가 필요합니다. PAL SDK는 Roku 애플리케이션의 메인 스레드에서 발생할 수 없는 외부 네트워크 요청을 실행하므로 이 작업이 필요합니다. 이 구성요소에 데이터를 전송하려면 구성요소가 전송하고 수신하는 데이터를 정의하는 인터페이스가 필요합니다.

components/PALInterface.xml

<?xml version="1.0" encoding="utf-8" ?>
<component name="PALInterface" extends="Task">
<interface>
  <!--Commands-->
  <field id="requestNonce" type="Boolean" />
  <field id="sendAdClick" type="Boolean" />
  <field id="sendAdTouchKey" type="String" />
  <field id="sendPlaybackStart" type="Boolean" />
  <field id="sendPlaybackEnd" type="Boolean" />
  <field id="endThread" type="Boolean" />
  <!--Responses-->
  <field id="errors" type="stringarray" />
  <field id="nonce" type="String" />
</interface>
</component>

IMA SDK 가져오기

PAL 라이브러리를 사용하려면 앱 매니페스트에서 Roku용 IMA SDK를 요구하고 PALInterface 구성요소로 가져와야 합니다.

매니페스트

...
splash_color=#000000
splash_min_time=1000
ui_resolutions=hd
bs_libs_required=googleima3

components/PALInterface.xml

<?xml version = "1.0" encoding = "utf-8" ?>

<component name="PALInterface" extends="Task">
<interface>
  <!-- commands -->
  <field id="requestNonce" type="Boolean" />
  <field id="sendAdClick" type="Boolean" />
  <field id="sendAdTouchKey" type="String" />
  <field id="sendPlaybackStart" type="Boolean" />
  <field id="sendPlaybackEnd" type="Boolean" />
  <field id="endThread" type="Boolean" />
  <!-- responses -->
  <field id="errors" type="stringarray" />
  <field id="nonce" type="String" />
</interface>
<script type = "text/brightscript">
<![CDATA[
  Library "IMA3.brs"
]]>
</script>
</component>

장면에서 인터페이스 구성요소 트리거

다음으로 사용자 상호작용을 리슨하고 인터페이스 구성요소의 변경사항을 트리거하는 BrightScript 코드를 추가합니다.

  • 인터페이스 구성요소에서 출력을 수신하려면 이러한 출력과 연결된 인터페이스 필드에 필드 관찰자를 구현하고 기본 구성요소의 콜백 함수에 연결합니다. 구성요소가 처음 등록될 때 실행합니다.

  • 사용자 상호작용을 인터페이스 구성요소로 전송하려면 이전에 만든 버튼에 필드 관찰자를 구현하여 이러한 명령어와 연결된 인터페이스 필드의 변경사항을 트리거합니다.

components/MainScene.xml

<?xml version="1.0" encoding="utf-8" ?>
<component name="MainScene" extends="Scene" initialFocus="requestNonceButton">
<children>
  <ButtonGroup>
    <button text="Request Nonce" id="requestNonceButton" />
    <button text="Send Ad Click" id="sendAdClickButton" />
    <button text="Send Ad Touch" id="sendAdTouchButton" />
    <button text="Send Playback Start" id="sendPlaybackStartButton" />
    <button text="Send Playback End" id="sendPlaybackEndButton" />
  </ButtonGroup>
  <Video id="YourVideoPlayer" width="720" height="480" focusable="true" />
</children>
<script type="text/brightscript">
<![CDATA[
  Function init()
    requestNonceButton = m.top.findNode("requestNonceButton")
    requestNonceButton.observeField("buttonSelected", "requestNonce")

    sendAdClickButton = m.top.findNode("sendAdClickButton")
    sendAdClickButton.observeField("buttonSelected", "sendAdClick")
    sendPlaybackStart = m.top.findNode("sendPlaybackStartButton")
    sendPlaybackStart.observeField("buttonSelected", "sendPlaybackStart")
    sendPlaybackEnd = m.top.findNode("sendPlaybackEndButton")
    sendPlaybackEnd.observeField("buttonSelected", "sendPlaybackEnd")

    loadImaSdk()
  End Function

  ' Initialize SDK Interface component and attach callbacks to field observers.
  Function loadImaSdk() as Void
    m.sdkTask = createObject("roSGNode", "PALInterface")
    m.sdkTask.observeField("errors", "onSdkLoadedError")
    m.sdkTask.observeField("nonce", "onNonceLoaded")
    print "Running load IMA task."
    m.sdkTask.control = "RUN"
  End Function

  Sub onSdkLoadedError(message as Object)
    print "----- errors in the sdk loading process --- ";message.getData()
  End Sub

  ' Callback triggered when Nonce is loaded.
  Sub onNonceLoaded(message as Object)
    nonce = m.sdkTask.nonce
    print "onNonceLoaded ";nonce
  End Sub

  Function requestNonceButtonPressed() As Void
    print "Request Nonce"
    ' Inform the SDK interface component to request a nonce.
    m.sdkTask.requestNonce = True
  End Function

  ' Action triggered on player start, either from user action or autoplay.
  Function sendPlaybackStart() As Void
    m.sdkTask.sendPlaybackStart = True
  End Function

  ' Action triggered on player end, either when content ends or the user exits
  ' playback of this content.
  Function sendPlaybackEnd() As Void
    m.sdkTask.sendPlaybackEnd = True
  End Function
]]>
</script>
</component>

포커스를 전송하는 메서드 추가

그런 다음 사용자 키 누르기를 캡처하여 동영상 요소로 포커스를 전송하고 반대로 전송합니다.

components/MainScene.xml

...

<script type="text/brightscript">
<![CDATA[
  Function init()

    ...

    m.sendPlaybackStart = m.top.findNode("sendPlaybackStartButton")
    m.sendPlaybackStart.observeField("buttonSelected", "sendPlaybackStart")
    m.sendPlaybackEnd = m.top.findNode("sendPlaybackEndButton")
    m.sendPlaybackEnd.observeField("buttonSelected", "sendPlaybackEnd")

    m.transferFocusToVideoButton = m.top.findNode("transferFocusToVideoButton")
    m.transferFocusToVideoButton.observeField("buttonSelected", "transferFocusToVideo")

    ' Your video player set up to handle key press events.
    m.video = m.top.findNode("YourVideoPlayer")
    m.video.observeField("pressedKey", "onVideoKeyPress")

    loadImaSdk()
  End Function

  ...

  ' Action triggered on player end, either when content ends or the user exits
  ' playback of this content.
  Function sendPlaybackEnd() As Void
    m.sdkTask.sendPlaybackEnd = True
  End Function

  Function transferFocusToVideo() As Void
    m.video.setFocus(true)
  End Function

  Function onVideoKeyPress() As Void
    key = m.video.pressedKey
    If key = ""
      Return
    End If
    m.sdkTask.sendAdTouchKey = key

    ' If back or up is pressed, transfer focus back up to the buttons.
    If key = "back" or key = "up"
      m.transferFocusToVideoButton.setFocus(true)
    End If

    ' Reset so that we get the next key press, even if it's a repeat of the last
    ' key.
    m.video.pressedKey = ""
  End Function
]]>
</script>
</component>

PAL SDK 초기화 및 nonceLoader 만들기

이제 PAL SDK 구현의 핵심 로직을 빌드할 수 있습니다. 먼저 별도의 스레드에서 SDK를 초기화합니다.

components/PALInterface.xml

...
<script type = "text/brightscript">
<![CDATA[
  Library "IMA3.brs"

  Sub init()
    ' It is not possible to access roUrlTransfer on the main thread. Setting
    ' functionName to a function and then setting control to "RUN" causes that
    'function to run on a separate thread.
    m.top.functionName = "runPalThread"

    ' Loads the SDK on the current thread if it is not yet loaded.
    ' This blocks execution of other functions on this thread until the SDK is loaded.
    If m.sdk = Invalid
      m.sdk = new_imaSdk()
    End If

    m.nonceLoader = m.sdk.CreateNonceLoader()
  End Sub

  ' Starts the player event loop. This loop only terminates when "endThread" is sent.
  Function runPalThread() as Void
    ' Used for the player life cycle loop.
    m.top.endThread = False
    port = CreateObject("roMessagePort")

  End Function
]]>
</script>
</component>

nonce 요청 처리

nonceLoader가 생성되면 관찰자를 requestNonce 필드에 연결하여 nonce 요청을 처리해야 합니다. nonceLoader가 초기화된 후에만 이 관찰자를 연결하면 nonce 요청이 SDK 스레드에서 처리되고 유효한 nonceLoader가 있는 경우에만 nonce 요청을 할 수 있습니다.

components/PALInterface.xml

...

  ' Starts the player event loop. This loop only terminates when "endThread" is sent.
  Function runPalThread() as Void
    ' Used for the player life cycle loop.
    m.top.endThread = False
    port = CreateObject("roMessagePort")

    ' Now that the nonceLoader exists, begin listening for nonce requests.
    m.top.observeField("requestNonce", m.port)

  End Function

  ' Requests a nonce from the PAL SDK.
  Function requestNonce() as Void
    nonceRequest = m.sdk.CreateNonceRequest()
    m.nonceManager = m.nonceLoader.loadNonceManager(nonceRequest)
    m.top.nonce = nonceManager.getNonce()
  End Function
]]>
</script>
</component>

NonceRequest.storageAllowed의 기본값은 true이지만 적절한 동의를 수집한 후 이 값을 변경할 수 있습니다. getConsentToStorage() 메서드는 CMP와 통합하거나 저장소 동의를 처리하는 다른 메서드를 기반으로 사용자 동의를 얻는 자체 메서드의 자리표시자입니다.

components/PALInterface.xml

...
<script type = "text/brightscript">
<![CDATA[
  Library "IMA3.brs"

  Sub init()
    ' It is not possible to access roUrlTransfer on the main thread. Setting
    ' functionName to a function and then setting control to "RUN" causes that
    'function to run on a separate thread.
    m.top.functionName = "runPalThread"

    ' Loads the SDK on the current thread if it is not yet loaded.
    ' This blocks execution of other functions on this thread until the SDK is loaded.
    If m.sdk = Invalid
      m.sdk = new_imaSdk()
    End If

    m.isConsentToStorage = getConsentToStorage()

    m.nonceLoader = m.sdk.CreateNonceLoader()
  End Sub

  ...

  ' Requests a nonce from the PAL SDK.
  Function requestNonce() as Void
    nonceRequest = m.sdk.CreateNonceRequest()

    ' Include changes to storage consent here.
    nonceRequest.storageAllowed = m.isConsentToStorage

    m.nonceManager = m.nonceLoader.loadNonceManager(nonceRequest)
    m.top.nonce = nonceManager.getNonce()
  End Function

플레이어 수명 주기 신호 리슨

PAL 통합이 신호를 올바르게 전송하도록 하려면 플레이어의 수명 주기 신호를 리슨하는 루프를 설정해야 합니다.

components/PALInterface.xml

...

    ' Now that the nonceLoader exists, begin listening for nonce requests.
    m.top.observeField("requestNonce", m.port)

    m.top.observeField("sendAdClick", m.port)
    m.top.observeField("sendAdTouchKey", m.port)
    m.top.observeField("sendPlaybackStart", m.port)
    m.top.observeField("sendPlaybackEnd", m.port)

    ' Setting endThread to true causes the while loop to exit.
    m.top.observeField("endThread", m.port)

    While Not m.top.endThread
      message = m.port.waitMessage(1000)
      If message = Invalid
        pollManager()
      Else If message.getField() = "requestNonce" And m.top.requestNonce = True
        requestNonce()
        m.top.requestNonce = False
      Else If message.getField() = "sendAdClick" And m.top.sendAdClick = True
        sendAdClick()
        m.top.sendAdClick = False
      Else If message.getField() = "sendAdTouchKey" And m.top.sendAdTouchKey <> ""
        sendAdTouch(m.top.sendAdTouchKey)
        m.top.sendAdTouchKey = ""
      Else If message.getField() = "sendPlaybackStart" And m.top.sendPlaybackStart = True
        sendPlaybackStart()
        m.top.sendPlaybackStart = False
      Else If message.getField() = "sendPlaybackEnd" And m.top.sendPlaybackEnd = True
        sendPlaybackEnd()
        m.top.sendPlaybackEnd = False
      End If
    End While
  End Function

  Function pollManager() as Void
    If m.nonceManager <> Invalid
      m.nonceManager.poll()
    End If
  End Function

  ' Requests a nonce from the PAL SDK.
  Function requestNonce() as Void
    nonceRequest = m.sdk.CreateNonceRequest()
    m.nonceManager = m.nonceLoader.loadNonceManager(nonceRequest)
    m.top.nonce = nonceManager.getNonce()
  End Function
]]>
</script>
</component>

sendPlaybackStart, sendPlaybackEnd, sendAdClick, sendAdTouch의 리스너 등록

그런 다음 '동영상 플레이어 시작'에서 sendPlaybackStart를 호출합니다. 이 메서드는 Google 서버에 대한 비동기 호출을 시작하여 IVT 모니터링 및 감지에 필요한 신호를 수집합니다. 재생이 끝나면 sendPlaybackEnd를 호출합니다. 광고 클릭 후 sendAdClick를 호출합니다. 그런 다음 클릭연결이 아닌 사용자 터치 또는 클릭 이벤트에 대해 sendAdTouch를 호출합니다.

components/PALInterface.xml

...

  ' Requests a nonce from the IMA SDK.
  Function requestNonce() as Void
    nonceRequest = m.sdk.CreateNonceRequest()
    m.nonceManager = m.nonceLoader.loadNonceManager(nonceRequest)
    m.top.nonce = nonceManager.getNonce()
  End Function

  ' Registers an ad click using the IMA SDK.
  Function sendAdClick() as Void
    If m.nonceManager <> Invalid
      m.nonceManager.sendAdClick()
    End If
  End Function

  ' Registers an ad touch event using the IMA SDK.
  Function sendAdTouch(touch as String) as Void
    If m.nonceManager <> Invalid
      m.nonceManager.sendAdTouch(touch)
    End If
  End Function

  ' Registers the start of playback using the IMA SDK.
  Function sendPlaybackStart() as Void
    If m.nonceManager <> Invalid
      m.nonceManager.sendPlaybackStart()
    End If
  End Function

  ' Registers the end of playback using the IMA SDK.
  Function sendPlaybackEnd() as Void
    If m.nonceManager <> Invalid
      m.nonceManager.sendPlaybackEnd()
    End If
  End Function
]]>
</script>
</component>

광고 요청에 nonce 첨부

프로덕션 애플리케이션에서 PAL 라이브러리에서 수신한 nonce를 사용하려면 nonce가 생성된 후에만 광고 요청을 시작하세요. 그런 다음 u_paln 매개변수를 사용하여 광고 태그에 nonce를 추가합니다.

components/MainScene.xml

...

  ' Callback triggered when Nonce is loaded.
  Sub onNonceLoaded(message as Object)
    nonce = m.sdkTask.nonce
    print "onNonceLoaded ";nonce
    makeAdRequest(nonce)
  End Sub

  Sub makeAdRequest(nonce)
    ' Sample ad tag URL used in this sample. Your apps method of getting this
    ' URL will likely be different.
    adTag = "https://pubads.g.doubleclick.net/gampad/ads?iu=/124319096/external/single_ad_samples"

    preparedTag = adTag + "&u_paln=" + nonce

    ' Implement custom ad request logic here.
    Print "ad tag with nonce ";preparedTag
  End Sub
...

작업이 끝났습니다. 이제 PAL nonce를 요청하고 PAL SDK에 재생 세션 이벤트를 등록할 수 있는 Roku 앱이 생겼습니다.

(선택사항) 서드 파티 광고 서버를 통해 Google Ad Manager 신호 전송

서드 파티 광고 서버의 Ad Manager 요청을 구성합니다.

서버의 Ad Manager 요청에 nonce를 포함하도록 서드 파티 광고 서버를 구성합니다. 다음은 서드 파티 광고 서버 내에서 구성된 광고 태그의 예입니다.

'https://pubads.serverside.net/gampad/ads?givn=%%custom_key_for_google_nonce%%&...'

자세한 내용은 Google Ad Manager 서버 측 구현 가이드를 참고하세요.

Ad Manager는 givn=를 찾아 nonce 값을 식별합니다. 서드 파티 광고 서버는 자체 매크로(예: %%custom_key_for_google_nonce%%)를 지원하고 이를 이전 단계에서 제공한 nonce 쿼리 매개변수로 대체해야 합니다. 이를 실행하는 방법에 관한 자세한 내용은 서드 파티 광고 서버 문서에서 확인할 수 있습니다.

작업이 끝났습니다. 이제 nonce 매개변수가 PAL SDK에서 중간 서버를 거쳐 Google Ad Manager로 전파됩니다. 이렇게 하면 Google Ad Manager를 통해 더 효과적으로 수익을 창출할 수 있습니다.