我最近为我喜欢的电台——Radio Paradise 创建了一个 Firefox OS 应用。开发这个应用非常有趣,所以我认为分享一些关于我如何构建它的笔记会很有帮助。
音频标签
首先,我实现了应用的主要功能,使用 HTML5 音频 元素播放从网络电台获得的 ogg 流。
<pre lang="xml">
<audio src="http://stream-sd.radioparadise.com/rp_192m.ogg" controls preload></audio>
</pre>
这很简单!此时,我们的应用已经完全可以运行了。如果你不相信,可以查看 这个 jsfiddle。但请继续阅读,因为我们还会添加一些更酷的功能。事实上,请查看下面的简短视频,了解最终效果。
由于此内容属于 Radio Paradise,所以在实现应用之前,我联系了他们,请求许可为他们的电台制作一个 Firefox OS 应用;他们回复道:
谢谢。我们很乐意您这么做。我们现有的网络播放器 是基于 HTML5 的。这可能是一个开始的地方。Firefox 应该原生支持我们的 Ogg Vorbis 流。
我无法要求得到更令人鼓舞的回应,这足以让我开始行动。
应用的功能
我希望这个应用非常简洁和简单——无论是在用户体验方面还是在支持它的代码方面。以下是我决定包含的功能列表:
- 一个简单易用的按钮来播放和暂停音乐
- 当前播放歌曲的艺术家姓名、歌曲标题和专辑封面应填充界面
- 设置选项以选择歌曲质量(在带宽不足以处理最高质量的情况下)
- 设置选项以播放或暂停音乐启动应用
- 即使应用发送到后台也继续播放
- 当应用在前台运行时保持屏幕开启
我没有使用 HTML 标签,而是决定在 JavaScript 中创建音频元素并对其进行配置。然后,我为按钮连接了一个事件监听器来播放或停止音乐。
<pre lang="javascript">
var btn = document.getElementById('play-btn');
var state = 'stop';
btn.addEventListener('click', stop_play);
// 创建一个可以在后台播放的音频元素
var audio = new Audio();
audio.preload = 'auto';
audio.mozAudioChannelType = 'content';
function play() {
audio.play();
state = 'playing';
btn.classList.remove('stop');
btn.classList.add('playing');
}
function stop() {
audio.pause();
state = 'stop';
btn.classList.add('stop');
btn.classList.remove('playing');
}
// 在播放和停止状态之间切换
function stop_play() {
(state == 'stop') ? play() : stop();
}
</pre>
访问当前歌曲信息
我面临的第一个挑战是访问当前歌曲信息。通常,只要第三方 API 提供正确的标头信息,我们就不需要任何特殊权限来访问它们。但是,Radio Paradise 提供给我的获取当前歌曲信息的链接不允许跨域访问。幸运的是,Firefox OS 为此类情况保留了一种特殊的权限——systemXHR 出现了。
<pre lang="javascript">
function get_current_songinfo() {
var cache_killer = Math.floor(Math.random() * 10000);
var playlist_url =
'http://www.radioparadise.com/ajax_rp2_playlist.php?' +
cache_killer;
var song_info = document.getElementById('song-info-holder');
var crossxhr = new XMLHttpRequest({mozSystem: true});
crossxhr.onload = function() {
var infoArray = crossxhr.responseText.split('|');
song_info.innerHTML = infoArray[1];
next_song = setInterval(get_current_songinfo, infoArray[0]);
update_info();
};
crossxhr.onerror = function() {
console.log('Error getting current song info', crossxhr);
nex_song = setInterval(get_current_singinfo, 200000);
};
crossxhr.open('GET', playlist_url);
crossxhr.send();
clearInterval(next_song);
}
</pre>
这意味着应用必须具有特权,因此需要打包。我通常会尝试将我的应用托管,因为这对 Web 应用来说非常自然,并且有很多好处,包括可以被搜索引擎访问的额外好处。但是,在这种情况下,我们别无选择,只能打包应用并赋予其所需的特殊权限。
<pre lang="javascript">
{
"version": "1.1",
"name": "Radio Paradise",
"launch_path": "/index.html",
"description": "Radio Paradise 的非官方应用",
"type": "privileged",
"icons": {
"32": "/img/rp_logo32.png",
"60": "/img/rp_logo60.png",
"64": "/img/rp_logo64.png",
"128": "/img/rp_logo128.png"
},
"developer": {
"name": "Aras Balali Moghaddam",
"url": "http://arasbm.com"
},
"permissions": {
"systemXHR": {
"description" : "访问 radioparadise.com 上的当前歌曲信息"
},
"audio-channel-content": {
"description" : "应用转到后台时播放音乐"
}
},
"installs_allowed_from": ["*"],
"default_locale": "en"
}
</pre>
更新歌曲信息和专辑封面
对 Radio Paradise 的那个 XHR 调用为我提供了三个重要的信息:
- 当前播放歌曲的名称及其艺术家
- 包含专辑封面的图像标签
- 当前歌曲结束剩余时间(毫秒)
当前歌曲结束剩余时间非常有用。这意味着我可以执行 XHR 调用并仅为每首歌曲更新一次歌曲信息。我首先尝试使用 setTimeout 函数,如下所示:
<pre lang="javascript">
// 不起作用的示例。你能发现错误吗?
crossxhr.onload = function() {
var infoArray = crossxhr.responseText.split('|');
song_info.innerHTML = infoArray[1];
setTimeout('get_current_songinfo()', infoArray[0]);
update_info();
};
</pre>
令我惊讶的是,这不起作用,并且我在 logcat 中收到了一条关于 CSP 限制 的错误消息。事实证明,出于安全原因,任何尝试动态执行代码的行为都被禁止。在这种情况下,为了避免 CSP 问题,我们只需传递一个可调用对象,而不是字符串。
<pre lang="javascript">
// 而不是向 setTimout 传递字符串,我们向其传递
// 一个可调用对象
setTimeout(get_current_songinfo, infoArray[0]);
</pre>
更新:Mindaugas 在下面的评论中 指出,以这种方式使用 innerHTML 来解析未知内容会带来一些安全风险。由于这些安全隐患,我们应该以文本而不是 HTML 的形式检索远程内容。一种方法是使用 song_info.textContent,它不会将传递的内容解释为 HTML。另一种选择,正如 Frederik Braun 指出的,是使用无法呈现 HTML 的文本节点。
添加独特的触感
为 Web 开发移动应用的一大优势在于,您可以完全自由地以任何想要的方式设计您的应用。没有强制的样式或对交互设计创新的限制。了解这一点后,我很难抑制住自己探索新想法并为应用添加一些乐趣的冲动。我决定将设置隐藏在主要内容后面,然后添加一个功能,以便用户可以从中间切开应用以访问设置。这样,它们就被隐藏起来了,但仍然可以通过直观的方式发现。对于设置页面中的 UI 元素以切换选项,我决定尝试使用 Brick,并添加了一些自定义样式。
使用滑动手势
如您在上面的视频中看到的,为了打开和关闭封面图像,我使用了平移和滑动手势。为了实现这一点,我使用了 来自 Gaia 的手势检测器。将手势代码作为模块集成到我的应用中并将其连接到封面图像非常容易。
组织代码
对于像这样的小型应用,我们不必使用模块化代码。但是,由于我最近开始学习 AMD 实践,所以我决定使用模块系统。我询问了 James Burke 在此类应用中使用 requirejs 的影响。他建议我改用 Alameda,因为它面向现代浏览器。
保存应用设置
我希望允许用户选择流质量以及是否希望应用在打开时立即开始播放音乐。这两个选项都需要保存在某个地方,并在应用启动时检索。我只需要保存几个键值对。我去到了 #openwebapps irc 频道并寻求建议。Fabrice 指向了我 Gaia 中的一段不错的代码(再次!),这段代码用于 异步存储 键值对甚至整个对象。这非常适合我的用例,所以我借鉴了它。Gaia 似乎是一个宝库。这是我为设置创建的模块。
<pre lang="javascript">
define(['helper/async_storage'], function(asyncStorage) {
var setting = {
values: {
quality: 'high',
play_on_start: false
},
get_quality: function() {
return setting.values.quality;
},
set_quality: function(q) {
setting.values.quality = q;
setting.save();
},
get_play_on_start: function() {
return setting.values.play_on_start;
},
set_play_on_start: function(p) {
setting.values.play_on_start = p;
setting.save();
},
save: function() {
asyncStorage.setItem('setting', setting.values);
},
load: function(callback) {
asyncStorage.getItem('setting', function(values_obj) {
if (values_obj) setting.values = values_obj;
callback();
});
}
};
return setting;
});
</pre>
分割封面图像
现在我们进入真正有趣的部分,即将封面图像分成两半。为了实现这种效果,我创建了两个大小相同的重叠画布元素,这两个元素的大小都适合设备宽度。一个画布裁剪图像并保留其左侧部分,而另一个画布保留右侧部分。
以下是 draw 函数的代码,其中大部分操作都在这里发生。请注意,此函数仅为每首歌曲运行一次,或者当用户将设备方向从纵向更改为横向或反之亦然时运行。
<pre lang="javascript">
function draw(img_src) {
width = cover.clientWidth;
height = cover.clientHeight;
draw_half(left_canvas, 'left');
draw_half(right_canvas, 'right');
function draw_half(canvas, side) {
canvas.setAttribute('width', width);
canvas.setAttribute('height', height);
var ctx = canvas.getContext('2d');
var img = new Image();
var clip_img = new Image();
// 透明度 0.01 用于使裁剪中的任何故障不可见
ctx.fillStyle = 'rgba(255,255,255,0.01)';
ctx.beginPath();
if (side == 'left') {
ctx.moveTo(0, 0);
// 添加一个像素以确保没有间隙
var center = (width / 2) + 1;
} else {
ctx.moveTo(width, 0);
var center = (width / 2) - 1;
}
ctx.lineTo(width / 2, 0);
// 在中心向下绘制波浪图案
var step = 40;
var count = parseInt(height / step);
for (var i = 0; i < count; i++) {
ctx.lineTo(center, i * step);
// 交替曲线控制点 20 像素,每次交替
ctx.quadraticCurveTo((i % 2) ? center - 20
center + 20, i * step + step * 0.5, center, (i + 1) * step);
}
ctx.lineTo(center, height);
if (side == 'left') {
ctx.lineTo(0, height);
ctx.lineTo(0, 0);
} else {
ctx.lineTo(width, height);
ctx.lineTo(width, 0);
}
ctx.closePath();
ctx.fill();
ctx.clip();
img.onload = function() {
var h = width * img.height / img.width;
ctx.drawImage(img, 0, 0, width, h);
};
img.src = img_src;
}
}
</pre>
保持屏幕开启
我需要添加的最后一个功能是在应用在前台运行时保持屏幕开启,事实证明这也很容易实现。我们需要请求一个 屏幕唤醒锁
<pre lang="javascript">
var lock = window.navigator.requestWakeLock(resourceName);
</pre>
屏幕唤醒锁实际上非常智能。当应用发送到后台时,它将自动释放,然后在应用返回前台时重新赋予您的应用。目前,在这个应用中,我没有提供释放锁的选项。如果将来我收到添加该选项的请求,我只需释放之前获取的锁并将选项设置为 false 即可。
<pre lang="javascript">
lock.unlock();
</pre>
获取应用
如果你有一台 FirefoxOS 设备并且喜欢优秀的音乐,你现在就可以在你的设备上安装这个应用。在应用商店搜索“radio paradise”,或者直接从这个链接安装。你也可以从 Github 获取完整的源代码。随意地 fork 和修改应用,创建你自己的网络电台应用!如果你发现问题,需要新功能或者想提交代码修改,请告诉我。
总结
我越来越惊讶于我们能用多快的速度使用 Web 技术创建非常实用且独特的移动应用。如果你还没有为 Firefox OS 创建一个移动 Web 应用,你绝对应该尝试一下。开放式 Web 应用的未来非常令人兴奋,而 Firefox OS 提供了一个体验这种兴奋的绝佳平台。
现在轮到你来发表评论了。你最喜欢这个应用的哪个功能?如果你开发这个应用,你会怎么做得不同?我们如何才能使这个应用变得更好(代码和用户体验方面)?
关于 Aras Balali Moghaddam
Aras 是一位交互设计师和前端工程师,居住在美丽的加拿大不列颠哥伦比亚省。他热爱开放式 Web,并且喜欢构建很棒的移动 Web 应用。你可以在他的博客上了解更多关于他的信息。
关于 Robert Nyman [荣誉编辑]
Mozilla Hacks 的技术布道师和编辑。进行 HTML5、JavaScript 和开放式 Web 相关的演讲和博客创作。Robert 是 HTML5 和开放式 Web 的坚定支持者,自 1999 年以来一直从事 Web 前端开发工作 - 在瑞典和纽约市。他还在http://robertnyman.com定期发布博客,并且喜欢旅行和结识新朋友。
17 条评论