HTML – ntzyz's blog https://archive.ntzyz.io Mon, 18 Sep 2017 12:11:28 +0000 zh-CN hourly 1 https://wordpress.org/?v=5.8 简易 Web Terminal 的实现 https://archive.ntzyz.io/2017/08/26/implementation-of-web-terminal/ https://archive.ntzyz.io/2017/08/26/implementation-of-web-terminal/#respond Sat, 26 Aug 2017 09:02:30 +0000 https://ntzyz.io/?p=848 继续阅读简易 Web Terminal 的实现]]> p.indent { text-indent: 2em; }

说起来也是有趣,本来是研究一下 WebSocket 准备给论坛/博客增加实时更新之类的特性,结果看着看着就脑洞大开搞了这么个玩意儿((

首先明确一下,这里说的 Web Terminal 是指再网页中实现的,类似于终端模拟器的玩意儿。举例的话应该是类似于 Linode 的 LiSH 和 Visual Studio Code 中内置的那个终端,而不是 ConoHa 提供的 VNC 式的终端(其实那玩意儿是个远程桌面了)。最终目标的效果就是和 Secure Shell 类似:打开一个网页,就能启动一个网页所在服务器的 shell,比如到处都有的 bash 或者非常强大的 zsh,然后就可以与这个终端进行交互式的操作,比如使用 vim 编辑文件,或者查阅 man 中的手册。

让我们从最简单的一些需求开始,如果只是需要远程执行一些命令或者脚本,那么我们只需要任何一个能调用系统 shell 的编程语言就行了。这里以 node 为例,代码很简单:

'use strict';
const express = require('express');
const child_process = require('child_process');
let server = express();

server.get('/eval', (req, res) => {
  if (req.query.cmd) {
    child_process.exec(req.query.cmd, (err, stdout, stderr) => {
      res.send({
        status: 'ok',
        stdout,
        stderr,
      });
    })
  } else {
    return res.send({
      status: 'ok';
    })
  }
});

server.listen(8123, 'localhost', () => {
 console.log(`Server running at http://localhost:8123`);
});

安装好 express 后,执行这段代码,打开另一个终端,执行代码:

curl 'http://localhost:8123/eval?command=ls'

我们就能看到 node 所在目录的文件列表了。看起来不错,但是如果需要执行面向终端的程序呢?

面向终端的程序,顾名思义,这种程序需要控制一个终端,同时有能力进行 job-control(fg 等) 和终端相关的信号(SIGINT 等)。典型的面向终端的程序就有:nano、vim、htop、less,等等。要让这些程序执行,我们需要通过 POSIX 的接口来创建一个伪终端(pseudoterminal,简称 pty)。

伪终端由两个虚拟的设备组成:一个 pseudoterminal master 和一个 pseudoterminal slave(pts)。这两个虚拟设备之间可以互相通讯,类似一个串口设备。两个进程可以打开这两个虚拟设备进行通讯,类似于一个管道。伪终端的关键就是 pts,这个设备在操作上和真实的终端设备(tty1, ttyS1, …)基本一致,不同之处在于 pts 没有速率之类的属性。所有能在 tty 上使用的操作都能在 pts 上使用,不支持的部分属性会被自动忽略,反正没什么卵用((

知道这些东西之后,终端模拟器的工作原理就很简单了:终端模拟器创建了一对 pty 设备,同时在 pty slave 上启动当前用户的默认 shell,比如`execlp(“/usr/bin/zsh”, [ “–login” ])`。pts 会将程序所有的输出发送给 pty master,终端模拟器在拿到这些数据后,再按照指定终端的标准将其输出。同时,所有的键盘输入也会发送给 pty slave。大致就是如下:

+----------+
| X Server |
+----+-----+
     |
+----+--------------+  +------------+
| Terminal Emulator +--+ pty master +
+-------------------+  +--+-----+---+  
                          |     |
                       +--+-----+--+  
                       + pty slave +
                       +--+-----+--+  
                          |     |
        +-----------------+-----+---+
        + Terminal-oriented program |
        +---------------------------+

Secure Shell 的远程登录的原理同样类似:ssh 客户端首先和 sshd 协商加密,互相认证,然后建立一个 SSH channel,由服务端创建一对 pty,然后将 pty master 的输出放到 SSH channel 中。ssh 客户端与服务端之间通过 SSH channel 通讯,便实现了远程登陆。

那么,Web Terminal 的实现思路就很明确了:在浏览器上,我们需要找到一个比较好用的终端框架(或者自己撸一个),在服务器上,我们需要一个当前程序语言与 ptmx 的接口(或者自己撸一个)。而通讯方面,SSH 用的是 TCP,Web 上能用的也就是 WebSocket 了(除非你想 XMLHttpRequest 然后疯狂刷新),这里能找到框架最好,全都自己撸就太累了(

嘛。虽然 npm 上坑爹的包非常多,但是在这种时候基本上还是能做到想要啥就有啥的。这里我选择了 xterm.js 作 HTML5 中的终端组件,node-pty 做服务端的 pty 操作工具。这两个也正是 Visual Studio Code 中内置的终端所采用的依赖。WebSocket 方面,我选择了 Socket.IO 这个框架。当然,为了让 ES6 Module 正常工作,我们还需要用
webpack 来处理。依靠着强大的 xterm.js 和 node-pty,需要我们来完成的工作非常少。以下晒代码:

服务端 JavaScript :

const express = require('express');
const site = express();
const http = require('http').Server(site);
const io = require('socket.io')(http);
const net = require('net');
const pty = require('node-pty');

site.use('/', express.static('.'));

io.on('connection', function (socket) {
  let ptyProcess = pty.spawn('bash', ['--login'], {
    name: 'xterm-color',
    cols: 80,
    rows: 24,
    cwd: process.env.HOME,
    env: process.env
  });
  ptyProcess.on('data', data => socket.emit('output', data));
  socket.on('input', data => ptyProcess.write(data));
  socket.on('resize', size => {
    console.log(size);
    ptyProcess.resize(size[0], size[1])
  });
});

http.listen(8123);

浏览器端 JavaScript:

import Terminal from 'xterm';
import 'xterm/src/xterm.css';
import io from 'socket.io-client';

Terminal.loadAddon('fit');

const socket = io(window.location.href);

const term = new Terminal({
  cols: 80,
  rows: 24,
});
term.open(document.getElementById('#terminal'));
term.on('resize', size => {
  socket.emit('resize', [size.cols, size.rows]);
})

term.on('data', data => socket.emit('input', data));

socket.on('output', arrayBuffer => {
  term.write(arrayBuffer);
});

window.addEventListener('resize', () => {
  term.fit()
});
term.fit()

Webpack 配置:

const path = require('path');

module.exports = {
    entry: "./src/entry.js",
    output: {
        path: path.join(__dirname, 'dist'),
        filename: "bundle.js"
    },
    module: {
        loaders: [
            { test: /\.css$/, loader: "style-loader!css-loader" }
        ]
    }
};

运行效果:

完整的代码可以参考 GitHub 上的 playground 仓库。

]]>
https://archive.ntzyz.io/2017/08/26/implementation-of-web-terminal/feed/ 0
单页应用下实现动态的脚本加载 https://archive.ntzyz.io/2017/08/13/dynamically-load-scripts-on-sap-page/ https://archive.ntzyz.io/2017/08/13/dynamically-load-scripts-on-sap-page/#respond Sat, 12 Aug 2017 16:33:23 +0000 https://ntzyz.io/?p=836 继续阅读单页应用下实现动态的脚本加载]]> p.indent { text-indent: 2em; }

之前在写自己的博客框架的时候遇到了一个问题:文章中的 `script` 标签没有任何作用,以 Vue 为 MVVM 框架举个例子:


  


这段代码的意思很简单,创建一个 ID 为 app 的 DIV,其中包含一个 p 标签,vm 实例化之后,p 的内容就是 data 字段的 html,渲染结束后效果应该是这样:


  

好吧……看起来这代码很不规矩,但是浏览器应该是接受在任意位置出现的 script 标签的,也就是按照预期,我们会在控制台里看到 JavaScript is awesome 这句话,没毛病。

然而实际结果是……并没有。原因很简单,v-html 是通过 innerHTML 来实现的,而 HTML5 的标准规定了通过 innerHTML 得到的 script 标签不会被执行。此举的目的是为了防止利用这个特性来进行 XSS 攻击,但是依然有不少场景需要使用这些脚本,所以我们需要一些 trick 来让浏览器加载这些脚本。与此同时,如果 script 是通过 appendChild 添加到 DOM,这段脚本是能被正常执行的。

所以解决方案也就很简单了:找到需要执行的 script 标签,然后通过 createElement 创建空 script,并将待执行标签的数据复制到空 script 中(比如 src,或者 innerHTML),最后通过 appendChild 将它添加到 DOM 中去。

其中,找到 script 标签的方法比较多,比如可以通过正则对字符串进行匹配。不过考虑到通过 innerHTML 设置的 script 标签仅仅只是没有执行,他们仍然在 DOM 树中,所以 querySelector 在鲁棒性(划掉)上就比正则高出一截,代码如下:


  

这样,我们就能顺利地在控制台里看到输出的字符串啦(

当然这样做也有他的缺点:

  1. 使用了全局的 JavaScript 环境。如果代码中出现了一些声明(比如用 let 定义了变量),或者是通过 new Audio() 之类的方式播放了音频,即使你在适当的时候将他们从 DOM 树中移除,他们产生的影响仍然将保留在当前的上下文中。我个人的解决方案是,使用一个 flag 来记录是否添加过 script,若添加过,则后续的路由操作将在 beforeEach 的钩子处被劫持,并将 fullPath 直接应用到 window.location.href 上,强制刷新当前页面,来获得一个全新的 JavaScript 上下文。
  2. 没有充分模拟 script 标签的特性。按照正常的网页加载模式,浏览器在加载和执行 script 标签时是阻塞的,顺序执行的。通过这种方式添加的 script 执行顺序并没有任何明确的规定,可能导致出现依赖上的问题。
]]>
https://archive.ntzyz.io/2017/08/13/dynamically-load-scripts-on-sap-page/feed/ 0
[HTML] AudioContext 折腾笔记 02 https://archive.ntzyz.io/2017/01/11/html-audiocontext-01-2/ https://archive.ntzyz.io/2017/01/11/html-audiocontext-01-2/#respond Wed, 11 Jan 2017 15:53:00 +0000 https://ntzyz.io/?p=763 继续阅读[HTML] AudioContext 折腾笔记 02]]> p.indent { text-indent: 2em; }

之前我们已经看到了如何利用 HTML 的 WebAudio API 播放一段自定义的音频,其实难度并不大,主要步骤就是创建音频上下文,创建缓冲区和音频源,之后在缓存区内暴力写入波形数据,就可以播放了。

有了这些功能,理论上我们是可以在浏览器里使用 JavaScript 解码任意的音频文件并播放的,然而实际上 JavaScript 本身算是个半残废语言,而且既然能用 HTMLAudio 标签来播放音频,为什么要自己解码呢(

不过话说回来,不是所有代码都是写了拿去用的,有的时候折腾折腾一些比较奇怪的东西也非常有趣。想到世界上有一个叫 WAV(audio/x-wave) 的音频格式,其结构听说也是非常的简单粗暴,在解码上基本没有什么复杂度。于是为什么不尝试着自己用 JavaScript 把 wav 音频通过音频上下文接口播放出来呢?

音频文件的加载和分析

这一段其实没什么好说的,走 XMLHttpRequest 请求一个 ArrayBuffer 基本就可以了,值得注意的是需要解决跨域问题(如果媒体文件和当前 URL 不在同一域下)。不过既然 Google Chrome 已经支持 Promise 了,就试试走 Promise 来封装一个 HTTP 客户端吧(x

  function $get(url) {
    return new Promise((resolve, reject) => {
      let xhr = new XMLHttpRequest();
      xhr.open('GET', url);
      xhr.responseType = 'arraybuffer';
      xhr.addEventListener('progress', (e) => {
        status.innerText = `Loading media ${(100 * e.loaded / e.total).toFixed(3)}% ...`;
      });
      xhr.onreadystatechange = () => {
        switch(xhr.readyState) {
        case xhr.DONE:
          resolve(xhr.response)
          break;
        default:
          break;
        }
      };
      xhr.send();
    });
  }

代码里的 status 是一个 span,用来实时显示 xhr 的加载进度的,毕竟 wav 这么大……

之后的代码里只要使用 $get(url).then(callback) 这样的形式就可以加载 wav 文件了。

wav 文件格式

嗯搞到文件之后就得开始解码了,在解码之前,必须先获得一些元数据例如采样率啊文件大小声道数什么的。

wav 格式本身是很简单的,首先得介绍一下 wav 文件里的整体描述单位:Chunk,其结构如下

struct wavChunk {
  unsigned char chunkName[4];
  unsigned int  chunkSize;
  unsigned char chunkData[];  // sizeof is chunkSize
}

知道了 Chunk 之后,剩下的就很好解释了:wav 文件本身就是一个名为 RIFF 的 Chunk,随后包含若干 Chunk 来表示一些元数据,比如 FMT Chunk 中描述了音频格式、声道数、采样率、采样位数,而 data Chunk 中就是音频的 PCM 数据。刚才提到的三个 Chunk 的结构都是固定的,剩下一些比如 LIST 之类的 Chunk 我们可以暂时先不管。

于是,一些分析前的准备:

      let rawFile = file;
      file = new Uint8Array(rawFile);

      // Reference for RIFF chunk and FMT chunk
      let RIFF = {
        RIFF:           { begin:    0, length:    4, type: 'CHAR'},
        ChunkSize:      { begin:    4, length:    4, type: 'INTEGER'},
        WAVE:           { begin:    8, length:    4, type: 'CHAR'},
      }
      let FMT = {
        fmt:            { begin:   12, length:    4, type: 'CHAR'},
        Subchunk1Size:  { begin:   16, length:    2, type: 'INTEGER'},
        AudioFormat:    { begin:   20, length:    2, type: 'INTEGER'},
        NumOfChan:      { begin:   22, length:    2, type: 'INTEGER'},
        SamplesPerSec:  { begin:   24, length:    4, type: 'INTEGER'},
        bytesPerSec:    { begin:   28, length:    4, type: 'INTEGER'},
        blockAlign:     { begin:   32, length:    2, type: 'INTEGER'},
        bitsPerSample:  { begin:   34, length:    2, type: 'INTEGER'},
      }
      
      file.subArray = (begin, length) => {
        return file.slice(begin, begin + length);
      };

      let readChar = (begin, length) => Array
        .from(file
          .subArray(begin, length))
        .map(ch => String.fromCharCode(ch))
        .join('');

      let readInt = (begin, length) => Array
        .from(file
          .subArray(begin, length))
        .reverse()
        .reduce((a, b) => a * 256 + b);

      let readChunk = (ref, save) => {
        for (let item in ref) {
          switch(ref[item].type) {
            case 'CHAR':
              save[item] = readChar(ref[item].begin, ref[item].length);
              break;
            case 'INTEGER':
              save[item] = readInt(ref[item].begin, ref[item].length);
              break;
          }
        }
      }

简单的描述一下吧,由于 JavaScript 这个破烂语言数据类型残缺,对于 uint32_t 类的数字我们需要自己准备办法读取,还好这是一个无符号整数,只要看下小端的定义就能轻松的将其转换为具体的数据。上面代码中的 readInt 就能完成这项需求。

接下来继续,开始读取 RIFF Chunk:

      // Read the RIFF chunk
      readChunk(RIFF, this);

这句没什么好说的,按照之前定义的 RIFF 变量,按格式读取数据。然后依次读取接下来的 Chunk,直到读到 data Chunk。其中,如果遇到 FMT Chunk 就需要按格式读取,剩下的就暂时先不管了吧。

      for (let offset = 12;;) {
        let chunkName = readChar(offset, 4); offset += 4;
        let chunkSize = readInt(offset, 4); offset += 4;
        this[chunkName] = {};
        this[chunkName].size = chunkSize;
        if (chunkName === 'fmt ') {
          // Read the RIFF chunk
          readChunk(FMT, this[chunkName]);
        }
        else {
          if (chunkName == 'data') {
            let type = `Int${this['fmt '].bitsPerSample}Array`;
            this[chunkName] = new window[type](rawFile.slice(offset, chunkSize))
            break;
          }
          // Read as raw
          this[chunkName]._rawData = file.subArray(offset, chunkSize);
        }
        offset += chunkSize;
      }

将以上部分放到 Wav 类的构造函数中去,构造完毕后,我们能得到这样的一个对象:

Wav {
 ChunkSize: 20045138,
 LIST: { ... },
 RIFF: 'RIFF',
 WAVE: 'WAVE',
 data: Int16Array[...],
 "fmt ": Object,
}

关键数据就在 fmt 和 data 里了:fmt 定义了如何去播放,data 则是待播放的内容。

初始化 AudioContext 和其他相关变量

这里还是比较简单的,将 fmt 区块里的数据拿出来就可以了,内容可以参考上一篇笔记:

      let ctx = this.ctx = new AudioContext();
      let framesCount = this.framesCount = wav.ChunkSize / (wav['fmt '].bitsPerSample / 8);

      let audioBuffer = this.audioBuffer = ctx.createBuffer(
        wav['fmt '].NumOfChan, 
        framesCount / 2, 
        wav['fmt '].SamplesPerSec
      );

      this.source = ctx.createBufferSource();
      this.source.loop = false;
      this.source.connect(ctx.destination);
      this.source.buffer = this.audioBuffer;

这里面少了一步:PCM 数据的填入。我是想着让音频一边播放一边填入的,可惜这样下来只有 Chrome 系浏览器能正常播放了)

边播放边填入本身是可以用 setInterval 实现,顺便趁机长试一波 ES6 的 yield 关键字:

    *step() {
      let ctx = this.ctx;
      let channelBuffering = [];
      for (let i = 0; i < wav['fmt '].NumOfChan; ++i) {
        channelBuffering[i] = this.audioBuffer.getChannelData(i);
      }

      for (let currentFrame = 0; currentFrame < this.framesCount; ++currentFrame) {
        channelBuffering[currentFrame % 2][Math.ceil(currentFrame / 2)]
          = Number(wav.data[currentFrame]) / (1 << (wav['fmt '].bitsPerSample - 1));

        if (!this.started) {
          setTimeout(() => {this.source.start()}, 10);
          this.started = true;
        }

        yield currentFrame;
      }
    }

将以上函数都怼到 class Wav 中后,只要在准备一个主函数就可以完成 wav 的解码和播放了。具体代码的话就不贴了,没什么好讨论的了(逃

最后附上一个运行效果:playground/wav-decode完整源代码,其中还加了点可视化的魔法,wav 的体积真的是槽点(

参考链接

  1. WAVE PCM soundfile format
  2. WAV – Wikipedia
]]>
https://archive.ntzyz.io/2017/01/11/html-audiocontext-01-2/feed/ 0
[HTML] AudioContext 折腾笔记 01 https://archive.ntzyz.io/2016/12/17/html-audiocontext-01/ https://archive.ntzyz.io/2016/12/17/html-audiocontext-01/#respond Sat, 17 Dec 2016 11:14:24 +0000 https://ntzyz.io/?p=753 继续阅读[HTML] AudioContext 折腾笔记 01]]> p.indent { text-indent: 2em; }

前天尝试了一波那个什么 MediaSourceExtension,结果发现那套API目前限制蛮大的,而且对我来说没什么帮助(audio/x-wav 完全不正常支持,audio/mpeg 也只能在 Chrome 上使用)于是只能放弃折腾了 QAQ

昨天突然想起之前写 nanoPlayer 的时候,使用了一个叫 Audio Context 的接口,nanoPlayer 用了这个 API 里的 createAnalyser 方法,来获得音频的频率数据,进而实现了一个频谱可视化功能。之前就注意到了这个接口中有个自定义 AudioBufferSource 的方法,可以指定若干 Float32Array 并交给浏览器播放,应该是蛮有意思的。

这里就实现一个可制定频率的正弦波音频吧,如果这个实现起来没有什么难度的话,就准备试试浏览器端解码 WAV 音频。

首先是 HTML,我们要准备一个文本框来获得指定的频率,两个按钮分别控制播放的开始与停止:



  
    
    
    
    
  

然后初始化若干变量:

let ctx = new AudioContext();
let frames = ctx.sampleRate;
let start = document.querySelector('button[start]');
let stop = document.querySelector('button[stop]');
let last = null;

其中 ctx.sampleRate 是 AudioContext 的采样率,这里直接将一秒钟的采样数作为帧数,产生一段持续时间为一秒的 AudioSourceBuffer 就足够了,还有 last 是用来保存上一次的 AudioSource,可以随时调用 stop 方法来终止播放。

然后就是为 start 按钮增加一个点击事件:根据文本框里的数值产生一段指定频率的音频采样:

start.onclick = (e) => {
  if (last) last.stop();

  let freq = document.querySelector('#freq').value;
  // 初始化一个单声道,采样率和 AudioCotnext 一致,持续时间为 1 sec 的 AudioBuffer
  let audioBuffer = ctx.createBuffer(1, ctx.sampleRate, ctx.sampleRate);
  // 获得其中第一个声道的数据源,类型是 Float32Array
  let nowBuffering = audioBuffer.getChannelData(0);
  // 填充数据,取值范围是 [-1, 1],直接用正弦函数就行了
  for (let i = 0; i < frames; ++i) {
    nowBuffering[i] = Math.sin((Math.PI * 2 * freq) * (i / ctx.sampleRate));
  }
  // 初始化一个 AudioBufferSource,并将上面的 AudioBuffer 与之绑定,并输出到 AudioContext
  let source = ctx.createBufferSource();
  source.loop = true;
  source.buffer = audioBuffer;
  source.connect(ctx.destination);
  // 开始回放
  source.start();
  // 保存以备后续使用
  last = source;
}

当然还需要对停止按钮绑定一个事件,来停止回放:

stop.onclick = (e) => {
  if (last) last.stop();
  last = null;
};

以上就是所有代码啦,效果的话,我在这里直接放个demo就行:


]]>
https://archive.ntzyz.io/2016/12/17/html-audiocontext-01/feed/ 0
[HTML] MediaSource 折腾笔记 01 https://archive.ntzyz.io/2016/12/16/html-fuck-mediasource-01/ https://archive.ntzyz.io/2016/12/16/html-fuck-mediasource-01/#respond Thu, 15 Dec 2016 17:26:26 +0000 https://ntzyz.io/?p=735 继续阅读[HTML] MediaSource 折腾笔记 01]]> p.indent { text-indent: 2em; }

MediaSource 是 HTML5 中的一个实验性特性,用于给 `HTMLMediaElement` 对象提供数据源。这里的数据源通常是使用 `XMLHttpRequest` 获得的数据。在 XHR 取得数据后,我们可以使用 JavaScript 对数据进行一些操作,比如提取 ID3、修改封装类型等等。 某 bilibili 开源的 Flv.js 就是使用这个特性实现的在 HTML5 环境下播放 H264 + AAC 格式的 FLV 视频。

最近在写 `Nano Player` 的时候遇到了两个比较尴尬的问题:1、ID3 信息必须手动指定;2、网易云音乐上下载的 MP3 由于某些原因必须缓冲 2MB 才能播放(详见MP3缓冲2MB才开始播放的解决方法)。于是想到是不是可以用这个 MediaSource 来暴改 MP3 呢?我也不知道

不管怎样,要用这个 MSE 来解决问题,首先就是要用它来播放文件(如果获得的数据直接喂给 MSE 都不能播放那还讨论个什么鬼)

创建若干变量并初始化:

let mediaSource = new MediaSource();
let audio = new Audio();
let media = 'audio.mp3';

audio.src = URL.createObjectURL(mediaSource);
audio.controls = 'controls';

然后是对 `sourceopen` 事件添加监听。当 `sourceopen` 事件触发后,我们需要创建一个 `sourceBuffer`,随后使用 `XMLHttpRequest` 获得媒体文件的内容,并附加到这个 `sourceBuffer` 内。当以上工作完成后,调用 `play` 方法播放音频:

mediaSource.addEventListener('sourceopen', () => {
  let sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');

  let xhr = new XMLHttpRequest();
  xhr.open('GET', media);
  xhr.responseType = 'arraybuffer';
  xhr.onreadystatechange = () => {
    if (xhr.readyState === xhr.DONE && xhr.status === 200) {
      sourceBuffer.addEventListener('updateend', () => {
        mediaSource.endOfStream();
        audio.play();
      });
      sourceBuffer.appendBuffer(xhr.response);
    }
  };
  xhr.send();
});

最后,为了方便检查,将 `audio` 添加到 `body` 中:

document.querySelector('body').appendChild(audio);

嗯,以上的这些代码就能实现一个本来只要三行代码就完成的工作了。具体效果可以看这里:MSE Playground 01

下面需要解决的问题是,为什么刚开始尝试使用的 M4A 格式无法正常播放,这个坑死人了我哪知道只能 MP3 啊浪费了两个多小时……


由于目前来看,MediaSource 的表现在各个浏览器中并不一致,同时支持的格式就目前而言还是十分有限,这个系列估计就这一篇了吧 23333(逃

参考链接:

  1. Media Source Extensions
  2. MediaSource – Web APIs | MDN
  3. Media Source Extensions for Audio
  4. HTML5 Media – types and codecs examples + canPlayType(type) test
  5. html5 video tag codecs attribute
  6. nickdesaulniers/netfix: Let’s build a Netflix
]]>
https://archive.ntzyz.io/2016/12/16/html-fuck-mediasource-01/feed/ 0
[WebGL] 更加灵活的描点实现 https://archive.ntzyz.io/2016/04/10/more-flexible-way-to-draw-a-point/ https://archive.ntzyz.io/2016/04/10/more-flexible-way-to-draw-a-point/#respond Sat, 09 Apr 2016 16:23:47 +0000 https://blog.dimension.moe/?p=412 继续阅读[WebGL] 更加灵活的描点实现]]> p.indent {text-indent: 2em;margin-top: 0.75em; margin-bottom: 0.75em;}

之前的代码实现了描绘一个巨大的、正方形的、红色的点,但是,稍有常识的人都能看出,那个点的颜色、位置、大小,都是无法在运行时改变的,这就比较尴尬了……

毫无疑问,Web 前端的编程语言是 ECMAScript,然而顶点着色器和片元着色器的控制用的却是 GLSL ES。为了能动态的修改着色器代码,我们就需要在着色器代码里添加需要动态编辑的变量,并且通过已有的方法来通过 JavaScript 获取并修改他们的值。

与之前相同,我们需要一个具有 canvas 的网页,其 ID 为 webgl。此处 HTML 代码可以直接参考之前的。同样我们也需要获取 WebGL 上下文,这些代码也是和之前一致的:

	var canvas = document.getElementById('webgl');
	var gl = getWebGLContext(canvas);

	if (!gl) {
		console.log('Error on getting context of WebGL');
		return;
	}

我们需要在着色器代码中添加相应的变量。其中,顶点着色器需要用 attribute 修饰符,是一个四维向量,而片元着色器则是使用的 uniform 修饰符,同样也是一个四维向量。片元着色器里的第一行 precision mediump float; 指明了数据精度:

	var VSHADER_SOURCE =
		'attribute vec4 a_Position;\n' +
		'void main() {\n' +
		'    gl_Position = a_Position;\n' +
		'    gl_PointSize = 10.0;\n' +
		'}\n';
	var FSHADER_SOURCE =
		'precision mediump float;\n' + 
		'uniform vec4 u_FragColor;\n' + 
		'void main() {\n' +
		'    gl_FragColor = u_FragColor;\n' +
		'}\n';

初始化这两个着色器。其实这里的工作不是调用一个函数这么简单。initShaders 函数是我使用的参考书中的库提供的,稍微看了下代码,似乎涉及了 shader 的编译与链接,还有程序对象的生成等内容。按照书本的意思,这些细节都会在后面详细说明,这里我也不去关注他们了。

	if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
		console.log('Error on initialize shaders.');
		return;
	}

嗯……接着就是使用 WebGL 提供的 getAttribLocation 函数和 getUniformLocation 函数,来获得那两个变量的位置。当获得位置出现错误时,函数将返回一个负值:

	var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
	if (a_Position < 0) {
		console.log('Error on getting the storage location of a_Position');
		return;
	}
	var u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
	if (u_FragColor < 0) {
		console.log('Error on getting the storage location of u_FragColor');
		return;
	}

这样下来,我们就可以直接的调用 gl.vertexAttrib3f 方法和 gl.uniform4f 方法,来动态的修改两个着色器的内容。为了体现出动态,做一个点到哪儿画到哪儿的页面,无疑是最简明的了。

因为涉及到了多个点的存储,我们需要新增加一个变量来保存已经单击过的点的位置,在 app.js 最开头增加一行:

	var points = new Array();

然后再写一个函数,来描绘 points 数组内的所有的点。其中,右上、左上、左下、右下的点的颜色依次为红、绿、蓝、白。

	function drawPoints() {
		gl.clearColor(0, 0 , 0, 1);
		gl.clear(gl.COLOR_BUFFER_BIT);
		for (var i = 0; i != points.length; ++i) {
			gl.vertexAttrib3f(a_Position, points[i].x, points[i].y, 0.0);
			if (points[i].x > 0 && points[i].y > 0)
				gl.uniform4f(u_FragColor, 1, 0, 0, 1);
			else if (points[i].x < 0 && points[i].y < 0)
				gl.uniform4f(u_FragColor, 0, 0, 1, 1);
			else if (points[i].x < 0)
				gl.uniform4f(u_FragColor, 0, 1, 0, 1);
			else
				gl.uniform4f(u_FragColor, 1, 1, 1, 1);
			gl.drawArrays(gl.POINT, 0, 1);
		}
	};

点的位置来源可以通过增加一个对 canvas 的 click 事件的监听,将获得的信息转换后 push 到 points 中。这里需要注意的就是 HTML 坐标与 GL 坐标的转换。说起来我还没说 WebGL 的默认坐标系呢(躺

想了想感觉还是没啥好说的,随手糊两张图应该就都能理解了……吧

名稱未設定 1

请叫我灵魂画师

然后就是代码了,理解了坐标系后转换应该也没啥难度了吧 pup

	canvas.onmousedown = function (e) => {
		var mx = e.clientX;
		var my = e.clientY;
		var rect = e.target.getBoundingClientRect();
		
		points.push({
			x: (2 * (mx - rect.left) - canvas.height) / canvas.height,
			y: (canvas.width - 2 * (my - rect.top)) / canvas.width
		});
		drawPoints();
	}

这样我们就做到了一个稍复杂但是更加灵活的描点网页了(

webgl2

]]>
https://archive.ntzyz.io/2016/04/10/more-flexible-way-to-draw-a-point/feed/ 0
[WebGL] 简单的 WebGL 描点实现 https://archive.ntzyz.io/2016/04/07/simple-point-drawing-program-in-webgl/ https://archive.ntzyz.io/2016/04/07/simple-point-drawing-program-in-webgl/#respond Thu, 07 Apr 2016 15:32:10 +0000 https://blog.dimension.moe/?p=391 继续阅读[WebGL] 简单的 WebGL 描点实现]]> p.indent {text-indent: 2em;margin-top: 0.75em; margin-bottom: 0.75em;}

相关库的地址:WebGLBook/lib at master

效果预览见最后。

注意:这只是个人的理解,不保证正确。如有错误欢迎指正!

首先准备具有一个画布(canvas)的页面,所有的 WebGL 程序都会通过这个 canvas 展现出来:



    
        
        Hello Canvas
    
    
        
            你可能是正版IE的受害者。
        
        
        
        
        
        
    

然后就是 app.js,即实现描点的相关代码。通过 DOM 操作,可以很容易地获得 canvas 的 DOM 节点,进而获得 WebGL 的上下文:

    var canvas = document.getElementById('webgl');
    var gl = getWebGLContext(canvas);
    if (!gl) {
        console.log('Error on getting context of WebGL');
        return;
    }

初始化完毕后,分别准备顶点着色器和片元着色器的GLSL ES代码。值得注意的是,JavaScript 是一个弱类型的语言,但 GLSL ES 不是,所以不能手滑将 0.0 写成 0,否则会出现类型错误。

在每一行 GLSL ES 代码后添加换行符 ‘\n’ 后可以在发生错误时输出行号,降低调试难度。当然,不写也是完全可以的。

    var VSHADER_SOURCE =
        'void main() {' +
        '    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);' +
        '    gl_PointSize = 10.0;' +
        '}';
    var FSHADER_SOURCE =
        'void main() {' +
        '    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);' +
        '}';

继续初始化 shader:

    if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
        console.log('Error on initialize shaders.');
        return;
    }

设置画布默认空颜色,并清空画布:

    gl.clearColor(0, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);

然后,描点:

    gl.drawArrays(gl.POINT, 0, 1);

简单说说我的理解吧。这些代码里获得 GL 上下文没啥好说的,主要的问题就在着色器(Shader)。此处一共出现了两个着色器:顶点着色器、片元着色器。描绘一个点所必须提供的参数就是点的位置与颜色,可以认为这里的顶点着色器指明了点的位置,而片元着色器则指明了其颜色。

顶点着色器代码 VSHADER_SOURCE 中有一个 main 函数,其中存在两个变量。其中 gl_Position 是一个四维向量,由四个浮点分量组成,是一个齐次坐标。其次坐标[x, y, z, w]等价于通常理解的[x/w, y/w, z/w]。使用齐次坐标进行三维处理可以获得更高的效率(?)。此处将 w 分量置为 1.0,即可将齐次坐标直接当成三位笛卡尔坐标使用。第二个变量 gl_PointSize,指明了点的大小。

片元着色器中只存在一个变量:gl_FragColor。它也是一个四维的矢量,其意义分别为红、绿、蓝与透明度的值。与常规的表示方法不同,GL 使用[0, 1]来表示单个颜色的强度,例如[0., 0., 0., 0.]表明了完全透明黑色,而[1., 1., 1., 1.]则表明完全不透明白色。

清空画布后,我们使用了 gl.drawArrays 来描点,其本质就是执行着色器代码。三个参数的意义分别为 描绘类型,从哪个点开始描绘,以及描绘的次数。当此函数被调用时,顶点着色器将会被执行 n 次,顶点着色器代码执行完毕后立即执行片元着色器。全部完成后,就可以看到灰色屏幕中的黑色画布里,描绘了一个红色的点。

顺便这哪里是点啊 这么大明明就是个正方形了嘛好伐)

效果预览~

QQ截图20160408000243

]]>
https://archive.ntzyz.io/2016/04/07/simple-point-drawing-program-in-webgl/feed/ 0