CSS毛玻璃特效

取自https://glassgenerator.netlify.app/#


<div class="glass-container" id="glass"></div>



.glass-container{
    width: 700px;
    height: 375px;
    color: white;
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 20px;
    border-radius: 21px;
    backdrop-filter: blur(5px);
    background-color: rgba(255,102,0, 0.181);
    box-shadow: rgba(0, 0, 0, 0.3) 2px 8px 8px;
    border: 1px rgba(255,255,255,0.4) solid;
    border-bottom: 1px rgba(40,40,40,0.35) solid;
    border-right: 1px rgba(40,40,40,0.35) solid;
}

视频在线下载部署记录

首先,安装you-get $ pip3 install you-get

Youtube-dl

再部署python代码,一般如果you-get下载不来的再用Youtube-dl来下,

最后如果离线下载要用就用clouddrive加个云盘到系统。

参考文章

https://github.com/soimort/you-get

视频专业下载工具,Youtube-dl 详细使用教程与初学上手示例

https://blog.csdn.net/Kim_Linshuo/article/details/109630654

https://zhuanlan.zhihu.com/p/344090839

https://blog.csdn.net/weixin_44578029/article/details/120676879

另外考虑加入下面这个,但是用go写的

https://github.com/iawia002/lux

还考虑加入代理池

https://github.com/ydf0509/proxypool_framework/tree/master

音乐播放器API

为WordPress网站添加一个音乐播放器,网上大部分的教程都是使用各种插件,有的插件还需要花钱买授权,用着不爽。本文用纯代码为网站添加音乐播放器,安全可靠,可以任意修改。

1、Aplayer

其实很简单,Github有很多大神开源了相关的内容。用着最舒服的,当属Aplayer了,可以去官网体验一下:

​ ​https://aplayer.js.org/​​

文档手册也很详细:

​ ​https://github.com/MoePlayer/APlayer​​

方法很简单,加载Aplayer的js和css,在想要展先的位置调用即可。Aplayer支持很多模式,上图展示的效果为吸底模式,就是在网页的页面最左下角展示。本文以吸底模式为例进行说明,如果想采用其它模式,可以根据文档手册修改代码。

2、Meting

如果只使用Aplayer,那么需要指定音乐的图片地址,mp4地址,歌词等,用着不是很方便,这时就可以使用Meting作为辅助:

​ ​https://github.com/metowolf/MetingJS​​

Meting为Aplayer提供了网易云音乐的API接口,只要获得网易云音乐歌单的ID,就可以自动加载歌单里所有的歌曲,直接调用,方便很多,当然除了网易云音乐的API,还有其他的接口。

3、开工

如果上述这些说了都不懂,没关系,跟着接下来的步骤走即可。

首先下载为各位打包好的js和css文件(下载地址)

解压后,会看到三个文件APlayer.min.css、APlayer.min.js、Meting.min.js,将两个js文件放到主题的js文件夹中,将css文件放到主题的css文件夹中。

在主题目录下的footer.php中,添加如下代码:


将代码中的wp-content/themes/construction-base改为你的WordPress主题的根目录。保存好后,刷新网页就可以看到播放器了。

1. <link rel="stylesheet" href="wp-content/themes/construction-
2. base/css/APlayer.min.css"><script src="wp-content/themes/construction-base/js/APlayer.min.js">
3. </script>
4. <div class="aplayer"
5. data-id="969649908"
6. data-fixed="true" data-
7. server="netease"
8. data-volume="0.8"
9. data-type="playlist">
10. </div>
11. <script src="wp-content/themes/construction-base/js/Meting.min.js">
12. </script>

但是这个播放器的歌单是我指定的歌单,如果想用自己的网易云音乐歌单,可以修改data-id的值,这个值的获取方法也很简单:

打开浏览器访问网页版的网易云音乐,地址栏的id参数就是对应的data-id值,自己创建一个歌单,想放什么音乐就放什么音乐。

PS:歌单更新歌曲实时更新

Vue和Flask实现前后端分离

后端采用Flask提供几个Restfull风格的API,前端放弃惯用的flasky常用的Jinja模板引擎,采用灵活的Vue.js框架,顺便实践一把前后端分离。既然前后端分离,那么就需要在开发环境独立创建两个项目。下面逐一介绍。

安装node.js

安装vue,安装webpack,安装vue-cli,创建工程

vue是以组件为单位组织成复杂的页面。我们直接在componets下的HelloWorld.vue上进行代码调整和编写。template部分主要包含一个select元素,value对应后端API,触发button会向选择的后端API发送请求。script部分methods部分实现对元素事件的响应,通过axios与后端服务器进行通信,分别采用get和post方法。具体代码如下图:

<template>
  <div class="hello">
    <button @click="test">测试</button>
    <select v-model="selected" name="url-option">
      <option value="">选择一个Url</option>
      <option value="/api/speech/event_extraction">思必驰警情信息抽取</option>
      <option value="/api/speech/addr_fix">思必驰地址理解</option>
      <option value="/api/gaode/get_poi">高德关键字搜索</option>
    </select>
    <input v-model="input_query" style="width: 400px">
    <button @click="sendRequest">发送请求</button>
    <br></br>
    <textarea style="width: 600px; height: 300px">{{ resp }}</textarea>
  </div>
</template>

<script>
import axios from 'axios'
// 后端服务器地址
axios.defaults.baseURL = 'http://10.67.*.*:5000';
export default {
  name: 'HelloWorld',
  data () {
    return {
      selected: '',
      input_query: '',
      resp: ''
    }
  },
  methods: {
    test () {
      axios
        .get('/api/hello')
        .then(response => {
          console.log(response.data.name)
          this.resp = response.data.name
        })
        .cache(error => {
          console.error(error)
        })
    },
    sendRequest () {
      console.log(this.selected)
      axios
        .post(this.selected, {query: this.input_query})
        .then(response => {
          console.log(response)
          this.resp = response.data
        }).catch(error => {
          console.error(error)
        })
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

在vue中还用

v-for 来在前端页面中做for循环,v-on:click=点击,v-if=表示IF语句

后端

打开pycharm,新建一个flask项目

在app.py里编写几个Restfull 风格的API,用于响应前端请求

from flask import Flask, jsonify, request
from flask_cors import CORS
import requests
import json

app = Flask(__name__)
# 实现跨域访问
cors = CORS(app, resources={r"*": {"origins": "*"}})

# get请求
@app.route('/api/hello')
def hello_world():
    content = {
            "name": "网站",
            "num": 3,
            "sites": [{"name": "Google", "info": ["Android", "Google 搜索", "Google 翻译"]},
                      {"name": "Runoob", "info": ["菜鸟教程", "菜鸟工具", "菜鸟微信"]},
                      {"name": "Taobao", "info": ["淘宝", "网购"]}],
            }

    return jsonify(content)

# post请求
@app.route('/api/gaode/get_poi', methods=['POST'])
def get_poi():
    json_data = request.get_json()
    url_prefix = "***"   
    url = url_prefix + '&keywords=' + json_data['query']
    headers = {"Content-Type": "application/json"}
    resp = requests.get(url, headers=headers)
    text = resp.json()
    return text

# 其它接口此处可进行补充.............

if __name__ == '__main__':
    app.run(
        # TODO 设置无效,原因未知
        # host='10.67.*.*',
        # port='5000',
        # debug=True
    )

通过 Data URI Scheme 的方式来引入图片到网页

常见的图片加载方式

一般而言,最常见的图片加载方式是 HTTP URI Scheme 简称 HTTP URI。例如:某图片存储在又拍云存储上,链接为 https://f.lxpzyy.top/upyun/upcdn.svg。我们可以通过引入图片 HTTP 链接的方式来加载图片。

除此之外,我们可以通过 Data URI Scheme 的方式来引入图片。

<img src=“data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGlu……/>

Data URI Scheme 的方式和 HTTP URI 的使用较为类似,只是看起来多了很多“乱码”。那这些乱码有什么作用呢?和 HTTP URI 相比,它有什么优势让我们选择它呢?

初识 Data URI

Data URI scheme 简称 Data URI ,是在 RFC2397 中进行定义的。目的是将一些小的数据,直接嵌入到网页中,从而不用再从外部文件载入。以上方的 Data URI 链接为例:

<img src=“data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGlu……/>

我们可以看到它由 data,image/svg+xml,base64 以及后面的字符串组成。

data:[][;charset=][;],

这几部分分别为:

  • data: 表示取得数据的协定名称,表明这是一个 Data URI。
  • mime type: 数据类型名称,也是就是 image/svg+xml,如果传入的是个 png 图片,那么可以指定类型为 image/png。
  • charset=: 可不填,默认是 charset=US-ASCII 编码。
  • base64: 是数据的编码方法。
  • encoded data: 经过 base64 编码后的数据。

我们可以看到,除去可不填的 charset=,使用 Data URI 必然会用到 base64 编码,那这个要如何获得呢?

如何获得 base64 编码

最简单的方式是直接上网找一个编码小工具。

现在网上有很多的 base64 编码小工具,他们可以直接将文件或者字符进行 base64 解编码。

除此之外各代码语言也都有其对应的 base64 编码方式,可以很方便地进行编码解码转换:

Data URI 的使用方式及优势

了解了 Data URI 的大致情况,我们来看看它要如何使用。

它主要有两种使用方式,第一种是将处理好的 Data URI 格式数据,放入 HTML 页面代码 img 标签的 src 属性中。这种方式的好处是减少了 HTTP 请求,缺点是无法进行缓存。另一种方式则是将 Data URI 放入到 css 里,优点是浏览器会积极缓存 css 文件来提升加载页面时的速度,缺点是增加了 css 的长度。

那么回到我们最初的问题 Data URI 比 HTTP URI 的优势是什么呢?

相比 HTTP URI,Data URI 拥有以下优势:

  • 使用 Data URI 能够有效减少 HTTP 请求数
  • 不依赖于网络环境,即没有网络的时候页面中的资源也可以被加载出来
  • 可以免除一些极小文件对 HTTP 请求的占用

当然 Data URI 也有不少缺点。经过 Base64 编码后的文件或者数据,通常来说比起原文件体积增大了30%左右,然后 Data URI 通常会写在 css 文件中,不易维护,另外手机端加载 Data URI 资源也比较消耗 CPU 资源。

相比之下,Data URI 还是很值得使用的,现在就有很多场景都使用 Data URI 的方式引入资源,比如百度首页的小图标以及谷歌的首页等。

如果想让网站拥有不同的加载方式,特别是避免因网络状况差导致的无法加载问题,那一定要试试 Data URI。

在考虑使用 Data URI 的过程中,我们可以从以下几点着重考虑:

  • 图片的实际尺寸比较小
  • 不经常更新的资源图片
  • 需要在页面中经常使用的图片

宝塔-狂雨小说

bbs.kyxscms.com

PHP建议高一点,实践7.4可用,在PHP设置中安装fileinfo扩展

建好网站后上传源码,伪静态里要选择thinkphp,然后保存,

在数字库操作以下代码,来新建一个采集,然后进后台去采集,但采出来的好像有很多文章并不完整。应该是采集的原站就文章不完整。

INSERT INTO `ky_collect` (`id`, `title`, `charset`, `type`, `url_complete`, `url_reverse`, `pic_local`, `source_url`, `section`, `url_rule`, `url_merge`, `url_must`, `url_ban`, `relation_url`, `rule`, `category_way`, `category_fixed`, `category_equivalents`, `status`, `create_time`, `update_time`, `collect_time`, `update`) VALUES (NULL, 'http://www.shuquge.com/', 'auto', 'novel', '1', '0', '0', '[{"url":"http://www.shuquge.com/category/1_[内容].html","type":"1","param":["1","1227","1",0]},{"url":"http://www.shuquge.com/category/2_[内容].html","type":"1","param":["1","317","1",0]},{"url":"http://www.shuquge.com/category/3_[内容].html","type":"1","param":["1","853","1",0]},{"url":"http://www.shuquge.com/category/4_[内容].html","type":"1","param":["1","230","1",0]},{"url":"http://www.shuquge.com/category/7_[内容].html","type":"1","param":["1","308","1",0]}]', '<body>[内容]</body>', '<li><span class="s1">(*)</span><span class="s2"><a href="[内容1]">(*)</a></span>', '', '', '', '[{"title":"章节页","page":"default","chapter":"1","section":"<dt>(*)正文</dt>[内容]</div>","url_rule":"<dd><a href=\\"[内容1]\\">[章节标题]</a></dd>","url_merge":""}]', '{"category":{"field":"category","source":"default","rule":"<meta property=\\"og:novel:category\\" content=\\"[内容1]\\" \\/> ","merge":"","strip":""},"title":{"field":"title","source":"default","rule":"<meta property=\\"og:novel:book_name\\" content=\\"[内容1]\\" \\/> ","merge":"","strip":"","replace":""},"author":{"field":"author","source":"default","rule":"<meta property=\\"og:novel:author\\" content=\\"[内容1]\\" \\/> ","merge":"","strip":"","replace":""},"serialize":{"field":"serialize","source":"default","rule":"<meta property=\\"og:novel:status\\" content=\\"[内容1]\\" \\/>","merge":"","serial":"连载中","over":"完结","strip":"","replace":""},"pic":{"field":"pic","source":"default","rule":" <meta property=\\"og:image\\" content=\\"[内容1]\\" \\/> ","merge":"","strip":"","replace":""},"content":{"field":"content","source":"default","rule":" <meta property=\\"og:description\\" content=\\" [内容1]\\" \\/> ","merge":"","strip":"","replace":""},"tag":{"field":"tag","source":"default","rule":"<meta property=\\"og:title\\" content=\\"[内容1]\\" \\/> ","merge":"","strip":"","replace":""},"chapter_title":{"field":"chapter_title","source":"0","rule":"<h1>[内容1]<\\/h1>","merge":"","strip":"","replace":""},"chapter_content":{"field":"chapter_content","source":"0","rule":"<div id=\\"content\\" class=\\"showtxt\\">[内容1]<br \\/>","merge":"","strip":"","replace":"[{\\"find\\":\\"<br\\/>\\",\\"replaces\\":\\"\\"}]"}}', '0', '0', '[{"target":"玄幻魔法","local":"18"},{"target":"武侠修真","local":"19"},{"target":"都市言情","local":"21"},{"target":"历史军事","local":"20"},{"target":"科幻灵异","local":"22"}]', '1', '1586771330', '1586774583', '1586793887', '0')

静态网站的免费存放

一、vercel是一个站点托管平台,可以托管静态网页,可以放个hexo博客系统在上面

二、Github Pages是完全基于Github创建的,也就是说你的博客站点实际上就是你的Github账户下的一个特殊的repo。所以,点击“New repository”新建一个仓库。
  而这个repo的特殊之处就是,它的名字必须是“username.github.io”(划重点啦,不要打瞌睡!),其中,username就是你的Github账户的用户名

同类的平台有Netlify等

把自己的域名做为邮箱后缀

新注册了个域名,想要有自己域名后缀的邮箱,

可以先到PC版企业微信注册帐号,进去后点‘邮件’进去,里面有个邮箱域名,设置成自己的域名,

并按要求先到域名注册商把域名设置两个MX记录,一个CNAME记录,验证通过就设置好自己的域名了,

然后在PC版企业微信进 邮件-邮箱管理-业务邮箱 进去后新增一个业务邮箱,并获取到密码,

这时你就可以按提示用foxmail等软件来收发邮件了,

如果不想用软件来收发邮件,则可用以下个人QQ邮箱来管理收发邮件

进自己的个人QQ邮箱-设置-其它邮箱

在里面’添加代收邮箱账户‘,然后填入邮箱地址,和之前获取的密码,

然后就可以在个人的邮箱里代收你自己域名邮箱的邮件了。发邮件时在’发件人‘选项卡中选择自己域名邮箱的这个账号就可以发邮件了

我的自有域名邮箱xtaa@xtaa.cn

流媒体视频基础 MSE 入门 & FFmpeg 制作视频预览缩略图和 fmp4

如果检查主流视频网站的视频,就会发现网站的 video 元素的 src 属性都是 blob 开头的字符串。

为什么视频链接前面会有 blob 前缀?这是因为视频网站使用了这篇文章要讲的 MSE 来播放视频。

Media Source Extensions

虽然 video 很强大,但是还有很多功能 video 并不支持,比如直播,即时切换视频清晰度,动态更新音频语言等功能。

MSE(Media Source Extensions)就来解决这些问题,它是 W3C 的一种规范,如今大部分浏览器都支持。

它使用 video 标签加 JS 来实现这些复杂的功能。它将 videosrc 设置为 MediaSource 对象,然后通过 HTTP 请求获取数据,然后传给 MeidaSource 中的 SourceBuffer 来实现视频播放。

如何将 MediaSourcevideo 元素连接呢?这就需要用到 URL.createObjectURL 它会创建一个 DOMString 表示指定的 File 对象或 Blob(二进制大对象) 对象。这个 URL 的生命周期和创建它的窗口中的 document 绑定。

const video = document.querySelector('video')
const mediaSource = new MediaSource()

mediaSource.addEventListener('sourceopen', ({ target }) => {
    URL.revokeObjectURL(video.src)
    const mime = 'video/webm; codecs="vorbis, vp8"'
    const sourceBuffer = target.addSourceBuffer(mime) // target 就是 mediaSource
    fetch('/static/media/flower.webm')
        .then(response => response.arrayBuffer())
        .then(arrayBuffer => {
            sourceBuffer.addEventListener('updateend', () => {
                if (!sourceBuffer.updating && target.readyState === 'open') {
                    target.endOfStream()
                    video.play()
                }
            })
            sourceBuffer.appendBuffer(arrayBuffer)
        })
})

video.src = URL.createObjectURL(mediaSource)
复制代码

addSourceBuffer 方法会根据给定的 MIME 类型创建一个新的 SourceBuffer 对象,然后会将它追加到 MediaSourceSourceBuffers 列表中。

我们需要传入相关具体的编解码器(codecs)字符串,这里第一个是音频(vorbis),第二个是视频(vp8),两个位置也可以互换,知道了具体的编解码器浏览器就无需下载具体数据就知道当前类型是否支持,如果不支持该方法就会抛出 NotSupportedError 错误。更多关于媒体类型 MIME 编解码器可以参考 RFC 4281

这里还在一开始就调用了 revokeObjectURL。这并不会破坏任何对象,可以在 MediaSource 连接到 video 后随时调用。 它允许浏览器在适当的时候进行垃圾回收。

视频并没有直接推送到 MediaSource 中,而是 SourceBuffer,一个 MeidaSource 中有一个或多个 SourceBuffer。每个都与一种内容类型关联,可能是视频、音频、视频和音频等。

视频格式

HTML5 标准指定时,想指定一种视频格式作为标准的一部分,所有浏览器都必须实现。但是对于 H.264 视频编码各个厂商产生的争论,主要是 H.264 非常强大(高画质、高压缩比、成熟的编解码器…),但是它也要高昂的授权费。Mozilla 这类免费浏览器,并没有从其开发的浏览器上获得直接收入,但是让 H.264 加入标准,它就要支付相应的授权费,所有认为是不可接受的。于是后来放弃了视频格式指定的统一,浏览器厂商可以自由选择支持的格式。

不过现在所有主流浏览器都支持 H.264 编码格式的视频,所有选择视频编码时优先选择 H.264 编码。

MediaSource API

MediaSource 属性

属性描述
sourceBuffers返回包含 MediaSource 所有 SourceBufferSourceBufferList 对象
activeSourceBuffers上个属性的子集,返回当前选择的视频轨道,启用的音频规定和显示或隐藏的文本轨道
duration获取或者设置当前媒体展示的时长,负数或 NaN 时抛出 InvalidAccessError
readyState 不为 openSourceBuffer.updating 属性为 true 时抛出 InvalidStateError
readyState表示 MediaSource 的当前状态

readyState 有以下值:

  • closed 未附着到一个 media 元素上
  • open 已附着到一个 media 元素并准备好接收 SourceBuffer 对象
  • ended 已附着到一个 media 元素,但流已被 MediaSource.endOfStream() 结束

MediaSource 方法

方法描述
addSourceBuffer(mime)根据给定 MIME 类型创建一个新的 SourceBuffer 对象,将它追加到 MediaSource 的 SourceBuffers 列表中
removeSourceBuffer(sourceBuffer)移除 MediaSource 中指定的 SourceBuffer。如果不存在则抛出 NotFoundError 异常
endOfStream(endOfStreamError)向 MediaSource 发送结束信号,接收一个 DOMString 参数,表示到达流末尾时将抛出的错误
setLiveSeekableRange(start, end)设置用户可以跳跃的视频范围,参数是以秒单位的 Double 类型,负数等非法参数会抛出异常
clearLiveSeekableRange清除上次设置的 LiveSeekableRange

其中 addSourceBuffer 可能会抛出一下错误:

错误描述
InvalidAccessError提交的 mimeType 是一个空字符串
InvalidStateErrorMediaSource.readyState 的值不等于 open
NotSupportedError当前浏览器不支持的 mimeType
QuotaExceededError浏览器不能再处理 SourceBuffer 对象

endOfStream 的参数可能是如下两种字符串:

  • network 终止播放并发出网络错误信号
  • decode 终止播放并发出解码错误信号

MediaSource.readyState 不等于 open 或有 SourceBuffer.updating 等于 true,调用 endOfStream 会抛出 InvalidStateError 异常。

它还有一个静态方法

静态方法描述
isTypeSupported(mime)是否支持指定的 mime 类型,返回 true 表示可能支持并不能保证

MediaSource 事件

错误描述
sourceopenreadyStateclosedendedopen
sourceendedreadyStateopenended
sourceclosereadyStateopenendedclosed

SourceBuffer API

SourceBuffer 通过 MediaSource 将一块块媒体数据传递给 meida 元素播放。

SourceBuffer 属性

属性描述
mode控制处理媒体片段序列,segments 片段时间戳决定播放顺序,sequence 添加顺序决定播放顺序,
MediaSource.addSourceBuffer() 中设置初始值,如果媒体片段有时间戳设置为 segments,否则 sequence
自己设置时只能从 segments 设置为 sequence,不能反过来。
updating是否正在更新,比如 appendBuffer()remove() 方法还在处理中
buffered返回当前缓冲的 TimeRanges 对象
timestampOffset控制媒体片段的时间戳偏移量,默认是 0
audioTracks返回当前包含的 AudioTrack 的 AudioTrackList 对象
videoTracks返回当前包含的 VideoTrack 的 VideoTrackList 对象
textTracks返回当前包含的 TextTrack 的 TextTrackList 对象
appendWindowStart设置或获取 append window 的开始时间戳
appendWindowEnd设置或获取 append window 的结束时间戳

append window 是一个时间戳范围来过滤 append 的编码帧。在范围内的编码编码帧允许添加到 SourceBuffer,之外的会被过滤。

SourceBuffer 方法

方法描述
appendBuffer(source)添加媒体数据片段(ArrayBufferArrayBufferView)到 SourceBuffer
abort中断当前片段,重置段解析器,可以让 updating 变成 false
remove(start, end)移除指定范围的媒体数据

SourceBuffer 事件

方法描述
updatestartupdatingfalse 变为 true
updateappendremove 已经成功完成,updatingtrue 变为 false
updateendappendremove 已经结束,在 update 之后触发
errorappend 时发生了错误,updatingtrue 变为 false
abortappendremoveabort() 方法中断,updatingtrue 变为 false

注意事项

  1. 几乎所有 MediaSource 方法调用或设置属性,在 MediaSource.readyState 不是 open 时会抛出 InvalidStateError 错误,应该在调用方法或设置属性前查看当前状态,即使是在事件回调中,因为可能在回调执行之前改变了状态。此外 endOfStream 方法还会因为 SourceBufferupdatingtrue 时也抛出该异常
  2. 在调用 SourceBuffer 方法或设置属性时,应该检查 SourceBuffer 是否为 false
  3. MediaSource.readyState 的值是 ended 时,调用 appendBuffer()remove() 或设置 modetimestampOffset 时,将会让 readyState 变为 open,并触发 sourceopen 事件,所有应该要有处理多个 sourceopen 事件准备

Initialization Segment

如果随便找一个 mp4 文件,使用上面那个例子播放,就会发现播放不了。这是因为 SourceBuffer 接收两种类型的数据:

  1. Initialization Segment 视频的初始片段,其中包含媒体片段序列进行解码所需的所有初始化信息。
  2. Media Segment 包含一部分媒体时间轴的打包和带时间戳的媒体数据。

MSE 需要使用 fmp4 (fragmented MP4) 格式,MP4 文件使用面向对象格式其中包含 Boxes (或叫 Atoms),可以使用 这个网站 查看 Mp4 文件信息。

这是一个普通的 MP4 文件,可以看到它有一个很大的 mdat (电影数据)box

这是 fragmented MP4 的截图,ISO BMFF 初始化段定义为单个文件类型框(File Type Box ftyp)后跟单个电影标题框(Movie Header Box moov),更多信息可以查看 ISO BMFF Byte Stream Format

moov 只包含一些视频基础的信息(类型,编码器等),moof 存放样本位置和大小,moof 框后都有一个 mdat,其中包含如前面的 moof 框中所述的样本。

要查看当前视频是不是 fmp4,就可以看 ftyp 后面是不是跟着 moov,然后是 moof mdat 对就行了。

要将普通 MP4 转换成 FMP4 可以下载 Bento4。然后用命令行输入如下命令就可以了。

mp4fragment ./friday.mp4 ./friday.fmp4
复制代码

Bento4 /bin 目录中有非常多好用的 mp4 工具,/utils 目录中都是 python 实用脚本。

工具

除了上面介绍的 Bento4,还有很多其他好用的工具。有了下面的工具,就可以快速制作 MSE 实践的视频素材了。

chrome://media-internals/

chrome://media-internals/ 是 chrome 浏览器用来调试多媒体的工具,直接在地址栏输入该值就可以。

Shaka Packager

Shaka Packager 是 Google 出的一个小巧视频工具,它只有 5M 左右,它可以用来查看视频信息,分离音频和视频,还支持 HLS 和 DASH。

它的命令行使用格式如下:

packager stream_descriptor[,stream_descriptor] [flags]
复制代码

比如用它查看视频信息:

packager in=friday.mp4 --dump_stream_info # 查看视频信息

File "friday.mp4":
Found 2 stream(s).
Stream [0] type: Audio
 codec_string: mp4a.40.2 # 音频的编码器字符串
 time_scale: 44100
 duration: 271360 (6.2 seconds)
 is_encrypted: false
 codec: AAC
 sample_bits: 16
 num_channels: 2
 sampling_frequency: 44100
 language: und

Stream [1] type: Video
 codec_string: avc1.4d401e # 视频的编码器字符串
 time_scale: 3000
 duration: 18500 (6.2 seconds)
 is_encrypted: false
 codec: H264
 width: 640
 height: 480
 pixel_aspect_ratio: 1:1
 trick_play_factor: 0
 nalu_length_size: 4
复制代码

更多请查看官方文档

FFmpeg

FFmpeg 是功能非常强大的视频处理开源软件,很多视频播放器就是使用它来做为内核。后面文章的实例都会使用这个工具。

比如上面将普通 MP4 转换为 FMP4,可以使用如下命令:

ffmpeg -i ./friday.mp4 -movflags empty_moov+frag_keyframe+default_base_moof f.mp4
复制代码

它的命令行格式如下:

ffmpeg [global_options] {[input_file_options] -i input_url} ... {[output_file_options] output_url} ...
复制代码

它可以有不限数量的输入和输出文件,-i 后面是输入 url,后面不能解析为参数的为输出文件。

 _______              ______________
|       |            |              |
| input |  demuxer   | encoded data |   decoder
| file  | ---------> | packets      | -----+
|_______|            |______________|      |
                                           v
                                       _________
                                      |         |
                                      | decoded |
                                      | frames  |
                                      |_________|
 ________             ______________       |
|        |           |              |      |
| output | <-------- | encoded data | <----+
| file   |   muxer   | packets      |   encoder
|________|           |______________|
复制代码

它将输入文件的容器解开,然后对里面的数据进行解码,然后按照指定的格式进行编码,然后使用指定的容器进行封装生成输出文件。在 decoded frames 后 FFmpeg 可以使用 filter 进行处理,比如添加滤镜、旋转、锐化等操作,filter 分为简单和复杂,复杂可以处理多个输入流。

如果我们只是想改变视频的容器,那么就可以省略解码和编码过程,来提升速度。

ffmpeg -i input.avi -c copy output.mp4
复制代码

-c 是指定编码器,-c copy 表示直接复制编码,-c:v 表示视频编码,-c:a 表示音频编码,比如 -c:v libx264 表示使用 CPU 将视频编码为 h.264-c:v h264_nvenc 则是使用 N卡,这样速度更快。-c:a copy 表示直接复制音频编码。

ffmpeg -y -i myvideo.mp4 -c:v copy -an myvideo_video.mp4
ffmpeg -y -i myvideo.mp4 -c:a copy -vn myvideo_audio.m4a
复制代码

-an 去除音频流 -vn 去除视频流。-y 是不经过确认,输出时直接覆盖同名文件。

ffmpeg -help #查看帮助
ffmpeg -i input.mp4 # 查看视频信息
ffmpeg -formats # 查看支持的容器
ffmpeg -codecs # 查看支持的编码格式
ffmpeg -encoders # 查看内置的编码器
复制代码

更多关于 FFmpeg 请查看官方文档

视频缩略图预览

了解了上面好用的工具,就来用 FFmpeg 来实现一个视频播放器小功能吧。

现在视频网站,当鼠标放到进度条上时就会出现,一个小缩略图来预览这个时间点内容。

ffmpeg -i ./test.webm -vf 'fps=1/10:round=zero:start_time=-9,scale=160x90,tile=5x5' M%d.jpg
复制代码

我们可以通过上面这个命令生成一个雪碧图,由 25 张 160×90 预览图组成。

-vf 参数后面跟着过滤器,多个过滤器用 , 分开,一个过滤器多个参数使用 : 分开。

fps=1/10 表示 10 秒输出一张图,fps=1/60 为一分钟一张,round=zero 时间戳向 0 取整,start_time=-9 是因为 fps 是每多少秒生成一张,并不是从 0 秒开始 -9 是让它从 1 秒开始截取,忽略掉 0 秒的黑屏帧。

scale=160x90 设置输出图像分辨率大小,tile=5x5 将小图用 5×5 的方式组合在一起,M%d.jpg 表示输出为 jpg,而且文件是 M1.jpg M2.jpg... 这样递增。

如果想用 NodeJS,可以用 node-fluent-ffmpeg 的 thumbnails 方法来生成。

有了雪碧图,我们就在上篇文章实现的播放器的基础上在加个视频缩略图功能。主要通过 css 的 background 来实现。

const thumb = document.querySelector('.thumb')
const gapSec = 10 // 一张图片显示几秒
const images = [...] // 图片
const row = 5, col = 5 // 一张图有几行几列
const width = 160, height = 90; // 缩略图的宽高
const thumbQuantityPerImg = col * row

function updateThumbnail(seconds) { // 传入要显示缩略图的秒数
    const thumbNum = (seconds / gapSec) | 0; // 当前是第几张缩略图
    const url = images[(thumbNum / thumbQuantityPerImg) | 0];
    const x = (thumbNum % col) * width; // x 的偏移量
    const y = ~~((thumbNum % thumbQuantityPerImg) / row) * height; // y 的偏移量

    thumb.style.backgroundImage = `url(${url})`;
    thumb.style.backgroundPosition = `-${x}px -${y}px`;
}
复制代码

效果如下

image.png

这里只展示缩略图更新的逻辑,忽略了时间显示,事件处理和样式等代码。

完整的代码请参考 NPlayer

在线演示:nplayer.js.org/

视频切片

有了 MSE 我们就可以将一个视频分割成多个小视频,然后可以自己控制缓存进度来节省流量,还可以将视频压缩成不同的分辨率,在用户网不好的情况动态加载码率低的分段,分成多段还可以实现插入广告,动态切换音频语言等功能。

./audio/
  ├── ./128kbps/
  |     ├── segment0.mp4
  |     ├── segment1.mp4
  |     └── segment2.mp4
  └── ./320kbps/
        ├── segment0.mp4
        ├── segment1.mp4
        └── segment2.mp4
./video/
  ├── ./240p/
  |     ├── segment0.mp4
  |     ├── segment1.mp4
  |     └── segment2.mp4
  └── ./720p/
        ├── segment0.mp4
        ├── segment1.mp4
        └── segment2.mp4
复制代码

当然也可以不切割视频,而使用 HTTP Range 来范围请求数据。

ffmpeg -i ./friday.mp4 -f segment -segment_time 2 -segment_format_options movflags=dash ff%04d.mp4
复制代码

我们使用上面命令将一个视频切成 2 秒的 fmp4 视频片段。

window.MediaSource = window.MediaSource || window.WebKitMediaSource
const video = document.querySelector('video')
const mime = 'video/mp4; codecs="avc1.4d401e, mp4a.40.2"'
const segments = [
    '/static/media/ff0000.5b66d30e.mp4', 
    '/static/media/ff0001.89895c46.mp4', 
    '/static/media/ff0002.44cfe1e4.mp4'
]
const segmentData = []
let currentSegment = 0

if ('MediaSource' in window && MediaSource.isTypeSupported(mime)) {
    const ms = new MediaSource()
    video.src = URL.createObjectURL(ms)
    mediaSource.addEventListener('sourceopen', sourceOpen)
}

function sourceOpen({ target }) {
    URL.revokeObjectURL(video.src)
    target.removeEventListener('sourceopen', sourceOpen)
    const sb = target.addSourceBuffer(mime)
    fetchSegment(ab => sb.appendBuffer(ab))
    sb.addEventListener('updateend', () => {
        if (!sb.updating && segmentData.length ) {
            sb.appendBuffer(segmentData.shift())
        }
    })
    video.addEventListener('timeupdate', function timeupdate() {
        if (
            currentSegment > segments.length &&
            !sb.updating &&
            target.readyState === 'open'
        ) { // 所有片段全部加载完成
            target.endOfStream()
            video.removeEventListener('timeupdate', timeupdate)
        } else if (
            video.currentTime > (currentSegment * 2 * 0.8) 
            // 一个片段时长 2 秒
            // 在一个片段播放到 80% 的时候才去请求下一个片段
        ) {
            fetchSegment(ab => {
                if (sb.updating) {
                    segmentData.push(ab)
                } else {
                    sb.appendBuffer(ab)
                }
            })
        }
    })
    video.addEventListener('canplay', () => {
        video.play()
    })
}

function fetchSegment(cb) {
    fetch(segments[currentSegment])
        .then(response => response.arrayBuffer())
        .then(arrayBuffer => {
            currentSegment += 1
            cb(arrayBuffer)
        })
}
复制代码

这个实例非常简单,并没有处理 seek,自适应码率等复杂功能。

总结

现在视频网站几乎全部都在使用 MSE 来播放视频。使用 MSE 有提供更好的用户体验,更加节约成本等好处。虽然视频播放一般使用 hls dash 等协议的开源客户端来播放视频,我们自己不会使用到 MSE,但这些客户端底层都是使用 MSE,了解 MSE 才更了解这些客户端。

这是弹幕播放器系列文章第二篇,上篇请查看 从零开发弹幕视频播放器1。下篇请查看 原来爱优腾等视频网站都是用这个来播放流媒体的

作者:wopen
链接:https://juejin.cn/post/6953777965838630926

通过PHP脚本自动登录WP

WordPress的密码不记得了怎么办?除了通过邮件找回密码、登录phpmyadmin修改密码以外,还可以通过PHP脚本自动登录Wordpress。比如创建一个autologin.php放到wp根目录下,然后访问这个文件,就自动用指定的用户名登录站点,随后删除该文件即可。

为什么要自动登录WordPress

  • 知道网站管理的用户名以及cpanel信息,不知道密码,也不能修改用户的密码。
  • 本地开发图省事,不想每次都要登录站点。

自动登录Wordpress的PHP脚本

将如下内容写入一个文件,名字叫什么都可以,比如autologin.php,放到网站根目录下。

<?php
require('wp-blog-header.php');
// Automatic login //
$username = "admin";
$user = get_user_by('login', $username );

// Redirect URL //
if ( !is_wp_error( $user ) )
{
    wp_clear_auth_cookie();
    wp_set_current_user ( $user->ID );
    wp_set_auth_cookie  ( $user->ID );

    $redirect_to = user_admin_url();
    wp_safe_redirect( $redirect_to );
    exit();
}

然后打开浏览器,输入该文件地址,例如:

https://www.solagirl.net/autologin.php

这样就能用指定的用户名自动登录WordPress了,在线站点上请勿保留此文件。

让Wordpress时刻保持登录状态

如果要让WordPress不论何时都自动登录,可以用下面的代码,代码写成插件或者放到主题的functions.php里。

function auto_login() {

    if( is_user_logged_in() ){
        return;
    }
    $loginusername = 'admin'; //username of the WordPress user account to impersonate

    // get this username's ID
    $user = get_user_by( 'login', $loginusername );

    if( ! $user ){
        return;
    }

    $user_id = $user->ID;

    // login as this user
    wp_set_current_user( $user_id, $loginusername );
    wp_set_auth_cookie( $user_id );
    do_action( 'wp_login', $loginusername, $user );

    // redirect to home page after logging in
    wp_redirect( home_url() );
    exit;
}

add_action( 'wp', 'auto_login', 1 );

这段代码的作用:只要访问站点,就会自动登录。适合某些特殊用途,比如本地站点测试不想重复登录。