使用 electron 做个播放器
使用 electron 做个播放器
本文同步更新在:https://github.com/whxaxes/blog/issues/8
前言
虽然 electron 已经出来好长时间了,但是最近才玩了一下,写篇博文记录一下,以便日后回顾。
electron 的入门可以说是相当简单,官方提供了一个 quick start,很流畅的就可以跑起来一个应用。
为啥要做个播放器呢,因为我在很久很久以前写过一个网页版的音频可视化播放器,但是因为是在页端,所以想播放本地音乐很麻烦,也没法保存。因此就想到用 electron 做个播放器 App,就可以读本地的网易云音乐目录了。
生成骨架
由于习惯用 vue,因此也准备用 vue 来实现这个应用。而目前就已经有个 electron-vue 的 boilplate 可以用。因此就直接通过 vue-cli 来进行初始化即可。
vue init simulatedgreg/electron-vue boom
然后就可以生成项目骨架了,结构如下:
.
├── .electron-vue
│ ├── build.js
│ ├── dev-client.js
│ ├── dev-runner.js
│ ├── webpack.main.config.js
│ └── webpack.renderer.config.js
├── dist
├── src
│ ├── index.ejs
│ ├── main
│ │ ├── index.dev.js
│ │ ├── index.js
│ └── renderer
│ ├── assets
│ ├── components
| ├── App.vue
│ ├── main.js
│ └── store.js
├── .eslintignore
├── .eslintrc.js
├── .travis.yml
├── appveyor.yml
├── .babelrc
├── package.json
├── README.md
生成好之后,就直接执行
yarn dev
就可以看到一个应用出现啦,然后就可以愉快的开始开发了。
主进程与渲染进程
在 electron 中有 main process 以及 renderer process 之分,简单来说,main process 就是用来创建窗口之类的,类似于后台,renderer process 就是跑在 webview 中的。两个进程中能调用的接口有部分是通用,也有一部分是独立的。不过不管是在哪个进程中,都可以调用 node 的常用模块,比如 fs、path 。
因此在 webview 跑的页面代码中,也可以通过 fs 模块读取本地文件,这点还是很方便的。
而且,就算在 renderer process 中想调用 main process 的接口也是可以的,可以通过 remote 模块。比如我需要监听当前窗口是否进入全屏,就可以这样写:
import { remote } from 'electron';
const win = remote.app.getCurrentWindow();
win.on('enter-full-screen', () => {
// do something
});
简直方便至极。
创建窗口
创建窗口的逻辑是在主进程中做的,逻辑很简单,就按照 electron 的 quick start 的方式进行创建即可。而且通过 electron-vue 生成的代码,其实也已经帮你把这块逻辑写好了。就自己进行一些小修改就可以了。
import { app, BrowserWindow, screen } from 'electron'
app.on('ready', () => {
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
cosnt win = new BrowserWindow({
height, width,
useContentSize: true,
titleBarStyle: 'hidden-inset',
frame: false,
transparent: true,
});
win.loadURL(`file://${__dirname}/index.html`);
})
由于我做的播放器,想全身是黑色风格的,因此在创建窗口时,传入 titleBarStyle
,frame
,transparent
这几个参数,可以把顶部栏隐藏掉。当然,隐藏之后,窗口就没法拖动了。所以还要在页面上加个用来拖动的透明顶部栏,再给个 css 属性:
-webkit-app-region: drag;
就可以实现窗口拖动了。
通信
主进程和渲染进程之间的通信是很常用的,通信是通过 IPC 通道实现的。代码逻辑写起来也很简单
main process 收发消息
import { ipcMain } from 'electron';
ipcMain.on('init', (evt, arg) => {
evt.sender.send('sync-config', { msg: 'hello' })
});
renderer process 收发消息
import { ipcRenderer } from 'electron';
ipcRenderer.send('init');
ipcRenderer.on('sync-config', (evt, arg) => {
console.log(arg.msg);
});
有一点要注意的就是,ipcMain 是没有 send 方法的,如果需要 ipcMain 主动推送消息到渲染进程,需要使用窗口对象实现:
win.webContents.send('sync-config', { msg: 'hello' });
配置保存
每个应用肯定是有一些用户配置的,比如放音乐的目录需要保存到配置中,下次打开就可以直接读取那个目录的音乐列表即可。
electron 提供了获取相关路径的接口 getPath
用于给应用保存数据。在 getPath 接口中,传入相应名称即可获取到相应的路径。
electron.app.getPath('home'); // 获取用户根目录
electron.app.getPath('userData'); // 用于存储 app 用户数据目录
electron.app.getPath('appData'); // 用于存储 app 数据的目录,升级会被福噶
electron.app.getPath('desktop'); // 桌面目录
...
由于我们这些配置数据不能保存在应用下,因为如果保存在应用下,应用升级后就会被覆盖掉,因此需要保存到 userData
下。
const electron = require('electron');
const dataPath = (electron.app || electron.remote.app).getPath('userData');
const fileUrl = path.join(dataPath, 'config.json');
let config;
if(fs.existSync(fileUrl)) {
config = JSON.parse(fs.readFileSync(fileUrl));
} else {
config = {};
fs.writeFileSync(fileUrl, '{}');
}
无论在 renderer process 中,还是在 main process 中,都是可以调用,在 main process 中就通过 electron.app 调用,否则就通过 remote 模块调用。
虽然无论在 main process 中还是在 renderer process 中都可以读到配置,但是考虑到两个进程中数据同步的问题,我个人觉得,这种配置读取与写入,还是统一在 main process 做好,renderer process 要保存数据就通过 IPC 消息通知 main process 进行数据的更改,保证配置数据的流向是单方向的,比如容易管理。
配置菜单
可以通过 Menu 类实现。
import { Menu } from 'electron';
Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
template 的格式可直接看官方文档,就是普通的 json 格式。
音频播放
讲完 electron 相关,然后就可以讲讲怎么播放音频了。
由于在 electron 中,在前端代码中也可以使用 node 的模块,因此,刚开始的想法是,直接用 fs 模块读取音频文件,然后再将读取的 Buffer 转成 Uint8Array 再转成 AudioBuffer ,然后连接到音频输出上进行播放就行了。大概逻辑如下:
const AC = new window.AudioContext();
const analyser = AC.createAnalyser();
const buf = fs.readFileSync(music.url);
const uint8Buffer = Uint8Array.from(buf);
// 音频解码
AC.decodeAudioData(uint8Buffer.buffer)
.then(audioBuf => {
const bs = AC.createBufferSource();
bs.buffer = audioBuf;
bs.connect(analyser);
analyser.connect(AC.destination);
bs.start();
});
但是,有个问题,音频解码很费时间,解码一个三四分钟的 mp3 文件就得 2 ~ 4 秒,这样的话我点击播放音乐都得等两秒以上,这简直不能忍,所以就考虑换种方法,比如用流的方式。
抱着这种想法就去查阅了文档,结果发现没有支持流的解码接口,再接着就想自己来模拟流的方式,读出来的 buffer 分成 N 段,然后逐段进行解码,解码完一段就播一段,嗯...想的挺好,但是发现这样做会导致解码失败,可能是粗暴的将 buffer 分段对解码逻辑有影响。
上面的方法行不通了,当然还有方法,audio 标签是支持流式播放的。于是就在启动应用的时候,建个音频服务。
function startMusicServer(callback) {
const server = http.createServer((req, res) => {
const musicUrl = decodeURIComponent(req.url);
const extname = path.extname(musicUrl);
if (allowKeys.indexOf(extname) < 0) {
return notFound(res);
}
const filename = path.basename(musicUrl);
const fileUrl = path.join(store.get(constant.MUSIC_PATH), filename);
if (!fs.existsSync(fileUrl)) {
return notFound(res);
}
const stat = fs.lstatSync(fileUrl);
const source = fs.createReadStream(fileUrl);
res.writeHead(200, {
'Content-Type': allowFiles[extname],
'Content-Length': stat.size,
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'max-age=' + (365 * 24 * 60 * 60 * 1000),
'Last-Modified': String(stat.mtime).replace(/\([^\x00-\xff]+\)/g, '').trim(),
});
source.pipe(res);
}).listen(0, () => {
callback(server.address().port);
});
return server;
}
然后在前端,直接更换 audio 标签的 src 即可,然后连接上音频输出:
<audio ref="audio"
:src="url"
crossorigin="anonymous"></audio>
const audio = this.$refs.audio;
const source = AC.createMediaElementSource(this.$refs.audio);
source.connect(analyser);
analyser.connect(AC.destination);
就这么愉快的实现了流式播放了。。。感觉白折腾了很久。
音频可视化
这个其实我在以前的博客里有说过了,不过再简单的说一下。在上一段中我会把音频连接到一个 analyser
中,其实这个是一个音频分析器,可以将音频数据转成频率数据。我们就可以用这些频率数据来做可视化。
只需要通过以下这段逻辑就可以获取到频率数据了,因为频率数据数据都是 0 ~ 255 的大小,长度总共 1024,因此用个 Uint8Array 来存储。
const arrayLength = analyser.frequencyBinCount;
const array = new Uint8Array(arrayLength);
analyser.getByteFrequencyData(array);
然后获取到这个数据之后,就可以在 canvas 中把不同频率以图像的形式画出来即可。具体就不赘述了,有兴趣的可以看我以前写的这篇博文。
打包
编写完代码之后,就可以使用 electron-packager 进行打包,在 Mac 上就会打包成 app,在 windows 应该会打成 exe 吧(没试过)。
安装 electron-packager (npm install electron-packager -g
)之后就可以打包了。
electron-packager .
总结
electron 还是相当方便的,让 web 开发者也可以轻松编写桌面应用。
上述代码均在:https://github.com/whxaxes/boom ,有兴趣的可以 clone 下来跑一下玩玩。
使用 electron 做个播放器的更多相关文章
- C#做音乐播放器时在自动下一曲中报异常的解决办法
---------------------- ASP.Net+Unity开发..Net培训.期待与您交流! ---------------------- 在利用Media Player做音乐播放器的时 ...
- 谈一谈做iOS播放器库开发所涉及的知识点
在自己研究生毕业的时候,想着能找上一份做视频编解码的工作,可惜没有如愿,最后到了一家iOS游戏渠道公司去做游戏支付业务的SDK开发,我的iOS正式开发生涯就这么开始了. 在那家iOS游戏渠道没做上一年 ...
- 自定义css样式结合js控制audio做音乐播放器
最近工作需求需要播放预览一些音乐资源,所以自己写了个控制audio的音乐播放器. 实现的原理主要是通过js调整audio的对象属性及对象方法来进行控制: 1.通过play().pause()来控制音乐 ...
- 慕课网electron写音乐播放器教程,代码跟随教程变动(十)
添加播放状态,首先是歌曲名称和时间 在index.html中添加 <div class="container fixed-bottom bg-white pb-4"> ...
- 用mciSendString做音乐播放器
音乐操作类 public class clsMCI { public clsMCI() { // // TODO: 在此处添加构造函数逻辑 // } //定义API函数使用的字符串变量 [Marsha ...
- C#调用VlcControl做一个播放器
开发环境: Visual Studio 2015 .Net Framework 4.5 1.新建一个Windows窗体应用程序 修改框架为.Net Framework 4.5 2.管理NuGet包 下 ...
- iOS播放器 - AVPlayer
之前有说到在播放器中一点点小技巧,现在正式记录一下AVPlayer. 这里主要是说明用AVPlayer做音乐播放器的功能实现,所以不介绍AVPlayer中那个图层类. 首先我们要声明一下播放器,这里有 ...
- Android音乐播放器的开发实例
本文将引导大家做一个音乐播放器,在做这个Android开发实例的过程中,能够帮助大家进一步熟悉和掌握学过的ListView和其他一些组件.为了有更好的学习效果,其中很多功能我们手动实现,例如音乐播放的 ...
- Android VLC播放器二次开发1——程序结构分析
最近因为一个新项目需要一个多媒体播放器,所以需要做个视频.音频.图片方面的播放器.也查阅了不少这方面的资料,如果要从头做一个播放器工作量太大了,而且难度也很大.所以最后选择了VLC作为基础,进行二次开 ...
随机推荐
- Spring Security教程系列(一)基础篇-1
第 1 章 一个简单的HelloWorld 第 1 章 一个简单的HelloWorld Spring Security中可以使用Acegi-1.x时代的普通配置方式,也可以使用从2.0时代才出现的命名 ...
- Interactive pivot tables with R(转)
I love interactive pivot tables. That is the number one reason why I keep using spreadsheet software ...
- 如何通过JS实现简单抖动效果
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...
- VR全景智慧城市:开启VR全景逛街新时代~
VR全景,又被称为3D实景,是一种新兴的富媒体技术,其与视频,声音,图片等传统的流媒体大的区别是"可操作,可交互". 对于顾客体验来说,VR确实是对于实体店是一种颠覆性的创新,它既 ...
- C# 类型基础(中)
前一篇文章中我们讲到了值类型和引用类型的一些区别,那这篇我们将深入的分析一下到底有什么不一样 先总结一下两者的差别: 黄金法则: 1.引用类型总是被分配到托管堆上. 2.值类型总是分配到它声明的地方: ...
- Pycon 2017: Python可视化库大全
本文首发于微信公众号“Python数据之道” 前言 本文主要摘录自 pycon 2017大会的一个演讲,同时结合自己的一些理解. pycon 2017的相关演讲主题是“The Python Visua ...
- python爬虫 模拟登陆校园网-初级
最近跟同学学习爬虫的时候看到网上有个帖子,好像是山大校园网不稳定,用py做了个模拟登陆很有趣,于是我走上了一条不归路..... 先上一张校园网截图 首先弄清一下模拟登陆的原理: 1:服务器判定浏览器登 ...
- Mac 上Python多版本切换
Mac上自带了Python2.x的版本,有时需要使用Python3.x版本做开发,但不能删了Python2.x,可能引起系统不稳定,那么就需要安装多个版本的Python. 1.安装Python3.x版 ...
- CSS3学习系列之选择器(四)
使用选择器来插入文字 css2中,使用before选择器在元素前面插入内容,使用after选择器在元素后面插入内容,在选择器的content属性中定义要插入的内容.将content属性值设定为none ...
- angular js 和 dajango 标签{{}} 冲突
问题描述: 如果在django的模板中使用{{ }},不会被angularjs 识别. 解决办法: >1.5 的django中,将需要angularjs解释的{{expression}}放在 v ...