经济型极客 - 使用 Roku DVP 重新混音互联网和电视
本月,挑战是撰写关于 Linux 和娱乐的文章。 MythTV 很棒,但已被报道过。 Boxee 看起来很棒,但有人抢先一步写了。 我开始担心找不到合适的题材,直到我的妻子提醒我,我们正在观看 Farscape 的设备运行的是 Linux 系统,她建议我看看除了丰富的纪录片资源外,是否还有其他值得探讨的内容。 幸运的是,有很多内容值得撰写。
该设备是 Roku 数字视频播放器,于 2008 年发布。这个小巧的盒子使用 Linux 系统将 Netflix 视频流传输到您的电视上。 直到最近,这还是购买它的唯一理由,但现在,Roku 已经开始宣布推出其他内容频道(例如美国职业棒球大联盟、Facebook、Pandora 等)。 看到这个小巧的盒子扩展其功能非常有趣。 更令人感兴趣的是,Roku 启动了一个开发者计划,允许任何人为其创建自定义频道。
在我深入探讨之前,先介绍一点背景信息。 我的妻子来自华盛顿州西雅图,是 KEXP 广播电台的忠实听众和支持者。 过去,我曾做过几个项目,尝试将 KEXP 带到我们在德克萨斯州圣安东尼奥的家中,但考虑到 Roku 的功能,它似乎是将 KEXP 点播内容带到我们客厅的理想平台。
成为开发者的第一步是访问 Roku.com 网站,并在其门户中创建一个帐户。 之后,点击开发者链接。 有一个小的注册表格,您需要在其中同意遵守开发者计划的规则。 完成后,您可以下载 SDK 并开始使用。
Roku 试图履行其对允许 Roku 工作的 GPL 代码的义务。 如果您访问 www.roku.com/support/gpl_rdvp.aspx,您可以下载软件的 tarball 文件。 在阅读开发者协议时,有一点让我印象深刻,其中一部分写道:“在宽限期内,您的频道应用程序必须始终……iii. 不包含任何可能要求 Roku 公开张贴或展示任何第三方声明或对此类代码的任何修改的开源代码或其他受限代码。” 我理解 Roku 员工试图保护自己免受什么影响。 显然,Roku 平台的部分组件是从第三方获得许可的(例如对 WMA 的支持),Roku 希望保持部分组件的专有性。 我假设 MIT 许可证是可以的,但这听起来这种环境可能对 GPL 不是非常友好。 我在开发者论坛上发布了一个问题,但在我提交本文以供发表时,没有收到任何回复。
假设您仍然对该平台感兴趣,那么您有三个不同的选项来部署频道:将其推送到本地 Roku、将其创建为私有频道或获得批准成为公共频道。
公共频道需要 Roku 的批准。 一旦获得批准,Roku 会在网站的频道商店中提供它。 如果您正在构建供公众使用的频道,则应该采取这种途径。 私有频道允许您向特定人群发布您的频道。 这样做的好处是您无需 Roku 的批准即可发布它。 缺点是将频道安装到 Roku 上需要更多步骤。 本地推送专为频道的开发阶段而设计。 一旦您在 Roku 上启用开发模式(使用遥控器输入 Home 3 次,向上 2 次,向右、向左、向右、向左、向右),您就可以直接上传您的频道。 SDK 包含一个方便的 Makefile,使此操作变得非常容易。 这就是我进行所有开发的方式,因为我正在为自己(和其他开发者)开发频道。 这种最后开发形式的主要限制是您一次只能安装一个“开发”频道。 这对我来说不是什么大问题,但如果您要共享代码,可能会使事情变得更加复杂。
Roku 开发完全使用 BrightScript 进行,这似乎是一种自定义语言,它结合了 Visual Basic 和 JavaScript 的思想。 主要目标是允许您使用动态语言编写代码,这些代码可以轻松编译成适用于 Roku 嵌入式环境的高效代码。 这是通过添加 BrightScript 组件来实现的,例如默认屏幕和媒体工具。 您可以使用 BrightScript 自定义屏幕并构建您的频道。 甚至还有一个 PowerPoint 演示文稿模板,可让您轻松地模拟应用程序屏幕。
本文的所有代码都在 github.com/economysizegeek/linux_journal_roku 上共享。 这将使查看不同的频道变得容易得多。 只需git clone该存储库,您将拥有所有代码的最新版本。 我假设您已为您的 Roku 开启了开发者模式。 您还需要在 shell 中设置一个环境变量,以便 Makefile 知道将代码推送到哪里。 在我的例子中,我添加了export ROKU_DEV_TARGET=192.168.210.244到我的 .bashrc 文件中。 确保您输入的是您的 Roku 的 IP 地址。
让我们从一个简单的 Hello World! 示例开始。 这将使您熟悉这些工具,并确认您已完成所有设置。 在 git 存储库中,有一个名为 hello_world 的目录。 在该目录中,您应该能够输入make install,它将自动将新频道推送到您的 Roku 上。 如果这不起作用,请确认您已安装 make、curl 和 zip(Makefile 依赖于它们)。 还要确保您的 Roku 处于开发者模式(输入本文中的组合键)。 此外,请确保您没有设置环境变量(echo $ROKU_DEV_TARGET应该打印一个 IP),并检查您是否将其设置为错误的 IP(Roku 的 IP 可以在设置→播放器信息中确认)。
一旦您解决了这个问题,您应该能够转到 Roku 并看到一个名为“Hello World”的新频道。 使用遥控器点击它将启动该频道。 它实际上除了说“Hello World!”之外什么也不做。 重点是确认您的环境,并为您快速浏览一下开始制作自己的频道所需的内容。
在 hello_world 目录中,您将看到两个目录和一些文件。 images 目录包含此小型应用程序所需的各种图像。 您会注意到有些图像的名称中带有 HD,有些带有 SD。 这代表高清和标清。 由于 Roku 连接到电视,因此您必须确保您显示的任何美术作品都针对电视屏幕进行了正确的大小调整。 Makefile 在那里是为了方便编译、安装和删除频道。 manifest 文件是 Roku 构建另一侧的软件包所必需的。 剩下的就是 source 目录和 HelloWorld.brs 文件(列表 1)。 brs 文件是实际设置频道的 BrightScript 代码。
列表 1. HelloWorld.brs
'Copyright 2010 Dirk Elmendorf Function Main() as Integer print "Hello World! to the console" print ' you will not see this ' p = CreateObject("roMessagePort") screen = CreateObject("roSpringboardScreen") screen.SetMessagePort(p) o = CreateObject("roAssociativeArray") o.ContentType = "episode" o.Title = "Sample App" o.Description = "Hello World!" o.SDPosterURL = "pkg:/images/episode_icon_sd.png" o.HDPosterURL = "pkg:/images/episode_icon_sd.png" screen.SetContent(o) screen.AddButton(1, "Exit") screen.Show() while true msg = wait(0, p) if msg.isButtonPressed() print "Goodbye World!" return 0 endif end while end Function
让我们逐步了解一下代码,但如果您从 git 存储库中获取的版本不同,则意味着我在本文付印后修复了一个错误。
第一个代码清单是 BrightScript 的速成课程。 它所做的只是创建一个 Main 函数,构建一个简单的屏幕,并在任何按钮按下时退出。 print 语句会输出到 Roku 控制台。 控制台还允许您进入调试器并查看任何语法错误。 您可以通过输入以下命令访问 Roku 控制台telnet $ROKU_DEV_TARGET 8085。 我不得不使用它几次来找出为什么频道没有显示出来(这通常意味着存在语法错误)。
如您所见,我创建了一个名为 Main 的函数。 这是所有 BrightScript 应用程序的入口点。 我包含了两个不同的 print 语句。 第一个语句有效,但第二个语句只打印一个空行,因为单引号在 BrightScript 中被视为注释。 然后,我创建了两个对象。 roMessagePort 是消息(事件)发送到的位置。 roSpringboardScreen 是一个 BrightScript 组件。 这基本上是一个您可以通过提供信息来重新配置的屏幕。 Roku 附带了许多此类屏幕供您使用。 我选择这个屏幕是因为它最容易使用。 我连接了屏幕和端口。 这告诉系统将屏幕上的所有事件发送到我创建的消息端口。 可能在某些情况下,您需要为不同的事件设置不同的消息端口,但到目前为止,我只使用了一个与每个屏幕共享的单个消息端口。
然后,我创建了一个 roAssociativeArray。 这非常像哈希(或普通的 JavaScript 对象)。 我使用它来设置许多预定义的字段。 这将传递到屏幕中。 接下来,我在屏幕上创建一个按钮,并告诉系统绘制屏幕。 最后一步是设置一个无限循环。 wait 允许系统在事件触发之前保持等待状态。 然后,我可以处理该事件的结果。 在这种情况下,这意味着程序退出。
下一步是添加另一个 BrightScript 组件——即 roAudioPlayer。 此组件处理 MP3 流的播放。 我从头开始编写了一个新脚本来完成此操作。 用于处理流媒体的频道位于 kexp 目录中。 主文件(列表 2)位于 source/KEXP.brs 中(位于 github.com/economysizegeek/linux_journal_roku)。
列表 2. KEXP.brs
'Copyright 2010 Dirk Elmendorf Function Init() as Object obj = { port: CreateObject("roMessagePort") screen: CreateObject("roSpringboardScreen") player: CreateObject("roAudioPlayer") screen_options: CreateObject("roAssociativeArray") song: CreateObject("roAssociativeArray") status: "" drawScreen: function(description) m.screen_options.Description = description m.screen.SetContent(m.screen_options) m.screen.Show() end function playingNow: function() m.screen.ClearButtons() m.screen.AddButton(1, "Pause Stream") m.screen.AddButton(3, "Exit") m.DrawScreen("Live MP3 Stream from KEXP.org") end function play: function() if m.status = "" then m.player.AddContent(m.song) m.player.SetLoop(true) m.player.play() m.status = "playing" else if m.status = "paused" m.player.resume() m.status = "playing" endif m.screen.ClearButtons() m.screen.AddButton(3, "Exit") m.drawScreen("Buffering....") end function pause: function() if m.status = "playing" then m.player.pause() m.status = "paused" endif m.screen.ClearButtons() m.screen.AddButton(2, "Resume Stream") m.screen.AddButton(3, "Exit") m.drawScreen("Stream Paused") end function exit: function() print "Goodbye World!" m.player.stop() return 0 end function } obj.screen.SetMessagePort(obj.port) obj.player.SetMessagePort(obj.port) obj.screen_options.ContentType = "episode" obj.screen_options.Title = "KEXP" obj.screen_options.SDPosterURL = "pkg:/images/episode_icon_sd.png" obj.screen_options.HDPosterURL = "pkg:/images/episode_icon_sd.png" obj.screen.SetStaticRatingEnabled(false) obj.song.Url = "http://kexp-mp3-2.cac.washington.edu:8000/" obj.song.StreamFormat = "mp3" obj.status = "" return(obj) end Function Function Main() as Integer app = Init() app.play() while true msg = wait(0, app.port) if type(msg) = "roAudioPlayerEvent" if msg.isStatusMessage() then print "Audio Player Event:"; msg.getmessage() if msg.GetMessage() = "start of play" then app.playingNow() endif endif else if msg.isButtonPressed() then if msg.GetIndex() = 1 then app.pause() else if msg.GetIndex() = 2 then app.play() else if msg.GetIndex() = 3 then return app.exit() endif else if msg.isScreenClosed() then return app.exit() endif end while end Function
列表 2 用于此频道。 它在两个主要方面有所不同。 首先,这次,事件循环实际上会查看被按下的按钮。 循环现在能够根据被按下按钮的索引值触发不同的行为。 另一个变化是我创建了一个应用程序对象。 我想演示 Roku 提供了一种非常动态的语言,所以我像在 JavaScript 中那样构建了它。 需要注意的一个奇怪的事情是在 app 对象的方法内部使用了“m.”。“m.”是指向该对象的指针。 您可以将其理解为“this.”,因为这就是 Roku 在编译时解析它的方式。
事件代码还在跟踪流媒体本身的状态。 如果您尝试暂停未播放的流媒体或尝试恢复未暂停的流媒体,则 roAudioPlayer 会将其视为错误。 代码处理了这些情况。 它还在您退出时处理关闭流媒体。
有了可用的流媒体应用程序后,我开始尝试将 KEXP 网站的“点播”部分集成到其中。 第一步是导航到一个节目并找到单个节目的 URL。 这花了一些功夫,因为点播节目使用与直播源不同的协议和 URL 提供服务。 最终,我通过下载和打开 KEXP 服务器提供的文件获得了 URL。
不幸的是,我遇到了 Roku 平台的限制。 它不支持 KEXP 正在使用的编解码器。 目前,第三方开发者无法添加额外的编解码器支持。 由于直播流以 MP3 格式提供,因此 Roku 可以很好地处理它,这对于我查看的许多广播源来说都是如此。 因此,如果您想要使用的流媒体通过 Flash 或其他一些编解码器进行流式传输,您可能也会很倒霉。 Roku 也支持 WMA,但这似乎更多的是用于文件而不是流媒体。
对于本文,我从头开始编写了所有内容。 当您处理自己的项目时,您无需这样做。 Roku 在 SDK 中包含许多不同的示例应用程序。 这些代码很好地展示了组件以及如何使用它们,包括音频、视频、XML 解析和处理注册过程。 这些示例也比我在此处提供的代码更加完善(它们包括如何为频道创建自己的主题的示例)。
接下来要玩的是视频流媒体。 我选择专注于本文的音频,因为它非常直接。 流式传输视频意味着获取视频源,以及正确编码它,然后您必须生成一个 XML 文档来描述它。 您将需要一个 Web 服务器,该服务器可以同时提供视频和 XML。 一旦您完成所有工作,您应该能够运行自己的小型 Netflix 风格的视频流媒体服务。 现在您已经开始涉足 Roku 开发,请访问 Brian Lane 的博客。 他记录了他为使所有这些工作正常运行所做的一些破解工作 (blog.brianlane.com/2009/12/20/streaming-local-video-with-your-roku)。
我写这篇文章的目标是为我的妻子设置流媒体(我基本上实现了这个目标——我仍在努力让 Shake the Shack 节目点播)。 我非常惊讶于上手和破解是如此容易。 最初,我因为不得不处理一种新语言而感到沮丧,但 BrightScript 与 JavaScript 非常相似,因此它不像我想象的那样成为障碍。 对我来说,最好的部分是 Roku 已经凭借 Netflix 赢得了它在我的 AV 机架中的位置。 增加对其进行破解的能力以及尝试其他人构建的频道的机会使其更加出色。
Dirk Elmendorf 是 Rackspace 的联合创始人,有时是家庭酿酒师,长期 Linux 倡导者,甚至是更长期的程序员。