diff --git a/2023/09/26/media/history/index.html b/2023/09/26/media/history/index.html index d923046..61c7c63 100644 --- a/2023/09/26/media/history/index.html +++ b/2023/09/26/media/history/index.html @@ -1 +1 @@ -电影发展简史 - FredTsang

电影发展简史

Contents

  1. 在我们的生活中,电影和电视为我们提供了丰富多样的娱乐选择,但我们是否知道它们哪一个更早出现?
    1. 黑白无声电影
    2. 有声电影
    3. 彩色电影
    4. 宽银幕和3D电影

在我们的生活中,电影和电视为我们提供了丰富多样的娱乐选择,但我们是否知道它们哪一个更早出现?

电影比电视更早出现。

在1878年,摄影师迈布里奇接受了一项实验委托,拍摄《运动中的马》,目的是观察马在奔跑时四只腿是否会同时离开地面;在迈布里奇的帮助下,他利用相机连续拍照技术,将多张照片按时间顺序生成了一条连贯的照片带,最终确定奔跑的马在某个瞬间会有四腿同时离地的状态。这一结论打破了早期的观念,即马在奔跑时至少有一腿接触地面。

在这之后,有人将迈布里奇制作的照片带快速牵动,然后发现照片带中每张静止的马竟然“活”了起来,这件事引起了巨大轰动,并被迅速传开。他的这项研究对于摄影和运动的理解产生了重大影响,被认为是早期电影的先驱之一。

在1888年,法国发明家路易·普林斯拍摄了现今已知最早的短片——《朗德海花园场景》,影片时长只有2.11s。

黑白无声电影

1895年,兄弟雅克和奥古斯特·卢米埃尔(Lumière brothers)在法国发明了光影放映机,展示了世界上第一部商业化的电影。随后,黑白无声电影开始流行起来,包括乔治·梅利埃斯(Georges Méliès)的幻想电影。

为了提升观众体验,一些放映会伴随着管弦乐团的演奏家们演出、剧院风琴演奏、现场音效和来自表演人员或者放映员的解读。

有声电影

1927年,华纳兄弟公司(Warner Bros.)推出了第一部有声电影《爵士歌王》(The Jazz Singer),这标志着有声电影时代的开始。有声电影的出现大大改变了电影产业,并引发了一系列技术和艺术的创新。

彩色电影

20世纪30年代,彩色电影开始兴起。通过不同的技术和处理方法,电影可以以彩色形式呈现,增加了观众的视觉享受。

宽银幕和3D电影

20世纪50年代和60年代,宽银幕和3D电影开始流行。宽银幕技术提供了更广阔的画面,增强了电影的视觉冲击力。而3D技术则使观众能够获得更加身临其境的观影体验。

\ No newline at end of file +电影发展简史 - FredTsang

电影发展简史

Contents

  1. 在我们的生活中,电影和电视为我们提供了丰富多样的娱乐选择,但我们是否知道它们哪一个更早出现?
    1. 黑白无声电影
    2. 有声电影
    3. 彩色电影
    4. 宽银幕和3D电影

在我们的生活中,电影和电视为我们提供了丰富多样的娱乐选择,但我们是否知道它们哪一个更早出现?

电影比电视更早出现。

在1878年,摄影师迈布里奇接受了一项实验委托,拍摄《运动中的马》,目的是观察马在奔跑时四只腿是否会同时离开地面;在迈布里奇的帮助下,他利用相机连续拍照技术,将多张照片按时间顺序生成了一条连贯的照片带,最终确定奔跑的马在某个瞬间会有四腿同时离地的状态。这一结论打破了早期的观念,即马在奔跑时至少有一腿接触地面。

在这之后,有人将迈布里奇制作的照片带快速牵动,然后发现照片带中每张静止的马竟然“活”了起来,这件事引起了巨大轰动,并被迅速传开。他的这项研究对于摄影和运动的理解产生了重大影响,被认为是早期电影的先驱之一。

在1888年,法国发明家路易·普林斯拍摄了现今已知最早的短片——《朗德海花园场景》,影片时长只有2.11s。

黑白无声电影

1895年,兄弟雅克和奥古斯特·卢米埃尔(Lumière brothers)在法国发明了光影放映机,展示了世界上第一部商业化的电影。随后,黑白无声电影开始流行起来,包括乔治·梅利埃斯(Georges Méliès)的幻想电影。

为了提升观众体验,一些放映会伴随着管弦乐团的演奏家们演出、剧院风琴演奏、现场音效和来自表演人员或者放映员的解读。

有声电影

1927年,华纳兄弟公司(Warner Bros.)推出了第一部有声电影《爵士歌王》(The Jazz Singer),这标志着有声电影时代的开始。有声电影的出现大大改变了电影产业,并引发了一系列技术和艺术的创新。

彩色电影

20世纪30年代,彩色电影开始兴起。通过不同的技术和处理方法,电影可以以彩色形式呈现,增加了观众的视觉享受。

宽银幕和3D电影

20世纪50年代和60年代,宽银幕和3D电影开始流行。宽银幕技术提供了更广阔的画面,增强了电影的视觉冲击力。而3D技术则使观众能够获得更加身临其境的观影体验。

\ No newline at end of file diff --git a/index.html b/index.html index 69876e0..577f6ac 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ -FredTsang

电影发展简史

在我们的生活中,电影和电视为我们提供了丰富多样的娱乐选择,但我们是否知道它们哪一个更早出现?

电影比电视更早出现。

在1878年,摄影师迈布里奇接受了一项实验委托,拍摄《运动中的马》,目的是观察马在奔跑时四只腿是否会同时离开地面;在迈布里奇的帮助下,他利用相机连续拍照技术,将多张照片按时间顺序生成了一条连贯的照片带,最终确定奔跑的马在某个瞬间会有四腿同时离地的状态。这一结论打破了早期的观念,即马在奔跑时至少有一腿接触地面。

在这之后,有人将迈布里奇制作的照片带快速牵动,然后发现照片带中每张静止的马竟然“活”了起来,这件事引起了巨大轰动,并被迅速传开。他的这项研究对于摄影和运动的理解产生了重大影响,被认为是早期电影的先驱之一。

在1888年,法国发明家路易·普林斯拍摄了现今已知最早的短片——《朗德海花园场景》,影片时长只有2.11s。

黑白无声电影

1895年,兄弟雅克和奥古斯特·卢米埃尔(Lumière brothers)在法国发明了光影放映机,展示了世界上第一部商业化的电影。随后,黑白无声电影开始流行起来,包括乔治·梅利埃斯(Georges Méliès)的幻想电影。

为了提升观众体验,一些放映会伴随着管弦乐团的演奏家们演出、剧院风琴演奏、现场音效和来自表演人员或者放映员的解读。

有声电影

1927年,华纳兄弟公司(Warner Bros.)推出了第一部有声电影《爵士歌王》(The Jazz Singer),这标志着有声电影时代的开始。有声电影的出现大大改变了电影产业,并引发了一系列技术和艺术的创新。

彩色电影

20世纪30年代,彩色电影开始兴起。通过不同的技术和处理方法,电影可以以彩色形式呈现,增加了观众的视觉享受。

宽银幕和3D电影

20世纪50年代和60年代,宽银幕和3D电影开始流行。宽银幕技术提供了更广阔的画面,增强了电影的视觉冲击力。而3D技术则使观众能够获得更加身临其境的观影体验。

了解 flv

一、Web 如何播放 MP4?

二、如何在 Web 播放 flv?

三、了解 flv 格式

玩转 flv.js

1. 如何极致实践低延迟?

2. 如何优雅断线重连?

3. 如何同时播放多路flv?

如何分析 flv

1. 保留现场

在定位 flv 直播问题时(由于直播场景的特殊性,出问题的流可能转瞬即逝),我们首先需要将流保存下来,以供后续的问题定位和修复验证;

我们可以在命令行中使用 curl 命令,将 flv 流以文件形式保存下来:

1
curl https://xxx.com/test.flv -o test.flv

2. 问题初判

在对保存下来的 flv 文件深入分析之前,我们可以使用如 ffplay, ffprobe 等工具初步分析/播放一下 flv 文件,看看这些工具能否给我们提供一些有用的信息:

2.1 使用 ffplay

1
ffplay -hide_banner -autoexit -loglevel level -report -i test.flv
  • -hide_banner 在控制台中不打印 ffplay 的 banner 信息
  • -autoexit 播放结束后自动关闭播放窗口
  • -loglevel level 在控制台和日志文件输出日志时,带上日志等级(如:[error], [warning], [debug], [verbose]
  • -report 输出日志文件
  • -i 后面接视频文件的相对路径 or 绝对路径

更多参数:https://ffmpeg.org/ffplay.html

2.2 使用 ffprobe

1
ffprobe -loglevel level -hide_banner -unit -show_frames -select_streams v -show_error -of ini -i test.flv > test.ini 2>&1
  • -unit 输出的信息中带上单位
  • -select_streams v 只查看视频轨(v)信息
  • -show_frames 查看每一帧信息
  • -show_error 打印错误
  • -of ini.ini格式输出结果
  • > test.ini 2>&1 使用管道符将结果及错误信息都输出到 test.ini 文件中

更多参数:https://ffmpeg.org/ffprobe.html

3. flv分析

ffplay 和 ffprobe 只能告诉我们哪个时间点,或者哪一帧出了问题,但具体是什么问题还是需要我们深入分析 flv 流/文件;

此处推荐使用 flvAnalyser,一个可视化的 flv 分析工具;该工具支持 H.264 和 H.265,可以查看到具体的 nalu;

如何压测流媒体服务器?

介绍一些用于压测流媒体服务器的工具:

st-load

SRS是一个开源的实时视频服务器,它可以支持RTMP、WebRTC、HLS、HTTP-FLV、SRT等多种实时流媒体协议;

SRS的作者为了能更好的验证SRS服务器的流媒体转发性能,同时提供了专门的测试套件 st-load

该套件中提供了:

  • sb_hls_load 支持HLS点播和直播
  • sb_http_load 支持HTTP负载测试,所有并发重复下载一个http文件
  • sb_rtmp_load 支持RTMP流播放测试,一个进程支持5k并发
  • sb_rtmp_publish 支持RTMP流推流测试,一个进程支持500个并发

该套件需要在 linux 下运行,它会模拟客户端行为拉流。

使用方法

Build from source, then run RTMP benchmark:

1
2
3
git clone https://github.com/ossrs/srs-bench.git &&
cd srs-bench && ./configure && make &&
./objs/sb_rtmp_load -c 1 -r rtmp://127.0.0.1:1935/live/livestream

Or directly by docker:

1
2
docker run --rm -it --network=host --name sb ossrs/srs:sb \
./objs/sb_rtmp_load -c 1 -r rtmp://127.0.0.1:1935/live/livestream

For HTTP-FLV benchmark:

1
2
docker run --rm -it --network=host --name sb ossrs/srs:sb \
./objs/sb_http_load -c 1 -r http://127.0.0.1:8080/live/livestream.flv

For HLS benchmark:

1
2
docker run --rm -it --network=host --name sb ossrs/srs:sb \
./objs/sb_hls_load -c 1 -r http://127.0.0.1:8080/live/livestream.m3u8

Or from Aliyun mirror:

1
2
3
docker run --rm -it --network=host --name sb \
registry.cn-hangzhou.aliyuncs.com/ossrs/srs:sb \
./objs/sb_rtmp_load -c 1 -r rtmp://127.0.0.1:1935/live/livestream

Note: Please use docker kill sb to stop it.

使用 ./objs/sb_rtmp_load or ./objs/sb_hls_load or ./objs/sb_http_load 去拉不同协议的流,-c 表示负载数

flazr

flazr 是 RTMP 的一个 java 实现,这个项目提供了一个流媒体服务器和相关的工具类;

该工具可以在 windows 上运行,我们可以使用它来压测 rtmp 拉流;

下载地址:https://sourceforge.net/projects/flazr/files/flazr/

JMeter

使用 JMeter 的BlazeMeter-HLS Plugin,可以压测 hls 播放

参考:https://zhuanlan.zhihu.com/p/624834220

npm 命令速记

  • 查看 npm 包所有版本
1
npm view <package_name> versions

iOS webview 调试

  1. 在 iOS 设备上,依次打开 “设置” -> “Safari浏览器” -> “高级” -> 开启 “JavaScript” 和 “网页检查器”

  1. 在 Mac 设备上,显示 Safari 浏览器的 “开发” 菜单

  1. 将 iOS 设备连接到对应的 Mac 设备上,并在 iOS 设备上打开对应的 webview 页面,此时能在 Safari 浏览器的 “开发” 菜单中看到可调试的 webview 页面,选中对应的页面即可

如何调试 WebRTC / 媒体播放?

在 Safari 的网页检查器中,打开 “媒体日志记录”、“MSE日志记录”、“WebRTC日志记录” 设置

清理前端项目中未被使用到的文件

一个项目经过多次迭代后,不可避免的会残留一些未被使用到/多余的文件,人为的去分析文件的引用情况是一种费时、费力的行为;
使用 unused-files-webpack-plugin webpack 插件可以快速的找出哪些文件未被使用到。

安装:

1
npm i --save-dev unused-files-webpack-plugin

or

1
yarn add --dev unused-files-webpack-plugin

配置:

1
2
3
4
5
6
7
8
9
const { UnusedFilesWebpackPlugin } = require("unused-files-webpack-plugin");

module.exports = {
plugins: [
new UnusedFilesWebpackPlugin({
patterns: ['src/**/*.*']
}),
],
};

node.js 图片压缩

1
npm install -D images imagemin imagemin-mozjpeg imagemin-pngquant slash get-all-files
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import images from 'images';
import imagemin from 'imagemin';
import imageminMozjpeg from 'imagemin-mozjpeg';
import imageminPngquant from 'imagemin-pngquant';
import fsPromises from 'fs/promises';
import path from 'path';
import convertToUnixPath from 'slash';
import { getAllFiles } from 'get-all-files';

const handleFile = async (sourcePath) => {
// 如果图片 width > 750,等比缩小图片宽高
if (images(sourcePath).width() > 375 * 2) {
const fileType = path.extname(sourcePath).replace('.', '').toLowerCase();

const buffer = await imagemin.buffer(
images(sourcePath)
.size(375 * 2) // 调整图片的宽度,高度会等比缩小
.toBuffer(fileType),
{
plugins: [
imageminMozjpeg({ quality: 70 }),
imageminPngquant({
quality: [0.6, 0.8],
}),
],
}
);

const destinationPath = path.join('./build', path.basename(sourcePath).toLowerCase());

await fsPromises.mkdir(path.dirname(destinationPath), { recursive: true });

await fsPromises.writeFile(destinationPath, buffer);
return;
}

// --- 纯粹的图片大小压缩 ---
await imagemin([sourcePath], {
destination: 'build', // 结果输出到 build 目录
plugins: [
imageminMozjpeg({ quality: 70 }), // 压缩 jpg
imageminPngquant({
quality: [0.6, 0.8],
}), // 压缩 png
],
});
};

// 递归获取 images 目录下所有文件
getAllFiles('./images')
.toArray()
.then((paths) => {
return paths
.map((it) => convertToUnixPath(it)) // 将 Windows 的 "\\" 路径转为 "/"
.filter((it) => {
const ext = path.extname(it).toLowerCase();

return ['.jpg', '.jpeg', '.png'].includes(ext); // 筛选出 .jpg, .jpeg, .png 后缀的文件
});
})
.then((paths) => {
return paths.map(async (p) => {
try {
return await handleFile(p);
} catch (err) {
throw err;
}
});
});

UmiJS

图片压缩

打包时使用 image-webpack-loader 压缩图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { defineConfig } from 'umi';

export default defineConfig({
chainWebpack(memo, { env, webpack, createCSSRule }) {
// 在默认的 images 规则上,添加 image-webpack-loader 来压缩图片
memo.module
.rule('images')
.use('image-webpack-loader')
.loader(require.resolve('image-webpack-loader'))
.options({
options: {
bypassOnDebug: true, // webpack@1.x
disable: true, // webpack@2.x and newer
},
});
},
});

Webpack 杂记

  1. 关于为什么使用 await import('@/common/themes/' + window.mode) 时,会把 @/common/themes 目录下的其他文件(如,markdown 文件)也打包进 chunk!

根据 webpack官方文档 的解释:

当我们在使用 import()require() 时,若其标识符参数中含有表达式,由于这种引入形式不能在打包编译阶段确定标识符参数的值,导致无法做静态分析、构建依赖图,因此会自动创建一个上下文(context);

webpack 会解析require()调用中的字符串,提取出如下的一些信息,供 context 使用:

1
2
3
4
从 '@/common/themes/' + window.mode 中提取出:

Directory:@/common/themes
Regular expression: /^.*/

而后,webpack 会将目录(@/common/themes)下所有符合正则表达式(/^.*/)的模块都打包到一起;因此这种方式,可能会引入一些不必要的模块。

对于require()的引入方式,可通过设置require.context()解决;对于import()方式,可以使用 webpack 的内联注释解决该问题。

  1. less webpack 配置

使用 less-loader 预编译 *.less 文件,而后使用 cssnano postcss 插件压缩 css,使用 autoprefixer 插件兼容各浏览器 css prefix。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.less$/,
exclude: /node_modules/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{
loader: 'postcss-loader',
options: {
postcssOptions: process.env.NODE_ENV === 'production' ? {
plugins: [
require('cssnano')({
preset: 'default',
}),
'autoprefixer'
]
} : undefined,
}
},
{ loader: 'less-loader' }
]
}
]
}
};

二分查找

基本的二分搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// nums 递增
let binarySearch = function (nums, target) {
let left = 0;
let right = nums.length - 1;

while (left <= right) {
let mid = left + ((right - left) >> 1);

if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}

return -1;
};

因为我们是要查找一个数存不存在,所以当 left == right 时,我们也应该继续执行 while 查找。

寻找左侧边界的二分搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let searchLeftBound = function (nums, target) {
if (nums.length == 0) return -1;
let left = 0;
let right = nums.length;

while (left < right) {
let mid = left + ((right - left) >> 1);

if (nums[mid] == target) {
right = mid;
} else if (nums[mid] > target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
}
}

return nums[left] == target ? left : -1;
};

对于 nums = [1, 2, 2, 2, 3]target = 2 而言,left 的结果为 1,可以理解为 nums 中小于 2 的个数有 1 个。
target 大于 nums 所有数时,left 的结果为 nums.length;此时 nums[left]nums[nums.length]undefined,必然不等于 target

寻找右侧边界的二分搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let searchRightBound = function (nums, target) {
let left = 0;
let right = nums.length - 1;

while (left <= right) {
let mid = left + ((right - left) >> 1);

if (nums[mid] == target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
}
}

if (right < 0 || nums[right] != target) return -1;

return right;
};

nums 为非递减顺序时,right < 0 表示 target 比所有元素都小,即算法不断在执行 right = mid - 1

JavaScript 算法常用语法

  1. 除2取整
1
2
3
4
let num = 5;

// let res = Math.floor(num / 2);
let res = num >> 1;
  1. 拼接字符串 String.prototype.concat()
1
2
3
4
5
6
7
8
var s1 = 'abc';
var s2 = 'def';

s1.concat(s2) // "abcdef"
s1 // "abc"

// 可以接受多个参数
'a'.concat('b', 'c') // "abc"
  1. 截取子字符串 String.prototype.slice()

slice 方法用于从原字符串取出子字符串并返回,不改变原字符串。它的第一个参数是子字符串的开始位置,第二个参数是子字符串的结束位置(不含该位置)。

1
'JavaScript'.slice(0, 4) // "Java"

如果省略第二个参数,则表示子字符串一直到原字符串结束。

1
'JavaScript'.slice(4) // "Script"

如果参数是负值,表示从结尾开始倒数计算的位置,即该负值加上字符串长度。

1
2
3
'JavaScript'.slice(-6) // "Script"
'JavaScript'.slice(0, -6) // "Java"
'JavaScript'.slice(-2, -1) // "p"
  1. Unicode 字符串比较

字符串按照字典顺序进行比较。

1
2
3
4
5
6
7
'a' > 'z' // false

'abc' < 'abd' // true

'abc' < 'abcd' // true

'a0b1' < 'a1b1' // true
  1. 截取子数组 Array.prototype.slice()

slice 方法用于提取目标数组的一部分,返回一个新数组,原数组不变。
第一个参数为起始位置(从0开始),第二个参数为终止位置(但该位置的元素本身不包括在内)。
如果省略第二个参数,则一直返回到原数组的最后一个成员。

1
2
3
4
5
6
7
var a = ['a', 'b', 'c'];

a.slice(0) // ["a", "b", "c"]
a.slice(1) // ["b", "c"]
a.slice(1, 2) // ["b"]
a.slice(2, 6) // ["c"]
a.slice() // ["a", "b", "c"]

如果 slice 方法的参数是负数,则表示倒数计算的位置。

1
2
3
var a = ['a', 'b', 'c'];
a.slice(-2) // ["b", "c"]
a.slice(-2, -1) // ["b"]
  1. 删除原数组元素 Array.prototype.splice()

splice 方法用于删除原数组的一部分成员,并可以在删除的位置添加新的数组成员,返回值是被删除的元素。注意,该方法会改变原数组。

splice 的第一个参数是删除的起始位置(从0开始),第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素。

1
2
3
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(4, 2) // ["e", "f"]
a // ["a", "b", "c", "d"]
  1. 数组排序 Array.prototype.sort()

sort 方法对数组成员进行排序,默认是按照字典顺序排序。排序后,原数组将被改变

1
2
3
4
5
var arr = ['d', 'c', 'b', 'a'];

arr.sort(); // ['a', 'b', 'c', 'd']

arr // ['a', 'b', 'c', 'd']
  • number[] 升序排序
1
numbers.sort((a, b) => a - b);
  • number[] 降序排序
1
numbers.sort((a, b) => b - a);
  1. Array.prototype.reverse()

reverse 方法用于颠倒排列数组元素,返回改变后的数组。注意,该方法将改变原数组

1
2
3
4
var a = ['a', 'b', 'c'];

a.reverse() // ["c", "b", "a"]
a // ["c", "b", "a"]
  1. 合并数组 Array.prototype.concat()

concat 方法用于多个数组的合并。它将新数组的成员,添加到原数组成员的后部,然后返回一个新数组,原数组不变

1
2
3
4
5
6
7
let arr = ['hello'];

arr.concat(['world'], ['!']) // ["hello", "world", "!"]

arr // ['hello']

[2].concat({a: 1}, {b: 2}) // [2, {a: 1}, {b: 2}]
  1. 易混淆知识点
  • inhasOwnProperty
1
2
3
4
5
var obj = {};

'toString' in obj // true

obj.hasOwnProperty('toString') // false
  • typeof
1
2
3
4
5
6
7
typeof 1 // 'number'
typeof false // 'boolean'
typeof 'abbc' // 'string'
typeof [] // 'object'
typeof null // 'object'
typeof function() {} // 'function'
typeof undefined // 'undefined'

Media PlayType Support Detect

online demo

Support built-in HLS

Based on flv.js.

1
2
3
4
5
6
7
function supportNativeMediaPlayback(mimeType: string) {
const videoElement = window.document.createElement('video');
let canPlay = videoElement.canPlayType(mimeType);
return canPlay === 'probably' || canPlay == 'maybe';
}

console.log('Support built-in HLS:', supportNativeMediaPlayback('application/vnd.apple.mpegurl'));

Support MSE H264

Based on hls.js.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// check MediaSource support
function getMediaSource(): typeof MediaSource | undefined {
return self.MediaSource || ((self as any).WebKitMediaSource as MediaSource);
}

// check SourceBuffer support
function getSourceBuffer(): typeof self.SourceBuffer {
return self.SourceBuffer || (self as any).WebKitSourceBuffer;
}

function isSupported(): boolean {
const mediaSource = getMediaSource();
if (!mediaSource) {
return false;
}
const sourceBuffer = getSourceBuffer();
// Check H264 support
const isTypeSupported =
mediaSource &&
typeof mediaSource.isTypeSupported === 'function' &&
mediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');

// if SourceBuffer is exposed ensure its API is valid
// safari and old version of Chrome doe not expose SourceBuffer globally so checking SourceBuffer.prototype is impossible
const sourceBufferValidAPI =
!sourceBuffer ||
(sourceBuffer.prototype &&
typeof sourceBuffer.prototype.appendBuffer === 'function' &&
typeof sourceBuffer.prototype.remove === 'function');
return !!isTypeSupported && !!sourceBufferValidAPI;
}

console.log('Support MediaSource:', !!getMediaSource());
console.log('Support SourceBuffer:', !!getSourceBuffer());
console.log('Support MSE & H264:', !!isSupported());

Convert TSConfig paths to Webpack alias

Inspired by marzelin/convert-tsconfig-paths-to-webpack-aliases.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const fs = require('fs');
const path = require('path');
const get = require('lodash/get');
const JSON5 = require('json5');

/**
* convert tsconfig.json paths to webpack alias
*/
const convertTSConfigPaths2Aliases = () => {
const tsConfigPath = path.resolve(process.cwd(), './tsconfig.json');

try {
if (fs.existsSync(tsConfigPath)) {
// dealing with json file with comments, we use json5 to parse tsconfig.json instead of requiring directly
const tsConfig = JSON5.parse(fs.readFileSync(tsConfigPath));
let { baseUrl, paths } = get(tsConfig, 'compilerOptions', {});

if (paths) {
baseUrl = baseUrl ? path.resolve(process.cwd(), baseUrl) : process.cwd();

const replaceGlobs = (path) => path.replace(/(\/\*\*)*\/\*$/, '');

return Object.keys(paths).reduce((aliases, pathName) => {
const alias = replaceGlobs(pathName);
const aliasPath = replaceGlobs(paths[pathName][0]);
aliases[alias] = path.resolve(baseUrl, aliasPath);
return aliases;
}, {});
}
}
} catch (err) {
console.error(err);
return {};
}

return {};
};

Usage

1
2
3
4
5
6
7
8
9
// webpack.js

module.exports = {
resolve: {
alias: {
...convertTSConfigPaths2Aliases(),
}
}
};

使用 GitHub Actions 自动化构建/部署 GitHub Page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# .github/workflows/deploy.yml
name: Deploy Blog
on:
push:
branches:
- master # 当有 commit 被 push 到 master 分支时,执行下面的 jobs
jobs:
deploy: # job name
runs-on: macos-latest # jobs 运行在 macos 上
steps:
- name: Check out Git repository
uses: actions/checkout@v2 # 拉取当前最新代码

- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v2 # 初始化 node 环境
with:
node-version: '12'

- name: Cache NPM dependencies
uses: actions/cache@v2 # 缓存 ~/.npm 内的 npm 包,以免每次执行 jobs 的时候都需要重新下载
with:
path: ~/.npm
key: ${{ runner.os }}-npm-cache
restore-keys: |
${{ runner.os }}-npm-cache

- name: Build # 安装 npm 包 & 执行 hexo 的打包命令(不使用 hexo 的 deploy 命令,而是通过下一个 step 来部署)
run: |
npm install
npm run build-only
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}

- name: Deploy
uses: peaceiris/actions-gh-pages@v3 # hexo 构建后的文件在 public 目录下,将该目录下的文件推送到另一个仓库
with:
personal_token: ${{ secrets.ACCESS_TOKEN }} # 由于推送到另一个仓库需要权限,所以需要创建/配置一个 personal access token
external_repository: FredZeng/FredZeng.github.io # 如果是推送到同一个仓库,就可以不用写这个
publish_dir: ./public
publish_branch: master

配置

  1. Creating a personal access token 创建自己的 access token,一般勾选上 repo 就可以了;请务必复制,保存好生成的 token
  2. Encrypted secrets 将创建好的 token 添加到项目的 Actions secrets 中,这样你才能在 action 里面用到上述的 secrets.ACCESS_TOKEN
  3. 🎉🎉🎉 你已经完成了所有前置步骤,可以享受自动化部署了

其他

其他方案可参考官方文档 将 Hexo 部署到 GitHub Pages 实现自动化部署。

\ No newline at end of file +FredTsang

电影发展简史

在我们的生活中,电影和电视为我们提供了丰富多样的娱乐选择,但我们是否知道它们哪一个更早出现?

电影比电视更早出现。

在1878年,摄影师迈布里奇接受了一项实验委托,拍摄《运动中的马》,目的是观察马在奔跑时四只腿是否会同时离开地面;在迈布里奇的帮助下,他利用相机连续拍照技术,将多张照片按时间顺序生成了一条连贯的照片带,最终确定奔跑的马在某个瞬间会有四腿同时离地的状态。这一结论打破了早期的观念,即马在奔跑时至少有一腿接触地面。

在这之后,有人将迈布里奇制作的照片带快速牵动,然后发现照片带中每张静止的马竟然“活”了起来,这件事引起了巨大轰动,并被迅速传开。他的这项研究对于摄影和运动的理解产生了重大影响,被认为是早期电影的先驱之一。

在1888年,法国发明家路易·普林斯拍摄了现今已知最早的短片——《朗德海花园场景》,影片时长只有2.11s。

黑白无声电影

1895年,兄弟雅克和奥古斯特·卢米埃尔(Lumière brothers)在法国发明了光影放映机,展示了世界上第一部商业化的电影。随后,黑白无声电影开始流行起来,包括乔治·梅利埃斯(Georges Méliès)的幻想电影。

为了提升观众体验,一些放映会伴随着管弦乐团的演奏家们演出、剧院风琴演奏、现场音效和来自表演人员或者放映员的解读。

有声电影

1927年,华纳兄弟公司(Warner Bros.)推出了第一部有声电影《爵士歌王》(The Jazz Singer),这标志着有声电影时代的开始。有声电影的出现大大改变了电影产业,并引发了一系列技术和艺术的创新。

彩色电影

20世纪30年代,彩色电影开始兴起。通过不同的技术和处理方法,电影可以以彩色形式呈现,增加了观众的视觉享受。

宽银幕和3D电影

20世纪50年代和60年代,宽银幕和3D电影开始流行。宽银幕技术提供了更广阔的画面,增强了电影的视觉冲击力。而3D技术则使观众能够获得更加身临其境的观影体验。

了解 flv

一、Web 如何播放 MP4?

二、如何在 Web 播放 flv?

三、了解 flv 格式

玩转 flv.js

1. 如何极致实践低延迟?

2. 如何优雅断线重连?

3. 如何同时播放多路flv?

如何分析 flv

1. 保留现场

在定位 flv 直播问题时(由于直播场景的特殊性,出问题的流可能转瞬即逝),我们首先需要将流保存下来,以供后续的问题定位和修复验证;

我们可以在命令行中使用 curl 命令,将 flv 流以文件形式保存下来:

1
curl https://xxx.com/test.flv -o test.flv

2. 问题初判

在对保存下来的 flv 文件深入分析之前,我们可以使用如 ffplay, ffprobe 等工具初步分析/播放一下 flv 文件,看看这些工具能否给我们提供一些有用的信息:

2.1 使用 ffplay

1
ffplay -hide_banner -autoexit -loglevel level -report -i test.flv
  • -hide_banner 在控制台中不打印 ffplay 的 banner 信息
  • -autoexit 播放结束后自动关闭播放窗口
  • -loglevel level 在控制台和日志文件输出日志时,带上日志等级(如:[error], [warning], [debug], [verbose]
  • -report 输出日志文件
  • -i 后面接视频文件的相对路径 or 绝对路径

更多参数:https://ffmpeg.org/ffplay.html

2.2 使用 ffprobe

1
ffprobe -loglevel level -hide_banner -unit -show_frames -select_streams v -show_error -of ini -i test.flv > test.ini 2>&1
  • -unit 输出的信息中带上单位
  • -select_streams v 只查看视频轨(v)信息
  • -show_frames 查看每一帧信息
  • -show_error 打印错误
  • -of ini.ini格式输出结果
  • > test.ini 2>&1 使用管道符将结果及错误信息都输出到 test.ini 文件中

更多参数:https://ffmpeg.org/ffprobe.html

3. flv分析

ffplay 和 ffprobe 只能告诉我们哪个时间点,或者哪一帧出了问题,但具体是什么问题还是需要我们深入分析 flv 流/文件;

此处推荐使用 flvAnalyser,一个可视化的 flv 分析工具;该工具支持 H.264 和 H.265,可以查看到具体的 nalu;

如何压测流媒体服务器?

介绍一些用于压测流媒体服务器的工具:

st-load

SRS是一个开源的实时视频服务器,它可以支持RTMP、WebRTC、HLS、HTTP-FLV、SRT等多种实时流媒体协议;

SRS的作者为了能更好的验证SRS服务器的流媒体转发性能,同时提供了专门的测试套件 st-load

该套件中提供了:

  • sb_hls_load 支持HLS点播和直播
  • sb_http_load 支持HTTP负载测试,所有并发重复下载一个http文件
  • sb_rtmp_load 支持RTMP流播放测试,一个进程支持5k并发
  • sb_rtmp_publish 支持RTMP流推流测试,一个进程支持500个并发

该套件需要在 linux 下运行,它会模拟客户端行为拉流。

使用方法

Build from source, then run RTMP benchmark:

1
2
3
git clone https://github.com/ossrs/srs-bench.git &&
cd srs-bench && ./configure && make &&
./objs/sb_rtmp_load -c 1 -r rtmp://127.0.0.1:1935/live/livestream

Or directly by docker:

1
2
docker run --rm -it --network=host --name sb ossrs/srs:sb \
./objs/sb_rtmp_load -c 1 -r rtmp://127.0.0.1:1935/live/livestream

For HTTP-FLV benchmark:

1
2
docker run --rm -it --network=host --name sb ossrs/srs:sb \
./objs/sb_http_load -c 1 -r http://127.0.0.1:8080/live/livestream.flv

For HLS benchmark:

1
2
docker run --rm -it --network=host --name sb ossrs/srs:sb \
./objs/sb_hls_load -c 1 -r http://127.0.0.1:8080/live/livestream.m3u8

Or from Aliyun mirror:

1
2
3
docker run --rm -it --network=host --name sb \
registry.cn-hangzhou.aliyuncs.com/ossrs/srs:sb \
./objs/sb_rtmp_load -c 1 -r rtmp://127.0.0.1:1935/live/livestream

Note: Please use docker kill sb to stop it.

使用 ./objs/sb_rtmp_load or ./objs/sb_hls_load or ./objs/sb_http_load 去拉不同协议的流,-c 表示负载数

flazr

flazr 是 RTMP 的一个 java 实现,这个项目提供了一个流媒体服务器和相关的工具类;

该工具可以在 windows 上运行,我们可以使用它来压测 rtmp 拉流;

下载地址:https://sourceforge.net/projects/flazr/files/flazr/

JMeter

使用 JMeter 的BlazeMeter-HLS Plugin,可以压测 hls 播放

参考:https://zhuanlan.zhihu.com/p/624834220

npm 命令速记

  • 查看 npm 包所有版本
1
npm view <package_name> versions

iOS webview 调试

  1. 在 iOS 设备上,依次打开 “设置” -> “Safari浏览器” -> “高级” -> 开启 “JavaScript” 和 “网页检查器”

  1. 在 Mac 设备上,显示 Safari 浏览器的 “开发” 菜单

  1. 将 iOS 设备连接到对应的 Mac 设备上,并在 iOS 设备上打开对应的 webview 页面,此时能在 Safari 浏览器的 “开发” 菜单中看到可调试的 webview 页面,选中对应的页面即可

如何调试 WebRTC / 媒体播放?

在 Safari 的网页检查器中,打开 “媒体日志记录”、“MSE日志记录”、“WebRTC日志记录” 设置

清理前端项目中未被使用到的文件

一个项目经过多次迭代后,不可避免的会残留一些未被使用到/多余的文件,人为的去分析文件的引用情况是一种费时、费力的行为;
使用 unused-files-webpack-plugin webpack 插件可以快速的找出哪些文件未被使用到。

安装:

1
npm i --save-dev unused-files-webpack-plugin

or

1
yarn add --dev unused-files-webpack-plugin

配置:

1
2
3
4
5
6
7
8
9
const { UnusedFilesWebpackPlugin } = require("unused-files-webpack-plugin");

module.exports = {
plugins: [
new UnusedFilesWebpackPlugin({
patterns: ['src/**/*.*']
}),
],
};

node.js 图片压缩

1
npm install -D images imagemin imagemin-mozjpeg imagemin-pngquant slash get-all-files
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import images from 'images';
import imagemin from 'imagemin';
import imageminMozjpeg from 'imagemin-mozjpeg';
import imageminPngquant from 'imagemin-pngquant';
import fsPromises from 'fs/promises';
import path from 'path';
import convertToUnixPath from 'slash';
import { getAllFiles } from 'get-all-files';

const handleFile = async (sourcePath) => {
// 如果图片 width > 750,等比缩小图片宽高
if (images(sourcePath).width() > 375 * 2) {
const fileType = path.extname(sourcePath).replace('.', '').toLowerCase();

const buffer = await imagemin.buffer(
images(sourcePath)
.size(375 * 2) // 调整图片的宽度,高度会等比缩小
.toBuffer(fileType),
{
plugins: [
imageminMozjpeg({ quality: 70 }),
imageminPngquant({
quality: [0.6, 0.8],
}),
],
}
);

const destinationPath = path.join('./build', path.basename(sourcePath).toLowerCase());

await fsPromises.mkdir(path.dirname(destinationPath), { recursive: true });

await fsPromises.writeFile(destinationPath, buffer);
return;
}

// --- 纯粹的图片大小压缩 ---
await imagemin([sourcePath], {
destination: 'build', // 结果输出到 build 目录
plugins: [
imageminMozjpeg({ quality: 70 }), // 压缩 jpg
imageminPngquant({
quality: [0.6, 0.8],
}), // 压缩 png
],
});
};

// 递归获取 images 目录下所有文件
getAllFiles('./images')
.toArray()
.then((paths) => {
return paths
.map((it) => convertToUnixPath(it)) // 将 Windows 的 "\\" 路径转为 "/"
.filter((it) => {
const ext = path.extname(it).toLowerCase();

return ['.jpg', '.jpeg', '.png'].includes(ext); // 筛选出 .jpg, .jpeg, .png 后缀的文件
});
})
.then((paths) => {
return paths.map(async (p) => {
try {
return await handleFile(p);
} catch (err) {
throw err;
}
});
});

UmiJS

图片压缩

打包时使用 image-webpack-loader 压缩图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { defineConfig } from 'umi';

export default defineConfig({
chainWebpack(memo, { env, webpack, createCSSRule }) {
// 在默认的 images 规则上,添加 image-webpack-loader 来压缩图片
memo.module
.rule('images')
.use('image-webpack-loader')
.loader(require.resolve('image-webpack-loader'))
.options({
options: {
bypassOnDebug: true, // webpack@1.x
disable: true, // webpack@2.x and newer
},
});
},
});

Webpack 杂记

  1. 关于为什么使用 await import('@/common/themes/' + window.mode) 时,会把 @/common/themes 目录下的其他文件(如,markdown 文件)也打包进 chunk!

根据 webpack官方文档 的解释:

当我们在使用 import()require() 时,若其标识符参数中含有表达式,由于这种引入形式不能在打包编译阶段确定标识符参数的值,导致无法做静态分析、构建依赖图,因此会自动创建一个上下文(context);

webpack 会解析require()调用中的字符串,提取出如下的一些信息,供 context 使用:

1
2
3
4
从 '@/common/themes/' + window.mode 中提取出:

Directory:@/common/themes
Regular expression: /^.*/

而后,webpack 会将目录(@/common/themes)下所有符合正则表达式(/^.*/)的模块都打包到一起;因此这种方式,可能会引入一些不必要的模块。

对于require()的引入方式,可通过设置require.context()解决;对于import()方式,可以使用 webpack 的内联注释解决该问题。

  1. less webpack 配置

使用 less-loader 预编译 *.less 文件,而后使用 cssnano postcss 插件压缩 css,使用 autoprefixer 插件兼容各浏览器 css prefix。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.less$/,
exclude: /node_modules/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{
loader: 'postcss-loader',
options: {
postcssOptions: process.env.NODE_ENV === 'production' ? {
plugins: [
require('cssnano')({
preset: 'default',
}),
'autoprefixer'
]
} : undefined,
}
},
{ loader: 'less-loader' }
]
}
]
}
};

二分查找

基本的二分搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// nums 递增
let binarySearch = function (nums, target) {
let left = 0;
let right = nums.length - 1;

while (left <= right) {
let mid = left + ((right - left) >> 1);

if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}

return -1;
};

因为我们是要查找一个数存不存在,所以当 left == right 时,我们也应该继续执行 while 查找。

寻找左侧边界的二分搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let searchLeftBound = function (nums, target) {
if (nums.length == 0) return -1;
let left = 0;
let right = nums.length;

while (left < right) {
let mid = left + ((right - left) >> 1);

if (nums[mid] == target) {
right = mid;
} else if (nums[mid] > target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
}
}

return nums[left] == target ? left : -1;
};

对于 nums = [1, 2, 2, 2, 3]target = 2 而言,left 的结果为 1,可以理解为 nums 中小于 2 的个数有 1 个。
target 大于 nums 所有数时,left 的结果为 nums.length;此时 nums[left]nums[nums.length]undefined,必然不等于 target

寻找右侧边界的二分搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let searchRightBound = function (nums, target) {
let left = 0;
let right = nums.length - 1;

while (left <= right) {
let mid = left + ((right - left) >> 1);

if (nums[mid] == target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
}
}

if (right < 0 || nums[right] != target) return -1;

return right;
};

nums 为非递减顺序时,right < 0 表示 target 比所有元素都小,即算法不断在执行 right = mid - 1

JavaScript 算法常用语法

  1. 除2取整
1
2
3
4
let num = 5;

// let res = Math.floor(num / 2);
let res = num >> 1;
  1. 拼接字符串 String.prototype.concat()
1
2
3
4
5
6
7
8
var s1 = 'abc';
var s2 = 'def';

s1.concat(s2) // "abcdef"
s1 // "abc"

// 可以接受多个参数
'a'.concat('b', 'c') // "abc"
  1. 截取子字符串 String.prototype.slice()

slice 方法用于从原字符串取出子字符串并返回,不改变原字符串。它的第一个参数是子字符串的开始位置,第二个参数是子字符串的结束位置(不含该位置)。

1
'JavaScript'.slice(0, 4) // "Java"

如果省略第二个参数,则表示子字符串一直到原字符串结束。

1
'JavaScript'.slice(4) // "Script"

如果参数是负值,表示从结尾开始倒数计算的位置,即该负值加上字符串长度。

1
2
3
'JavaScript'.slice(-6) // "Script"
'JavaScript'.slice(0, -6) // "Java"
'JavaScript'.slice(-2, -1) // "p"
  1. Unicode 字符串比较

字符串按照字典顺序进行比较。

1
2
3
4
5
6
7
'a' > 'z' // false

'abc' < 'abd' // true

'abc' < 'abcd' // true

'a0b1' < 'a1b1' // true
  1. 截取子数组 Array.prototype.slice()

slice 方法用于提取目标数组的一部分,返回一个新数组,原数组不变。
第一个参数为起始位置(从0开始),第二个参数为终止位置(但该位置的元素本身不包括在内)。
如果省略第二个参数,则一直返回到原数组的最后一个成员。

1
2
3
4
5
6
7
var a = ['a', 'b', 'c'];

a.slice(0) // ["a", "b", "c"]
a.slice(1) // ["b", "c"]
a.slice(1, 2) // ["b"]
a.slice(2, 6) // ["c"]
a.slice() // ["a", "b", "c"]

如果 slice 方法的参数是负数,则表示倒数计算的位置。

1
2
3
var a = ['a', 'b', 'c'];
a.slice(-2) // ["b", "c"]
a.slice(-2, -1) // ["b"]
  1. 删除原数组元素 Array.prototype.splice()

splice 方法用于删除原数组的一部分成员,并可以在删除的位置添加新的数组成员,返回值是被删除的元素。注意,该方法会改变原数组。

splice 的第一个参数是删除的起始位置(从0开始),第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素。

1
2
3
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(4, 2) // ["e", "f"]
a // ["a", "b", "c", "d"]
  1. 数组排序 Array.prototype.sort()

sort 方法对数组成员进行排序,默认是按照字典顺序排序。排序后,原数组将被改变

1
2
3
4
5
var arr = ['d', 'c', 'b', 'a'];

arr.sort(); // ['a', 'b', 'c', 'd']

arr // ['a', 'b', 'c', 'd']
  • number[] 升序排序
1
numbers.sort((a, b) => a - b);
  • number[] 降序排序
1
numbers.sort((a, b) => b - a);
  1. Array.prototype.reverse()

reverse 方法用于颠倒排列数组元素,返回改变后的数组。注意,该方法将改变原数组

1
2
3
4
var a = ['a', 'b', 'c'];

a.reverse() // ["c", "b", "a"]
a // ["c", "b", "a"]
  1. 合并数组 Array.prototype.concat()

concat 方法用于多个数组的合并。它将新数组的成员,添加到原数组成员的后部,然后返回一个新数组,原数组不变

1
2
3
4
5
6
7
let arr = ['hello'];

arr.concat(['world'], ['!']) // ["hello", "world", "!"]

arr // ['hello']

[2].concat({a: 1}, {b: 2}) // [2, {a: 1}, {b: 2}]
  1. 易混淆知识点
  • inhasOwnProperty
1
2
3
4
5
var obj = {};

'toString' in obj // true

obj.hasOwnProperty('toString') // false
  • typeof
1
2
3
4
5
6
7
typeof 1 // 'number'
typeof false // 'boolean'
typeof 'abbc' // 'string'
typeof [] // 'object'
typeof null // 'object'
typeof function() {} // 'function'
typeof undefined // 'undefined'

Media PlayType Support Detect

online demo

Support built-in HLS

Based on flv.js.

1
2
3
4
5
6
7
function supportNativeMediaPlayback(mimeType: string) {
const videoElement = window.document.createElement('video');
let canPlay = videoElement.canPlayType(mimeType);
return canPlay === 'probably' || canPlay == 'maybe';
}

console.log('Support built-in HLS:', supportNativeMediaPlayback('application/vnd.apple.mpegurl'));

Support MSE H264

Based on hls.js.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// check MediaSource support
function getMediaSource(): typeof MediaSource | undefined {
return self.MediaSource || ((self as any).WebKitMediaSource as MediaSource);
}

// check SourceBuffer support
function getSourceBuffer(): typeof self.SourceBuffer {
return self.SourceBuffer || (self as any).WebKitSourceBuffer;
}

function isSupported(): boolean {
const mediaSource = getMediaSource();
if (!mediaSource) {
return false;
}
const sourceBuffer = getSourceBuffer();
// Check H264 support
const isTypeSupported =
mediaSource &&
typeof mediaSource.isTypeSupported === 'function' &&
mediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');

// if SourceBuffer is exposed ensure its API is valid
// safari and old version of Chrome doe not expose SourceBuffer globally so checking SourceBuffer.prototype is impossible
const sourceBufferValidAPI =
!sourceBuffer ||
(sourceBuffer.prototype &&
typeof sourceBuffer.prototype.appendBuffer === 'function' &&
typeof sourceBuffer.prototype.remove === 'function');
return !!isTypeSupported && !!sourceBufferValidAPI;
}

console.log('Support MediaSource:', !!getMediaSource());
console.log('Support SourceBuffer:', !!getSourceBuffer());
console.log('Support MSE & H264:', !!isSupported());

Convert TSConfig paths to Webpack alias

Inspired by marzelin/convert-tsconfig-paths-to-webpack-aliases.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const fs = require('fs');
const path = require('path');
const get = require('lodash/get');
const JSON5 = require('json5');

/**
* convert tsconfig.json paths to webpack alias
*/
const convertTSConfigPaths2Aliases = () => {
const tsConfigPath = path.resolve(process.cwd(), './tsconfig.json');

try {
if (fs.existsSync(tsConfigPath)) {
// dealing with json file with comments, we use json5 to parse tsconfig.json instead of requiring directly
const tsConfig = JSON5.parse(fs.readFileSync(tsConfigPath));
let { baseUrl, paths } = get(tsConfig, 'compilerOptions', {});

if (paths) {
baseUrl = baseUrl ? path.resolve(process.cwd(), baseUrl) : process.cwd();

const replaceGlobs = (path) => path.replace(/(\/\*\*)*\/\*$/, '');

return Object.keys(paths).reduce((aliases, pathName) => {
const alias = replaceGlobs(pathName);
const aliasPath = replaceGlobs(paths[pathName][0]);
aliases[alias] = path.resolve(baseUrl, aliasPath);
return aliases;
}, {});
}
}
} catch (err) {
console.error(err);
return {};
}

return {};
};

Usage

1
2
3
4
5
6
7
8
9
// webpack.js

module.exports = {
resolve: {
alias: {
...convertTSConfigPaths2Aliases(),
}
}
};

使用 GitHub Actions 自动化构建/部署 GitHub Page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# .github/workflows/deploy.yml
name: Deploy Blog
on:
push:
branches:
- master # 当有 commit 被 push 到 master 分支时,执行下面的 jobs
jobs:
deploy: # job name
runs-on: macos-latest # jobs 运行在 macos 上
steps:
- name: Check out Git repository
uses: actions/checkout@v2 # 拉取当前最新代码

- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v2 # 初始化 node 环境
with:
node-version: '12'

- name: Cache NPM dependencies
uses: actions/cache@v2 # 缓存 ~/.npm 内的 npm 包,以免每次执行 jobs 的时候都需要重新下载
with:
path: ~/.npm
key: ${{ runner.os }}-npm-cache
restore-keys: |
${{ runner.os }}-npm-cache

- name: Build # 安装 npm 包 & 执行 hexo 的打包命令(不使用 hexo 的 deploy 命令,而是通过下一个 step 来部署)
run: |
npm install
npm run build-only
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}

- name: Deploy
uses: peaceiris/actions-gh-pages@v3 # hexo 构建后的文件在 public 目录下,将该目录下的文件推送到另一个仓库
with:
personal_token: ${{ secrets.ACCESS_TOKEN }} # 由于推送到另一个仓库需要权限,所以需要创建/配置一个 personal access token
external_repository: FredZeng/FredZeng.github.io # 如果是推送到同一个仓库,就可以不用写这个
publish_dir: ./public
publish_branch: master

配置

  1. Creating a personal access token 创建自己的 access token,一般勾选上 repo 就可以了;请务必复制,保存好生成的 token
  2. Encrypted secrets 将创建好的 token 添加到项目的 Actions secrets 中,这样你才能在 action 里面用到上述的 secrets.ACCESS_TOKEN
  3. 🎉🎉🎉 你已经完成了所有前置步骤,可以享受自动化部署了

其他

其他方案可参考官方文档 将 Hexo 部署到 GitHub Pages 实现自动化部署。

\ No newline at end of file