为我喜欢的网络电台构建 Firefox OS 应用

我最近为我喜欢的电台——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 的文本节点。

radio paradise mobile web app running on FirefoxOS

通过一些 CSS 魔法,事情开始迅速步入正轨。

添加独特的触感

为 Web 开发移动应用的一大优势在于,您可以完全自由地以任何想要的方式设计您的应用。没有强制的样式或对交互设计创新的限制。了解这一点后,我很难抑制住自己探索新想法并为应用添加一些乐趣的冲动。我决定将设置隐藏在主要内容后面,然后添加一个功能,以便用户可以从中间切开应用以访问设置。这样,它们就被隐藏起来了,但仍然可以通过直观的方式发现。对于设置页面中的 UI 元素以切换选项,我决定尝试使用 Brick,并添加了一些自定义样式。

radio paradise app settings

用户可以滑动打开封面图像以访问其后面的应用设置

使用滑动手势

如您在上面的视频中看到的,为了打开和关闭封面图像,我使用了平移和滑动手势。为了实现这一点,我使用了 来自 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>

分割封面图像

现在我们进入真正有趣的部分,即将封面图像分成两半。为了实现这种效果,我创建了两个大小相同的重叠画布元素,这两个元素的大小都适合设备宽度。一个画布裁剪图像并保留其左侧部分,而另一个画布保留右侧部分。

Each canvas clips and renders half of the image

每个画布裁剪并渲染图像的一半

以下是 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 应用。你可以在他的博客上了解更多关于他的信息。

更多 Aras Balali Moghaddam 的文章…

关于 Robert Nyman [荣誉编辑]

Mozilla Hacks 的技术布道师和编辑。进行 HTML5、JavaScript 和开放式 Web 相关的演讲和博客创作。Robert 是 HTML5 和开放式 Web 的坚定支持者,自 1999 年以来一直从事 Web 前端开发工作 - 在瑞典和纽约市。他还在http://robertnyman.com定期发布博客,并且喜欢旅行和结识新朋友。

更多 Robert Nyman [荣誉编辑] 的文章…


17 条评论

  1. SanThosh K

    太棒了…… :) :) 动画简直太棒了。

    2013 年 10 月 24 日 23:06

    1. Aras Balali Moghaddam

      感谢你的评论 SanThosh,很高兴你喜欢它 :)

      2013 年 10 月 25 日 11:43

  2. Ivan Dejanovic

    我真的很喜欢你的应用在视频中的样子。即使在通读整篇文章后,我仍然对这个应用如此酷炫感到惊讶,考虑到构建它所需的代码量。

    我也非常喜欢你的两个画布的想法。我第一次看视频时,真的很好奇你是如何让这种分割效果看起来如此好和自然。如果你不介意,我会仔细检查一下你是如何实现这个图像分割的。我可能会在我的工作中使用类似的东西。

    2013 年 10 月 25 日 01:19

    1. Aras Balali Moghaddam

      当然!这就是整个想法 Ivan。我非常乐意看到这个想法的类似实现/变体。也许在我们实现了几个变体之后,我们可以考虑将其变成一个可重用的组件 - 如果它被证明是有用的。请随时告诉我。

      2013 年 10 月 25 日 11:42

  3. Mindaugas J.

    通过简单的 HTTP 协议使用来自某些网站的未知内容的 innerHTML 看起来不太好。你容易受到恶意代码访问权限提升的攻击。

    2013 年 10 月 25 日 09:44

    1. Aras Balali Moghaddam

      感谢你的评论 Mindaugas。这是一个合理的担忧,我同意人们在使用这种技术时应该非常小心。但是,在本例中,我信任了第三方 radioparadise,我的应用完全依赖于它。从某种意义上说,你可以说这是他们的应用而不是我的,我认为如果他们不突然更改该页面的内容,这对他们来说也是最好的 - 他们的其他应用也依赖于它。还要注意,由于 CSP 限制,没有任何代码的动态评估。

      2013 年 10 月 25 日 11:52

      1. Mindaugas J.

        重点是,即使你现在信任该网站,它也可能被入侵或被中间人攻击伪造。任意代码将拥有与你的应用相同的访问权限。

        2013 年 10 月 28 日 09:21

        1. Aras Balali Moghaddam

          我明白了。你能推荐一些在发生 MITM 攻击时如何使此代码更安全的方法吗?是否有办法剥离可能注入到 HTML 内容中的脚本?如果提供了具体的安全改进建议,我很乐意更新这篇文章。

          2013 年 10 月 28 日 11:42

          1. Frederik Braun

            我建议用一个函数(例如 updateText)替换 innerHTML 赋值,然后该函数更新一个文本节点(document.createTextNode),并将该文本节点设置为你的标签的子元素。文本节点无法渲染 HTML,因此不会有注入;)

            2013 年 10 月 29 日 12:46

          2. Aras Balali Moghaddam

            感谢你们两位对这个安全问题的评论。我已经更新了这篇文章以解决这个问题。

            2013 年 10 月 30 日 14:53

  4. nadrimajstor

    你会如何处理以下附加功能?
    1. 在 FxOS 主浏览器中打开广播电台的关于网页。
    2. 在设备的无线连接断开时限制下载?

    2013 年 10 月 25 日 15:39

    1. Aras Balali Moghaddam

      非常好的问题!
      1) 当用户点击设置页面中的 radio paradise 链接时,实际上已经实现了这一点,我只是没有在演示中展示它。为了从你的应用中在默认浏览器中打开链接,你需要做的就是设置 target="_blank"。
      2) 我也一直在考虑这个问题,并且在一段时间前问过这个问题:https://groups.google.com/forum/#!searchin/mozilla.dev.webapps/network$20api/mozilla.dev.webapps/NKdRZ9GLsX8/AyCqn3yS2x4J

      我相信一旦相关错误被修复(https://bugzilla.mozilla.org/show_bug.cgi?id=713199),网络 API 将可用,我们可以像你建议的那样添加这个非常有用的功能。

      2013 年 10 月 25 日 18:05

  5. nadrimajstor

    你没有解决 Brick 的 require/alameda 问题 #83 吗?;(

    2013 年 10 月 26 日 12:02

    1. Aras Balali Moghaddam

      Nadrimajstor,不幸的是,该兼容性问题尚未解决,但我相信他们正在努力解决。对于其他好奇这个问题是什么的人,这里有一个链接:https://github.com/mozilla/brick/issues/83

      2013 年 10 月 26 日 13:28

  6. Kevin

    我不幸没有 FFOS 手机来测试这个,但是当你断开连接时会发生什么,你能自动重新连接吗?

    此外,我上次尝试在 Firefox 中简单地播放 .ogg 流时,与例如 mplayer 相比,它会缓冲很多 - 你能从 js 中控制它吗?

    有没有办法确保你不会从旧缓冲区播放?(例如,我希望流停止播放,如果它不是“当前”的,并且在重新连接时重新缓冲,这样我们永远不会播放“旧”内容。)

    (顺便说一下,此表单尝试验证我的电子邮件并严重失败,我不得不删除 +)

    2013 年 10 月 30 日 00:57

    1. Aras Balali Moghaddam

      感谢你的评论 Kevin。我尽量根据我的知识回答。
      > 你能自动重新连接吗?
      是的。我已经实现了当出现错误时尝试从类似质量流队列中获取另一个流的方法:https://github.com/arasbm/radio-paradise/blob/master/scripts/app.js#L143-L160
      > 我们能从 js 中控制缓冲吗?
      我还没有尝试过,所以还不能回答。但看起来 `audioContext` 存在于 nightly 版本中:https://mdn.org.cn/en-US/docs/Web/API/AudioContext,我认为我们可以用它来控制缓冲区(以及更多其他功能)。我不知道这是否已在 FxOS 中可用。
      > 有没有办法确保你不会从旧缓冲区播放?
      我能想到的最简单的方法是在每次暂停和播放时重置流源,这将丢弃旧缓冲区。我相信还有其他方法可以做到这一点。

      2013 年 10 月 30 日 15:43

      1. Kevin

        感谢你的提示!AudioContext 看起来非常强大且复杂,我想如果 audio.addEventListener('error',f) 对我来说不够用,我将不得不研究它 :)

        2013 年 10 月 31 日 00:28

本文的评论已关闭。