Apple 于今年秋季发布了新版的 Apple TV,也带来了 iOS 开发者一直期盼的全新电视操作系统 — tvOS,正如 iPhone 的成功,Apple 从根本上就坚信基于应用的电视体验才是未来。tvOS 脱胎于 iOS,但又是一个完全独立的操作系统,拥有独立的 App Store。
官方提供了两种解决方案开发 tvOS 应用:
- Traditional Apps : 使用原有的 iOS Framework 开发,开发出的 App 可以同时兼容 iOS 设备和 Apple TV
- Client-Server Apps : 面向 Web 开发者的新解决方案,使用 JavaScript 和 TVML 编写 Native 应用
本文将介绍如何使用 TVML 和 TVJS 开发 一个 Client/Server App。
环境准备
- 开发 tvOS 应用需要 Xcode 7.1 及以上版本, 下载页面
- Midway ,本教程使用淘宝的 Node.js 框架 Midway 生成动态的 TVML 模板。(如果你对 Midway 或者 Node.js 不熟悉,也可以使用其他 Server 技术,只要能够在特定路由生成对应的 XML 模板和 main.js 就可以了)
SDK 介绍
先介绍 SDK 的组成:
- TVML : Apple’s Television Markup Language,基本上是一些 XML 语句,用于布局界面,布局界面时,我们会用到一些 Apple 提供的 TVML 模板创建我们的 UI,然后用 TVJS 写交互脚本
- TVJS : 一系列 JavaScript API,通过它你可以展示 TVML,控制应用流程
- TVMLKit : C/S 应用的容器,原生 SDK,实现 JavaScript 和 Native 的 Bridge
下图是 C/S App 的应用架构:
- 所有界面和逻辑代码都可以放在 Web Server 上,客户端只需要提供容器
- 每一个界面只需要提供一个 TVML 文件,App 中的 TVMLKit 框架负责解析并生成 Native 界面
让我们开始吧~
准备 Web Server
进入工作目录,先初始化一个 Midway 项目:
midway init //选择经典的 `Midway(koa) + BDO + Render + Security` 即可 // ... 等待依赖安装完成 midway start // 启动应用
打开 http://localhost:6001/
,如果显示 Midway 欢迎页面就是说明 Server 环境 ok 啦。
我们需要做一些定制,主要用来请求远程数据和创建 TVML 模板。
-
安装 npm 依赖包
tnpm i koa-jade koa-static --save
-
打开 app.js,替换为如下代码
'use strict';
var midway = require('midway'),
koa = require('koa'),
serve = require('koa-static'),
Jade = require('koa-jade');
var app = midway(
koa()
);
// 使用 jade 模板引擎生成 xml 内容
var jade = new Jade({
viewPath: __dirname + '/app/views/',
debug: false,
noCache: true,
debug:true
})
app.use(jade.middleware);
// static,存储 app 需要的启动 JS
app.use(serve(__dirname + '/static'));
module.exports = app; -
删除 app/views 下的所有文件和文件夹,新建一个名为 hello.jade 的文件,输入以下内容
document
alertTemplate
title hello world
description first tvOS App with TVML and Midway -
app 目录下新建 static 文件夹,新建一个 main.js 文件,输入以下内容
/* tvjs 启动文件 */
// app 启动回调
App.onLaunch = function() {
getDocument('http://localhost:6001/', function(error, doc) {
navigationDocument.pushDocument(doc);
});
}
// 获取 xml doc
function getDocument(url, callback) {
callback = callback || function() {};
var templateXHR = new XMLHttpRequest();
templateXHR.responseType = "document";
templateXHR.addEventListener("load", function() {
callback(null, templateXHR.responseXML);
}, false);
templateXHR.addEventListener("error", function(err) {
callback(err);
}, false);
templateXHR.open("GET", url, true);
templateXHR.send();
return templateXHR;
}
-
打开 app/controllers/home-controller,替换为如下内容
'use strict';
exports.index = function* () {
this.render('hello');
this.type = 'text/plain';
}; -
重启 Midway
- 打开 http://localhost:6001/main.js ,你会看到刚刚创建的 main.js
-
打开 http://localhost:6001/ ,你应该看到 jade 生成的 xml 模板
<document>
<alertTemplate>
<title>hello world</title>
<description>first tvOS App with TVML and Midway </description>
</alertTemplate>
</document>
好了,我们的服务端就搭建完成啦~
准备 Native 容器
-
新建一个 tvOS Single View Application 项目:
-
点击 next,输入项目名称为 demo,语言选择 Swift
-
删除
Main.storyboard
和ViewController.swift
,选择 Move to Trash -
打开
Info.plist
,删除 Main storyboard file base name 这一配置 -
iOS9 默认不允许非 HTTPS 链接,需要在
Info.plist
里面添加配置。右击Info.plist
,选择open as => SourceCode
,在 dict child 中新增:<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
-
打开 AppDelegate.swift,替换为如下内容
import UIKit
import TVMLKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate ,TVApplicationControllerDelegate{
var window: UIWindow?
var appController: TVApplicationController?;
// 服务器地址
static let TVBootURL = "http://localhost:6001/main.js";
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// 创建 tvmlkit 环境
self.window = UIWindow(frame: UIScreen.mainScreen().bounds);
let appControllerContext = TVApplicationControllerContext();
if let javaScriptURL = NSURL(string: AppDelegate.TVBootURL) {
appControllerContext.javaScriptApplicationURL = javaScriptURL;
}
appController = TVApplicationController(context: appControllerContext, window: window, delegate: self);
return true
}
} -
CTRL + R
启动 App,你会看到如下界面 -
哇哈哈,配置这么长时间,成功走到这一步,必须要:
应用流
-
从代码可以看到,App 容器只需要指定一个 JS 文件地址(我们新建的 main.js ),而在 JS 文件中请求了一个 XML 模板,并添加到 navigationStack 中,界面就生成了。先介绍代码中用到的 TVJS 对象。
-
App
: TVJS 提供的全局对象,用来管理应用生命周期,当应用启动的时候,会触发 App 的 onLaunch 事件 -
navigationDocument
: NavigationDocument 实例,NavigationDocument 用来控制应用中的页面栈。应用生命周期中只有一个全局的 navigationDocument 实例
-
-
下图展示了一个 C/S App 的生命周期流程
TVML
- TVML 用来绘制每一个页面,App 页面栈中的每个页面都是一个 TVML 文件生成的 Docuemnt DOM
- TVML 本质上就是 XML,Apple 官方定义了一些用于绘制界面的 XML Element 和 Template,你必须使用这些 Template 和元素搭建页面
- 每个 Templete 代表了一种布局,如表单、列表、多维数据等, 每个 TVML 文件只能使用一个 Templete 。
- 下图表示了 TVML,TVML Templete,TVML Element 的关系
-
下图是官方提供的 catalogTemplate,用于展示二维数据
-
官方一共提供了近 20 种模板和上百个标签元素( 地址 ),已经能够满足绝大部分需求,你还可以用 Swift 和 TVMLKit 实现自定义的标签。
WWDC 视频合集
我们了解了 C/S App 的大致工作原理后,就可以开发更复杂的应用啦。这里我们实现一个历届 WWDC 视频播放的 App。
列表
- 首先需要实现一个视频列表界面,这里可以使用前面介绍的 catalogTemplate
-
打开 app/controllers/home-controller.js,替换成如下代码,添加一些假数据(你也可以直接从官方页面抓取数据)
'use strict';
// 模拟的 wwdc 数据
var videoData = [{
title: 'wwdc 2015',
desc:'wwdc 2015 年的学习视频',
data: [{
img: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/images/102_734x413.jpg',
title: 'Platforms State of the Union',
video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
}, {
img: 'http://devstreaming.apple.com/videos/wwdc/2015/105ncyldc6ofunvsgtan/105/images/105_734x413.jpg',
title: 'Platforms State of the Union',
video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
}, {
img: 'http://devstreaming.apple.com/videos/wwdc/2015/106z3yjwpfymnauri96m/106/images/106_734x413.jpg',
title: 'Platforms State of the Union',
video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
}, {
img: 'http://devstreaming.apple.com/videos/wwdc/2015/104usewvb5m0qbwafx8p/104/images/104_734x413.jpg',
title: 'Platforms State of the Union',
video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
}, {
img: 'http://devstreaming.apple.com/videos/wwdc/2015/709jcaer6su/709/images/709_734x413.jpg',
title: 'Platforms State of the Union',
video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
}, {
img: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/images/102_734x413.jpg',
title: 'Platforms State of the Union',
video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
}, {
img: 'http://devstreaming.apple.com/videos/wwdc/2015/212mm5ra3oau66/212/images/212_734x413.jpg',
title: 'Platforms State of the Union',
video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
}, {
img: 'http://devstreaming.apple.com/videos/wwdc/2015/106z3yjwpfymnauri96m/106/images/106_734x413.jpg',
title: 'Platforms State of the Union',
video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
}]
},{
title: 'wwdc 2014',
desc:'wwdc 2014 年的学习视频',
data: [{
img: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/images/102_734x413.jpg',
title: 'Platforms State of the Union',
video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
}]
},{
title: 'wwdc 2013',
desc:'wwdc 2013 年的学习视频',
data: [{
img: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/images/102_734x413.jpg',
title: 'Platforms State of the Union',
video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
}]
}];
exports.index = function*() {
this.render('list', {
data:videoData
});
this.type = 'text/plain';
}; -
在 views 目录中新增 list.jade 模板,内容如下
<?xml version="1.0" encoding="UTF-8" ?>
document
catalogTemplate
banner
title 历届 wwdc 视频
list
section
each item in data
listItemLockup
title #{item.title}
decorationLabel
relatedContent
grid
section
each video in item.data
lockup(data-video-url="#{video.video}")
img(src="#{video.img}", width="350" , height="250")
title #{video.title}
```
* 重启 Midway
* 重新运行 App,哇咔咔,列表这就出来了
![wwdc](https://g01.alibaba-inc.com/tfscom/TB1XevrKVXXXXbJXFXXXXXXXXXX.tfsprivate.png)
### 播放视频
接下来就可以继续实现播放视频功能了,首先让我们实现 cell 的点击事件。
在 main.js 中添加
```javascript
App.onLaunch = function() {
getDocument('http://localhost:6001/', function(error, doc) {
navigationDocument.pushDocument(doc);
// 添加下面的内容
// 点击事件
doc.addEventListener('select', function(event) {
var ele = event.target;
// 获取视频地址
var videoURL = ele.getAttribute('data-video-url');
if (videoURL) {
var player = new Player();
var playlist = new Playlist();
var mediaItem = new MediaItem("video", videoURL);
player.playlist = playlist;
player.playlist.push(mediaItem);
player.present();
};
})
});
}
再次重启 Midway,重启应用,点击视频,哇咔咔,done
- TVML 的事件处理就是 标准的 DOM 事件 ,我们可以使用 DOM 的 API 添加元素的点击事件,获取元素的属性等(视频地址在元素的 data-video-url 属性上)
- TVJS 提供了完善的 API 播放视频,可以参考官方 Player 和 PlayerList 的 API
-
我们的 App 就完成了,效果如下