跳转至

Welcome

进位位和溢出位(carry bit/overflow bit)

这两个标志位是运算器(alu)的组成部分,因为运算器在执行加法时,要关注两个问题:是否进位超了;是否溢出了。这两个问题很相似,但有区别。

加法和减法

对于减法而言,实际是将减数翻转,然后执行加法。即:

1 - 1 = 1 + (-1)

运算器做减法时要比较两数的绝对值大小再进行减法,符号设置成大的数的符号,例如

2 - 1 => 2 - 1 = 1 => 符号位设置成2的符号位
1 - 2 => 2 - 1 = 1 => 符号位设置成1的符号位

数字在内存中存放的是“补码”形式,补码很多好处,对于正数而言,源码、反码、补码的二进制形式相同。补码统一了+0和-0的形式,而且补码适合做减法运算。

+0/-0
原码: 0000 0000/ 1000 0000
反码: 0000 0000/ 1111 1111 正数不变,负数的符号位不变,其它位取反
补码: 0000 0000/10000 0000 正数不变,负数先转反码再加1,进位位丢掉后相同

进位标志位

进位位用来标志两数相加产生的进位超过了位数,例如 1+1 = 10,要多一位,如果运算器是8位的,执行 11111111+1的运算,就要进位,且超过了位数,这时候进位位就会置1。

判断方法 Carry = (Result >> n) & 1

溢出标志位

溢出位只针对有符号数的运算,有符号数的第一位表示正负,0代表正,1代表负,其它位表示值。当值的部分想加产生进位时,导致符号位覆盖。

判断方法(就8位而言) Overflow = (Result [7bit] XOR Op1[7bit] XOR Op2[7bit])

溢出位置1时,虽然符号为不正确,但其值是正确的(包含符号位)。

参考

源码/反码/补码 进位位/溢出位

firefox 启用手动添加搜索引擎

  1. Open a new tab and type about:config in the address bar
  2. In the search box type: browser.urlbar.update2.engineAliasRefresh
  3. Click on the little + symbol on the right. This is what it should look like after you pressed it: boolean true value after pressing plus sign
  4. Go to firefox Settings → Search. Or enter this in the address bar: about:preferences#search
  5. In the "Search Shortcuts" section you should notice a new "add" button.

参考

https://superuser.com/questions/7327/how-to-add-a-custom-search-engine-to-firefox

streaming h264 with rtp

这里记录了使用ffmpeg来发送h264的rtp流,主要问题是处理pps和sps的发送,看了非常多的文档和例子包括gptchat,直到用gdb跟ffmpeg才找到解决办法。

背景

公司的有个发送视频彩铃的业务,需要向终端发送h264。开始想法是创建ffmpeg进程来发送,不过发现进程太好资源并发上不去。后来用写代码来多线程发送。

sdp协商转码问题

sdp协商结果会有不同的分辨率、等级、质量之类的参数(pps/sps),为了避免转码,提前制作了不同参数的视频。不过后来发现只要正确发送pps/sps,终端都能正确解码,不是必须按sdp里的视频参数。

用ffmpeg发送

开始直接使用命令ffmpeg发送,发现终端不能解码。对比正常的rtp流发现缺少了pps/sps。

ffmpeg -i video.mp4 -an -c:v copy -f rtp rtp://ip:port
一番搜索发现ffmpeg将pps/sps等参数写到sdp中(out-of-band),还用base64编码了。
a=fmtp:96 packetization-mode=1; sprop-parameter-sets=Z2QAKKzRAHgCJ+XAWoCAgKAAAAMAIAAAB4HjBiJA,aOvvLA==; profile-level-id=640028

ffplay等播放软件会解析sdp,载入pps/sps,所以正确解码,但是sip场景下只能通过rtp来发送sps/pps(in-band)。 后来发现用ffmpeg的'bit stream filter'能解决问题

ffmpeg -i video.mp4 -an -c:v copy -bsf h264_mp4toannexb -f rtp rtp://ip:port

即使不转码,使用ffmpeg进程发送视频,在两核的系统中大概只能发送十几路。

代码实现发送

基于ffmpeg的示例代码'doc/example/remux.c', 将原来写文件改为rtp即可。因为还没有用到'h264_mp4toannexb',还不会发送pps和sps。

关键问题就是如何使用这个bsf

网上看到的例子包括用gptchat生成的例子,都是类似下面的步骤。

# 搜索bsf
av_bsf_get_by_name("h264_mp4toannexb")
# 创建bsf的上下文
av_bsf_alloc(bsf, &bsf_ctx)
# 从输入的format上下文中复制编码参数
avcodec_parameters_copy(bsf_ctx->par_in, input_ctx->streams[video_stream_idx]->codecpar)
# 初始化bsf上下文
av_bsf_init(bsf_ctx)
# 读入packet
av_read_frame(input_ctx, &pkt)
# 送到bsf中处理
av_bsf_send_packet(bsf_ctx, pkt)
# 取出处理后的pkt
av_bsf_receive_packet(bsf_ctx, pkt)
# 发送rtp

抓包发现还是没有发送PPS/SPS, 而且第一个NALU是SEI,并且是坏的(Malformed)。调试发现bsf确实成功将SEI从AVCC转换成了AnnexB形式,也在SEI后追加了PPS和SPS。

(gdb) x/150bx pkt->data
0x5555556da310: 0x00    0x00    0x00    0x01    0x06    0x05    0x2e    0xdc
0x5555556da318: 0x45    0xe9    0xbd    0xe6    0xd9    0x48    0xb7    0x96
0x5555556da320: 0x2c    0xd8    0x20    0xd9    0x23    0xee    0xef    0x78
0x5555556da328: 0x32    0x36    0x34    0x20    0x2d    0x20    0x63    0x6f
0x5555556da330: 0x72    0x65    0x20    0x31    0x35    0x35    0x20    0x72
0x5555556da338: 0x32    0x39    0x30    0x31    0x20    0x37    0x64    0x30
0x5555556da340: 0x66    0x66    0x32    0x32    0x00    0x80    0x00    0x00
0x5555556da348: 0x00    0x01    0x67    0x64    0x00    0x28    0xac    0xd1
0x5555556da350: 0x00    0x78    0x02    0x27    0xe5    0xc0    0x5a    0x80
0x5555556da358: 0x80    0x80    0xa0    0x00    0x00    0x03    0x00    0x20
0x5555556da360: 0x00    0x00    0x07    0x81    0xe3    0x06    0x22    0x40
0x5555556da368: 0x00    0x00    0x00    0x01    0x68    0xeb    0xef    0x2c
0x5555556da370: 0x00    0x00    0x01    0x65    0x88    0x84    0x02    0xff
0x5555556da378: 0x91    0x3c    0x4a    0x51    0x5b    0xfd    0x02    0x3f
0x5555556da380: 0xc1    0x67    0x8d    0xc0    0x94    0x98    0xee    0x7d
0x5555556da388: 0x43    0x23    0xc0    0x4f    0xf7    0x56    0x37    0xfc
0x5555556da390: 0xf1    0xf3    0xd3    0x83    0x03    0xa9    0x6d    0xd2
0x5555556da398: 0x07    0xcf    0x19    0xa2    0x1e    0x29    0x64    0xfe
0x5555556da3a0: 0x1f    0x8e    0xd6    0x71    0x5f    0x33
0x00 0x00 0x00 0x01是annexb格式的起始码,第一个NALU是0x06(SEI),第二个NALU是0x07(SPS),第三个是0x08(PPS)。问题出在rtp打包的ff_rtp_send_h264_hevc。这里判断出s->nal_length_size不是0而是4, 所以还是以AVCC格式的首四个字节代表长度来解析pkt,而这是pkt是annexB格式了,前四个字节就是0x00, 0x00, 0x00, 0x01。所以打包错误。 问题怎么使rtp按annexB来打包,为什么nal_length_size是4不是0。

void ff_rtp_send_h264_hevc(AVFormatContext *s1, const uint8_t *buf1, int size)
{
    const uint8_t *r, *end = buf1 + size;
    RTPMuxContext *s = s1->priv_data;

    s->timestamp = s->cur_timestamp;
    s->buf_ptr   = s->buf;
    if (s->nal_length_size)
        r = ff_avc_mp4_find_startcode(buf1, end, s->nal_length_size) ? buf1 : end;
    else
        r = ff_avc_find_startcode(buf1, end);

找到初始化rtp的初始化函数rtp_write_header,发现设置nal_length_size的地方,原来判断extradata,如果第一个字节是1,则按avcc打包。

    case AV_CODEC_ID_H264:
        /* check for H.264 MP4 syntax */
        if (st->codecpar->extradata_size > 4 && st->codecpar->extradata[0] == 1) {
            s->nal_length_size = (st->codecpar->extradata[4] & 0x03) + 1;
        }
        break;
而当前的extradata是类似avcc的(又不同于AVCC, 因为多一个header),第一个字节等于1,而通过调试ffmpeg_g到这里时,extradata是annexB格式,即首4个字节是0x00,0x00,0x00,0x01。关于extradata格式的问题,从开始我就发现刚初始化时,extradata就是0x01开头,那么问题ffmpeg_g的extradata什么时候变的,再次调试发现,bsf初始化跟上面的不同。在ffmpeg_g中bsf初始化在fftools/ffmpeg_mux.c文件中:
# fftools/ffmpeg_mux.c
static int bsf_init(MuxStream *ms)
{
    OutputStream *ost = &ms->ost;
    AVBSFContext *ctx = ms->bsf_ctx;
    int ret;

    if (!ctx)
        return avcodec_parameters_copy(ost->st->codecpar, ost->par_in);

    ret = avcodec_parameters_copy(ctx->par_in, ost->par_in);
    if (ret < 0)
        return ret;

    ctx->time_base_in = ost->st->time_base;

    ret = av_bsf_init(ctx);
    if (ret < 0) {
        av_log(ms, AV_LOG_ERROR, "Error initializing bitstream filter: %s\n",
               ctx->filter->name);
        return ret;
    }

    ret = avcodec_parameters_copy(ost->st->codecpar, ctx->par_out);
    if (ret < 0)
        return ret;
    ost->st->time_base = ctx->time_base_out;

    ms->bsf_pkt = av_packet_alloc();
    if (!ms->bsf_pkt)
        return AVERROR(ENOMEM);

    return 0;
}
发现,ffmpeg的bsf初始化多一个步骤,将bsf的par_out拷贝到输出AVstream中。而这par_out中的extradata就是我们要的annexB格式!
ret = avcodec_parameters_copy(ost->st->codecpar, ctx->par_out);

问题找到答案了,1是初始化rtp muxer前先初始化bsf,2是初始化bsf后将par_out拷贝回rtp muxer,再初始化rtp muxer。

正确例子

基于ffmpeg的doc/example/remux.c 删除了错误处理。只关心h264,所以输入输出都只处理index=0的包。

int main(int argc, char **argv) {
  const AVOutputFormat *ofmt = NULL;
  AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
  AVPacket *pkt = NULL;
  const char *in_filename, *out_filename;
  int ret = 0;
  in_filename = "video.mp4";
  out_filename = "rtp://127.0.0.1:10020";
  pkt = av_packet_alloc();
  ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0);
  ret = avformat_find_stream_info(ifmt_ctx, 0);
  av_dump_format(ifmt_ctx, 0, in_filename, 0);

  // 创建输出rtp上下文,不初始化
  avformat_alloc_output_context2(&ofmt_ctx, NULL, "rtp", out_filename);
  // 初始化bsf
  const AVBitStreamFilter *bsf_stream_filter =
      av_bsf_get_by_name("h264_mp4toannexb");
  AVBSFContext *bsf_ctx = NULL;
  ret = av_bsf_alloc(bsf_stream_filter, &bsf_ctx);
  ret =
      avcodec_parameters_copy(bsf_ctx->par_in, ifmt_ctx->streams[0]->codecpar);
  ret = av_bsf_init(bsf_ctx);

  ofmt = ofmt_ctx->oformat;
  AVStream *out_stream = avformat_new_stream(ofmt_ctx, NULL);
  // 关键在这!! 原来是从ifmt_ctx的stream中拷贝codecpar,改成从bsf中拷贝
  // ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
  ret = avcodec_parameters_copy(out_stream->codecpar, bsf_ctx->par_out);
  out_stream->codecpar->codec_tag = 0;
  av_dump_format(ofmt_ctx, 0, out_filename, 1);
  if (!(ofmt->flags & AVFMT_NOFILE)) {
    ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
  }
  // 初始化bsf后再初始化rtp
  ret = avformat_write_header(ofmt_ctx, NULL);

  while (1) {
    AVStream *in_stream, *out_stream;
    ret = av_read_frame(ifmt_ctx, pkt);
    if (pkt->stream_index != 0) {
      continue;
    }
    in_stream = ifmt_ctx->streams[pkt->stream_index];
    out_stream = ofmt_ctx->streams[pkt->stream_index];
    log_packet(ifmt_ctx, pkt, "in");

    av_bsf_send_packet(bsf_ctx, pkt);
    av_bsf_receive_packet(bsf_ctx, pkt);
    /* copy packet */
    av_packet_rescale_ts(pkt, in_stream->time_base, out_stream->time_base);
    pkt->pos = -1;
    log_packet(ofmt_ctx, pkt, "out");
    ret = av_interleaved_write_frame(ofmt_ctx, pkt);
  }

  av_write_trailer(ofmt_ctx);
}

h264_mp4toannexb

这个bsf将从mp4文件中读取的avcc格式的packet转换成annexb格式的packet。调试发现第一个读出来的包是SEI一个I帧,通过bsf处理后,会在SEI后面追加上PPS和SPS信息。而读取mp4文件时,sps/pps在extradata中。

相关知识

这些知识反复看来看去,总也不能贯穿起来,直到问题解决才算明白。

NALU

NALU是真正用来保存h264视频信息的,包括I帧,P/B帧,PPS,SEI,SPS等。NALU由两部分组成:头(1字节)和payload,头中包含nalu的类型。h264规范只定义了NALU本身单元,但没有定义怎么保存NALU单元,所以有了两种格式保存NALU,AVCC和AnnexB。

NALU类型

NAL unit type的值和说明,类型后面跟payload。详细参见在Rec. ITU-T H.264文件的63页,这里展示常用的 * 5,Coded slice of an IDR picture (I帧) * 6,Supplemental enhancement information (SEI) * 7,Sequence parameter set (SPS) * 8,Picture parameter set (PPS) * 24,Single-Time Aggregation Packet(STAP-A)

从抓包看到SPS算上payload的长度为30,PPS算上payload的长度为4。不知道长度是不是固定的。

STAP-A

STAP-A是多个NALU的聚合(Aggregation),即这个NALU的payload里是多个NALU。STAP-A类型的NAL用来发送PPS/SPS/SEI等多种聚合。因为这些单元都很小。STRAP-A类型的header也是一个字节,但是payload里面有多个NALU,并且每个NALU前面用2字节来表示这个NALU的大小。

|STAP-A header|NALU-1 size|NALU-1|NALU-2 size|NALU-2|

NALU-1中又有header和payload。

AVCC和AnnexB

上面说了规范没有定义怎么保存NALU,所以有了这两个格式,他们两是平等关系,只有保存的格式不同而已。AVCC用来保存,annexB用来流传输。 * AVCC用1~4个字节来表示NALU的长度,长度后面是NALU。读取方法是先读长度,再读取NALU。再读下一个长度,再读下一个NALU... * 而annexb用0x00,0x00,0x00,0x01或者0x00,0x00,0x01的起始码(start code)来分隔不同的NALU,所以方法是先读起始码,再一直读,直到发现下一个起始码,表示这个NALU结束,下一个NALU开始。

ffmpeg使用中发现,AVCC一般用4字节表示NALU的长度,具体多少字节,在ffmpeg的extradata中有定义。annexB也是用4字节的起始码,也就是0x00,0x00,0x00,0x01。

Fragmentation Units (FUs) 分片

FU就是网络分片,因为I帧是一个完整的图片,所以非常大,为了保证udp不丢包,所以要分次发送。 第一个分片的FU的头设置了Start bit, 最后一个分片的FU头设置了END bit。分片是在rtp muxer中完成,注意ffmpeg中一个packet可以包含多个音频帧,但是只包含一个帧,直到发送rtp之前,一个packet总是完整的一帧视频(I/P/B)。 对于比较小的packet,例如聚合了PPS/SPS等信息的STAP-A包,不需要分片。

extradata

上面很多次提到ffmpeg的extradata, 就是AVCodecParameters.extradata,它的长度是AVCodecParameters.extradata_size。在读取mp4文件的时候,ffmpeg会自动填充,在解码rtp的时候可能就需要手动填充了。extradata的比特位如下,首先是6字节的头,然后是多个SPS类型的NALU(2字节的长度分割多个NALU),再然后是PPS类型的NALU个数,最后是PPS类型的多个NALU(2字节的长度分割多个NALU)。

bits    
8   version ( always 0x01 )
8   avc profile ( sps[0][1] )
8   avc compatibility ( sps[0][2] )
8   avc level ( sps[0][3] )
6   reserved ( all bits on )
2   NALULengthSizeMinusOne
3   reserved ( all bits on )
5   number of SPS NALUs (usually 1)

repeated once per SPS:
  16         SPS size
  variable   SPS NALU data

8   number of PPS NALUs (usually 1)

repeated once per PPS:
  16       PPS size
  variable PPS NALU data

里面包含了一个或多个PPS/SPS(NALU),但保存的格式,既不是AVCC也不是AnnexB。因为上面可知AVCC用14个字节表示NALU的长度,AnnexB用34字节的起始码,而extradata是有一个6字节的header,里面有个字段叫NALULengthSizeMinusOne就是定义了AVCC使用多少个字节来表示NALU的长度。如果NALULengthSizeMinusOne等于0,那么AVCC用1字节表示NALU的长度。通常就是用4字节。

extradata例子
(gdb) x /150bx fmt_ctx->streams[0]->codecpar->extradata
0x55555571d9c0: 0x01    0x64    0x00    0x28    0xff    0xe1    0x00    0x1e
0x55555571d9c8: 0x67    0x64    0x00    0x28    0xac    0xd1    0x00    0x78
0x55555571d9d0: 0x02    0x27    0xe5    0xc0    0x5a    0x80    0x80    0x80
0x55555571d9d8: 0xa0    0x00    0x00    0x03    0x00    0x20    0x00    0x00
0x55555571d9e0: 0x07    0x81    0xe3    0x06    0x22    0x40    0x01    0x00
0x55555571d9e8: 0x04    0x68    0xeb    0xef    0x2c    0x00    0x00    0x00
0x55555571d9f0: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x55555571d9f8: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x55555571da00: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x55555571da08: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x55555571da10: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x55555571da18: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x55555571da20: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x55555571da28: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x55555571da30: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x55555571da38: 0xc1    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x55555571da40: 0x9d    0xad    0x00    0x00    0x50    0x55    0x00    0x00
0x55555571da48: 0xfa    0x19    0xc4    0x92    0x40    0xa0    0xdc    0xba
0x55555571da50: 0x00    0x00    0x00    0x00    0x00    0x00
  • 第5字节0xff,二进制为:1111 1111, 后2位表示NALULengthSizeMinusOne=3,所以ffmpeg用4字节表示NALU的大小(AVCC格式)。
  • 第6字节0xe1,二进制为:1110 0001,后5位表示SPS的个数=1,所以只有一个SPS。
  • 第7,8字节表示SPS的长度,0x00,0x1e,二进制为:0000 0000, 0001 1110,所以SPS长度为30。
  • 跳过30个字节,0x01,二进制为:0000 0001, 表示有1个PPS。
  • 后面的2个字节,0x00,0x04,二进制为:0000,0004,表示PPS的长度为4个字节。
NALU头的解析

NALU头是1字节,第一位F bit, 后两位NRI bit, 后五位表示NALU的type。

SPS的头是0x67,而进制为: 01100111,所以type值正好是7。 PPS的头是0x68,而进制为: 01101000,所以type值正好是8。

对比wireshark解析结果可以确认上面的理解正确,可见extradata就是6个字节的header加多个AVCC格式的NALU。

创建extradata

rtp解码时,需要手动生成extradata。创建AVCC格式的extradata

write(0x1);  // version
write(sps[0].data[1]); // profile
write(sps[0].data[2]); // compatibility
write(sps[0].data[3]); // level
write(0xFC | 3); // reserved (6 bits), NULA length size - 1 (2 bits)
write(0xE0 | 1); // reserved (3 bits), num of SPS (5 bits)
write_word(sps[0].size); // 2 bytes for length of SPS
for(size_t i=0 ; i < sps[0].size ; ++i)
  write(sps[0].data[i]); // data of SPS

write(&b, pps.size());  // num of PPS
for(size_t i=0 ; i < pps.size() ; ++i) {
  write_word(pps[i].size);  // 2 bytes for length of PPS
  for(size_t j=0 ; j < pps[i].size ; ++j)
    write(pps[i].data[j]);  // data of PPS
}

创建annexB格式的extradata

write(0x00)
write(0x00)
write(0x00)
write(0x01)
for each byte b in SPS
  write(b)

for each PPS p in PPS_array
  write(0x00)
  write(0x00)
  write(0x00)
  write(0x01)
  for each byte b in p
    write(b)

wireshark解析

从wiresshark对比用bsf和不用bsf的抓包发现,SEI包的内容没有变化,是不是可以不需要转成annexb,直接将PPS/SPS直接拷贝到packet中发出去呢?

参考

https://membrane.stream/learn/h264/3 https://github.com/cisco/openh264/issues/2501#issuecomment-231340268 https://stackoverflow.com/questions/17667002/how-to-add-sps-pps-read-from-mp4-file-information-to-every-idr-frame https://stackoverflow.com/questions/24884827/possible-locations-for-sequence-picture-parameter-sets-for-h-264-stream https://aviadr1.blogspot.com/2010/05/h264-extradata-partially-explained-for.html

Notes

笔记本的笔记整理和誊抄

主要是关于ffmpeg的知识总结

FFmpeg

读法"F-F-M-派格", 由三个可执行文件组成"ffmpeg/ffplay/ffprobe"。

ffmpeg

  • 支持的容器 ffmpeg -muxers 分为Demuxing和Muxing(D/E)封装和解封装,支持Muxing肯定支持Demuxing。
  • 支持的编码 ffmpeg -codecs 分为解码和编码(decoder/encoder),支持编码一般能支持解码。
  • 容器帮助信息 ffmpeg -h muxer=mp4 ffmpeg -h demuxer=mp4 加上-h full查看更多信息
  • 编码帮助信息 ffmpeg -h encoder=h264 ffmpeg -h decoder=h264 加上-h full查看更多信息
  • 支持的滤镜 ffmpeg -filters 这里的滤镜不是PS等图片编辑软件里面的滤镜,而应该称做特效,支持视频和音频还有字幕。

当转码或转容器格式时,-map参数用来手动选择流,参数格式 -map n:m:x 其中n表示选择第n个输入,m代表第n个输入中的第m个流,而x表示第n个输入中的第m个流的第x个通道。当没有-map参数时,ffmpeg会根据容器类型自动选择合适的流。-map还能用来选择filtergraph中的滤镜输出。

filter 滤镜

分为 filtergraph filterchain 和 filter * filtergraph 是包含很多个filter的有向图,每两个滤镜之间都可以有多个连接。 * filter 分为source filter, sink filter, filter,其中source filter没有输入端,sink filter没有输出端。

多个filter用","来连接,形成filterchain,而多个chain用";"来连接,形成fitergraph。

简单滤镜格式参数格式 -f:v 或 -f:a [输入流或标记名]滤镜参数[临时标记名];(重复n个)。复杂滤镜格式 -filter_complex overlay滤镜用来设置显示层次,例如[overlay]filter 将输入显示在另一个上面。

加速

使用滤镜来加速播放

加速视频

使用setpts来加速视频 -f:v setpts=0.5*PTS, 注意:1调整范围[0.25,4],越小越快,从加速4倍到减速4倍。2若只是调整视频则将音频关掉。3对视频加速时,如果不想丢帧,则使用-r参数调整输出的FPS。

加速音频

最简单的方法是调整采样率,但这样会改变音色,一般使用对原音进行重采样。 -f:a atempo:2.0。范围 [0.5,2.0],若要4倍加速,使用滤镜组合 -f:a atempo=2.0,atempo=2

同时加速音频和视频

可以用 -filter_complex "[0:v]setpts=0.5*PTS[v];[0:a]atempo=2.0[a]" -map [v] -map [a] 。也可以组合使用上面的两个滤镜。

视频裁剪

-ss 选项seek,从开头跳过一段片长,参数两种形式 00:00:00或者秒数。-ss设置在输入对象和输出对象上有不同效果。 * -ss 指定在输入时,不仅可以用于复制,也可以用于转码。并且都是基于关键帧来寻找位置,默认启用选项“frame_accurate" * -ss 指定在输出时,源文件依旧会每帧都要解码并丢弃,知道跳过指定的时间所以要等待。但是-SS用在输出时,最大的优点是当使用滤镜时,其timestamp不会重置0,这个在录制字幕时有用,不需要修改字幕的时间戳。

在裁剪时有两个选项 frame_accurate_seek 和 -noaccurate_seek 区别, 后者会使用附近的关键帧。
-ss -t 与 -ss -to的区别,-ss 10 -t 20表示截取10s~30s的视频,而 -ss 10 -to 20 表示截取10s~20s的视频。

截取视频不准确问题: * 当使用-ss和-c:copy时,由于ffmpeg强制使用I帧,所以可能调整起始时间到负值,即提前于ss设置的时间 * 例如 -ss 157 但直到159时才有I帧,它会有2秒的时间只有声音没有画面。

编译问题
  • 编译ffmpeg支持h265, 因为h265库是用c++写的,所以编译ffmpeg时要加上额外库-lstdc++
  • 如果pc文件不在标准路径,需要修改环境变量export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:"pc文件路径"
  • 如果链接到了动态库,可以使用enable-rpath设置运行时找库路径

ffplay 播放媒体文件

  • 在视频中插入字幕 ffplay -f:v "subtitle=input.srt" input.mp4
  • 音频可视化 ffplay -showmode 1 input.mp3 默认是0, 用傅立叶变换显示声音的频率频谱,处理时长差不多是1s。
  • 视频显示运动方向 ffplay -flags2 +export_mvs -vf codecview=mv=pf -i input.mp4

ffplay 指定视频显示大小,当播放原始视频时如input.h264,用-video_size来指定视频大小,而非设置播放器显示大小。如果要控制播放器显示大小用 scale/resize滤镜。

-s 选项,当用在输入选项时,可以替代video_size。当用在输出选项时,可以替代scale滤镜,但只能于filtergraph最后的filter,若想作用在filtergraph其它位置,则必须显示使用scale。

时间戳 PTS/DTS

PTS是显示时间戳, presentation timestamp 指表示渲染的时间点,而DTS是Decode timestamp,解码时间戳,表示解码时间点。因为视频的B帧要等待下一个I帧,所以B帧会先保存在缓冲区。 在ffplay和ffmpeg 播放/转码视频时,控制台显示了tbn,tbc,tbr, 这些都是timebase,也叫时间精度。每次处理后当前的时间戳会增加一个timebase。 * tbn: timebase of AVstream, 表示从容器中读取的timestamp的增加一次timebase。 * tbc: timebase of codec, 表示某个流的对应的编码采用的时间基,每次解码都增加一次timebase。 50tbc时,1S有50帧。 * tbr: 一帧的timebase,是预估值。

ffprobe 查看媒体文件信息

视频分析软件除了ffprobe还有 mp4info。

直播

视频直播技术里, HLS和DASH等分片技术比较流行。

rtp流

rtp是通信语音行业的常用协议,可以发送视频和语音之一,不可以同时发送视频和语音,语音也只支持单通道(对于PCM类型的语言而言,而rtp支持OPUS发送多通道)。rtp有多流同步机制,接收终端可以将多个rtp流合并,支持类似webrtc的会议模式,rtp也能支持5G的视频通话。

rtp的sdp

在建立rtp连接之前,两个终端要协商一个两边都支持的视频和语音编码,否则无法建立。 sdp则描述了ip/端口/媒体编码相关的信息,这些不能通过rtp协议来传送(in-band)。

clockrate 时钟频率

sdp中的a属性有clok

ffmpeg发送接收rtp

rtp对于ffmpeg而言是一种容器,与其它的mp4/avi类似,代码实现也同样在libavformat里。只是不同在于前着是写文件,rtp容器则是发送网络包。例如读取mp4文件并发送音频的命令ffmpeg -i input.mp4 -vn -ac 1 -f rtp rtp://ip:port-vn表示不发送视频,-ac 1表示合并音频多通道为单通道。

常见分辨率

640x480(480p)分辨率比4:3 1280x720(720p HD) 1920x1080(1080p FHD) 3840x2160(4k UHD)

h264

I/P/B 三种类型的帧: * I帧, Index frame, 是完整的一幅图片,不依赖其它帧就能解码显示。 * P帧, Delta frame, 是向前帧,依赖I帧来解码。只有运动的对象,而没有背景信息。 * B帧, Bidirectional pridict picture, B帧则依赖P帧和下一个I帧来解码I->P->B->I,最节省字节。 所以视频种的B帧越多,视频文件的体积越小,而解码的复杂度越高。

ffmpeg转码h264的命令,ffmpeg -i input.mp4 -av sample-rate -crf {17~23} -b:v video-bitrate -r frame-rate -profile {baseline|main|high} * -av 指定sample rate采样率 * -crf 指定视频质量 * -b:v video bitrate 视频的比特率 * -b:a audio bitrate 音频的比特率 * -r frame rate 指定帧率 * -profile 指定视频的档次 * -level 指定视频等级

使用baseline prfile时,不会包含B帧,当使用实时流媒体直播时,采用baseline编码相对main和high相对可靠,但加入B帧可以减小比特率。

视频格式P/I

p代表progressive代表逐行扫描,I代表interlaced 隔行扫描,但是一旦视频损坏时,视频几乎无法观看。

音视频同步

音频和视频都是独立的线程处理,主要通过各自PTS来同步,但实际上音视频大部分情况是不同步的,偶尔是同步的。当音频和视频时间误差超过阈值时,就会去重同步。重同步主要是调整视频,因为人眼对视频的敏感度不如对音频的敏感度。

ffmpeg播放h264时,限制比特率(bps,bit per second)

视频的比特率分为两个码率,CBR恒定比特率,VBR波动比特率。互联网视频多为VBR。如果想用CBR,-b:v 设置编码比特率,但这里设定的平均值,不能很好控制最大和最小码率。要控制最大最小比特率,需要组合使用 -b:v,maxrate,minrate。另外还要设置buff打小-bufsize。例如:

ffmpeg -i input.avi -bo 15M -minrate 0.5M -maxrate 0.5M -bufsize 1M out.mkv
bufsize 说明,如果不使用bufsize,其变化范围将比我们的预期大很多。当设置bufsize很大时浮动范围比较大,设置太小时,会导致视频质量降低。最合适的大小时-b:v的大小一半,然后逐渐增加bufsize,直到bitrate变化比较明显时,这是质量最高而且比较恒定的大小。

通过比特率来计算文件大小:

视频大小 = 比特率 * time_in_second / 8 未压缩音频大小 = 采样率 * 采样深度 * channel数 * time_in_second / 8 压缩音频文件大小 = 比特率 * time_in_second /8

图片

alpha 通道是RGB通道外的一个通道,用来表示像素的透明度,是另一个维度。 * 当使用16bit的位图时,对于每个像素5bit表示红,5bite表示绿,5表示蓝,最后一个bit表示alpha。所以这时只有1/0选择,所以只有透明和不透明的。 * 当使用32bit的位图时,8bit来表示alpha通道,就有0~255个值表示不同程度的透明。而alpha的值不直接表示透明度而是通过与RGB三个通道的值相乘,这样得道了显示的值。

高频洗劫低频轮廓 outline

在傅里叶边换中,用不同频率相位的正弦波,可以模拟出各种类型的信号。其中高频信号越高,还原效果越好。

Avframe 存储一帧解码后的像素数据,Avpacket 存储一帧压缩的数据。

音频pcm

  • 采样:是将音频模拟信号定时取值,得到一个离散序列。为了能将离散序列恢复成为模拟信号供播放器播放,采样频率要是声音频率的至少两倍。所以若要采样1k频率的声音,需要用2k采样率。
  • 采样深度:单个采样的储存长度。
  • 音频帧:单位时间里的多个采样。

数学

差分和微分 difference equation / differential equation

差分是离散,各个连续项之间不存在其它元素。例如1,2,3,4,5 不存在1.1,1.2,1.3。而微分是连续的,1和2之间可以有1.1,1.11,1.12,1.2等。总结是差分是微分的离散化。

信号和系统

离散傅里叶边换 DFT discret fourier transform,傅里叶变换出来的结果是复数=X+YI,I^2=-1。 谐波是什么,为何傅里叶变换要复数,因为表示波的方法要用到频率和相位还有振幅。 复数模等于振幅 e^(jwb) = cos(wb) + isin(wb)

波的表示方法: y=Asin(B(x+C)), A振幅,B频率,C相位。

使用iwd创建ap热点

windows中有很多软件能创建热点,现在Linux也可以创建热点了,下面记录方法

确认网卡支持AP

使用iw list查看wifi网卡信息,如果在Supported interface modes能找到AP,说明网卡支持创建热点。

使用iwd

之前研究的wifi棒子,都是用wpa_supplicant来创建热点。而在我自己的电脑里一直使用的intel的iwd。 1. 先修改iwd的配置文件/etc/iwd/main.conf

[General]
EnableNetworkConfiguration=true
2. 在iwd保存密码的目录创建ap子目录
mkdir -p /var/lib/iwd/ap/
3. 在ap目录创建配置文件名字如main.ap,内容如下
[Security]
Passphrase=password123

[IPv4]
Address=192.168.250.1
Gateway=192.168.250.1
Netmask=255.255.255.0
DNSList=8.8.8.8
4. 执行iwctl命令进入到iwd交互模式
[iwd]# device wlan0 set-property Mode ap
[iwd]# ap wlan0 start-profile main
[iwd]# 
5. 启动内核转发
# sudo sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
# sudo iptables -t nat -A POSTROUTING -s 192.168.250.0/24 -j MASQUERADE

我做完上面的步骤后,发现wifi连接不上,也有报错。然后重启了iwd服务,发现没有了main这个热点,所以这个热点不会自动启动。在执行4步骤后,就能正常连接wifi了。

ad-hoc 和 ap 的区别

ad-hoc是去中心化的组网方式,各连接节点可以直接通讯,而有些终端可能不支持。AP模式又叫做infrustructure Mode,就是类似传统的路由器模式,所以兼容性更好。

使用总结

这个ap不稳定,长时间使用后没有网络, 需要重建ap, 不推荐。最终还是装了个便宜路由器MI-mini。老旧路由器的性能差,当路由(NAT)的丢包率太高, 但当桥接入口(交换机)正常。

参考

https://iwd.wiki.kernel.org/ap_mode

database acronyms

常常被几个缩写搞晕,今天有空好好查了下。

CRUD 增删改查

web后端代名词,CREATE READ UPDATE DELETE

ACID 事物操作的四个特性

Atomic 原子性,一次事物包含多个操作,这些操作要么都成功,要么都失败。 Consistency 一致性 Isolation 隔离性,多个事物之间按优先级并发,互不影响 Durability 持久性,数据储持久化,存储在磁盘上而非内存中

CAP 分布式系统的不可能三角的定理

Consistency 一致性,保证所有节点的数据状态都一致 Availbility 可用性,保证能对外服务 Partition tolerance 分区容忍性,可以接受系统出现分区,俗称脑裂。

CAP是被证明的定理,不可能三角表示同时最多只能保证两个,而不能保证三个。三种情况解释如下: 1. 多节点的系统不能保证没有故障,就需要系统能接受系统出现分区(脑裂)。当出现分区时,需要从C一致性和A可用性中二选一,当选择一致性时,就放弃可用性。因为网络有分区无法保证查询信息是一致的,所以请求将返回失败。 2. 当选择可用性时,系统将每次都尝试返回最新的结果,即使由于分区导致数据不一定是最新的。 3. 而当没有出现分区时,可以同时保证一致性和可用性。

简而言之1保证了C+P,2保证了A+P,3保证了C+A

udp 查看统计的方法

  • /proc/net/snmp
  • /proc/net/udp

除此之外,还有通常使用netstat/ss 来查看发送队列和接收队列的缓冲区占用。

/proc/net/snmp

cat /proc/net/snmp | grep Udp\:
输出格式 Udp: InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrors

  • InDatagrams recvmsg调用时,值会增加。
  • NoPorts 发送到没有监听的端口,值会增加
  • InErrors 没有内存,或者checksum失败
  • OutDatagrams 成功发送到IP层,注意这不代表包成功发送出去了,因为在IP层可能有错误,比如路由错误,会导致IP层的OutNoRoutes值增加

/proc/net/udp

cat /proc/net/udp
输出格式 sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops

  • sl hash slot for the socket
  • local_address 16进制的ip.端口
  • st socket state
  • tx_queue 发送队列的内存大小
  • rx_queue 接收队列的内存大小
  • inode socket对应的inode,可以判断哪个进程打开了这个socket
  • drops 只记录了接收方向被丢掉的包,不包括发送方向丢掉的包。

参考

https://blog.packagecloud.io/monitoring-tuning-linux-networking-stack-sending-data/#monitoring-udp-protocol-layer-statistics

Linux创建热点

iptables 用来配置转发数据包规则的工具,Linux的防火墙配置就是利用的iptables。普通的路由器也是使用的iptables来实现NAT地址转换。

使用iptables转发流量

路由器使用的局域网ip,例如可使用的局域网有3类 * A类地址:10.0.0.0 - 10.255.255.255 ==> 10.0.0.0/8 * B类地址:172.16.0.0 - 172.31.255.255 ==> * C类地址:192.168.0.0 -192.168.255.255 ==> 192.168.0.0/16

公网访问路由LAN,常见防火墙后的服务器

假设公网客户(8.8.8.8)访问路由器下的服务(192.168.0.2)。请求先到达路由的公网地址假设1.1.1.1, 此时的ip层源地址和目的地址分别为(8.8.8.8/1.1.1.1), 然后路由器需要将目的地址改写为192.168.0.2,此时ip包的地址为(8.8.8.8/192.168.0.2),这需要的iptable规则为

iptables -t nat -A PREROUTING -d 1.1.1.1 -j DNAT --to-destination 192.168.0.2
ip包就能正常到达192.168.0.2,处理完成之后,ip包变成(192.168.0.2/8.8.8.8),这是可能有几种情况: * 没有路由规则能匹配8.8.8.8,则会被丢弃。 * 一般会发送到默认网关,然后由网关发送正确发送到8.8.8.8,但8.8.8.8客户不认识源地址192.168.0.2,所以包会被丢弃 * 直接在第一个路由上被丢弃,因为192.168.0.2是一个私有地址,不能被转发。

所以需要另外一条iptables规则,将192.168.0.2发出的ip包改成(1.1.1.1/8.8.8.8)

iptables -t nat -A POSTROUTING -s 192.168.0.2 -j SNAT --to-source 1.1.1.1

Reference

https://serverfault.com/questions/627608/redirect-all-incoming-traffic-from-a-secondary-public-ip-to-an-internal-ip-addre

随身wifi工具刷机

本文记录了淘宝卖的随身wifi刷机成Debian,还能收发短信。还有救砖的艰辛过程,不过结局美满。

依赖

  • edl备份恢复工具,https://github.com/bkerler/edl, 下载源代码后执行git submodule update --init --recursivepip install -r requirements.txt
  • openstick编译好的debian https://github.com/OpenStick/OpenStick。 在release页面下载 base.zip, debian.zip, firmware-ufi001c.zip,boot-ufi001c.img
  • apt 安装 adb fastboot

切卡

刚接触时看网上的都是在后台页面切卡,但是刷机了没有后台页面如何切卡呢? 为了切卡又重刷安卓,导致变砖了。参考网上资料发现,Debian下可以直接切。

debian默认卡槽位1:

echo 1 > /sys/class/leds/sim:sel/brightness
echo 0 > /sys/class/leds/sim:en/brightness
echo 0 > /sys/class/leds/sim:sel2/brightness
echo 0 > /sys/class/leds/sim:en2/brightnessR
modprobe -r qcom-q6v5-mss
modprobe qcom-q6v5-mss
systemctl restart rmtfs
systemctl restart dbus-org.freedesktop.ModemManager1.service

esim槽位3:

echo 0 > /sys/class/leds/sim:sel/brightness
echo 0 > /sys/class/leds/sim:en/brightness
echo 1 > /sys/class/leds/sim:sel2/brightness
echo 0 > /sys/class/leds/sim:en2/brightness
modprobe -r qcom-q6v5-mss
modprobe qcom-q6v5-mss
systemctl restart rmtfs
systemctl restart dbus-org.freedesktop.ModemManager1.service

其他两个槽位

槽位2:

echo 0 > /sys/class/leds/sim:sel/brightness
echo 1 > /sys/class/leds/sim:en/brightness
echo 0 > /sys/class/leds/sim:sel2/brightness
echo 0 > /sys/class/leds/sim:en2/brightness
modprobe -r qcom-q6v5-mss
modprobe qcom-q6v5-mss
systemctl restart rmtfs
systemctl restart dbus-org.freedesktop.ModemManager1.service

槽位4:
echo 0 > /sys/class/leds/sim:sel/brightness
echo 0 > /sys/class/leds/sim:en/brightness
echo 0 > /sys/class/leds/sim:sel2/brightness
echo 1 > /sys/class/leds/sim:en2/brightness
modprobe -r qcom-q6v5-mss
modprobe qcom-q6v5-mss
systemctl restart rmtfs
systemctl restart dbus-org.freedesktop.ModemManager1.service

有些棒子esim槽位不一样槽位3不行自行试试槽位2 4
https://github.com/OpenStick/OpenStick/issues/49#issuecomment-1568202001

刷机

先使用edl全量备份,方法在最下面。先刷base包,解压base.zip后执行里面的flash.sh。然后刷debian包,解压debian.zip后执行里面的flash.sh。这时进入系统发现可能出现问题(我也没明显感觉到问题),因为他们是ufi001b的。 所以再刷适配ufi001c的包,解压firmware-ufi001c.zip。

cd firmware-ufi001c
adb push ./* /lib/firmware
adb reboot bootloader
fastboot flash boot boot-ufi001c.img
fastboot reboot
但是在刷ufi001c的包之前我能找到modem网络,刷完就没了,所以有了下面基带恢复的步骤。

基带

基带文件在随身wifi的modem分区,先用edl备份modem分区。当然下面介绍了edl全量备份,跳过了userdata,估计是root和home目录。

恢复基带文件

我在安装debian系统时,发现移动网络不可用,于是参考网上说的,将原始基带恢复后正常。 基带文件名为"modem.bin", edl全量备份后在dumps目录下。直接mount该文件,然后将modem.*和mba.mbn取出来,当发现刷机后发现移动网络不能使用时,重新拷回去就能用了。

sudo mount modem.bin /mnt
复制到firmware-ufi001c目录,覆盖之前的文件。
cp /mnt/modem.* .
cp /mnt/mba.mbn .

再次拷贝到wifi棒子的/lib/firmware目录, 配合再刷一次适配001C的boot文件。

adb push ./* /lib/firmware
adb reboot bootloader
fastboot flash boot boot-ufi001c.img
fastboot reboot #重启

然后就能找到modem网卡了。

重启modem

有时重刷了firmware,还是出现没有modem网卡的情况,也就是mmcli -m 0提示找不到modem设备的时候,需要执行下面的命令。或者直接等一会重启ModemManager

systemctl stop ModemManager
qmicli -d /dev/wwan0qmi0 --uim-sim-power-off=1 && qmicli -d /dev/wwan0qmi0 --uim-sim-power-on=1
systemctl start ModemManager

短信收发和转发邮件

下载两个python文件,地址 https://gitee.com/jiu-xiao/ufi-message。配置smtp.py中的邮箱信息,我使用的QQ邮箱登录,然后转发到另一个邮箱。开始发送不成功,修改后能发送了,内容如下。

#!python3

my_sender='@qq.com'
my_user='@hotmail.com'
server_address='smtp.qq.com'
server_port=465
server_passwd='' #填写qq邮箱授权码


import smtplib
from email.mime.text import MIMEText
from email.utils import formataddr
import datetime
import time

def mail(text):
    msg=MIMEText(text,'plain','utf-8')
    msg['From']=formataddr(["随身Wifi",my_sender])
    msg['To']=formataddr([my_user,my_user])
    msg['Subject']="转发 "+time.strftime("%H:%%M:%S %m/%d")

    server=smtplib.SMTP_SSL(server_address,server_port)

    server.login(my_sender,server_passwd)
    server.auth_plain()
    server.sendmail(my_sender,[my_user,],msg.as_string())
    server.quit()

然后就能写进crontab自动跑了。

救砖

edl使用

github上的开源工具,可以替代Windows那些高通工具,Miko,星海啥的。是我这Linux用户的福音,并且简单,windows常常失败。 使用edl工具之前要先重启到edl也就是9008模式,使用命令adb reboot edl,不需要按RST键。

备份:推荐第一种方式,第二种方式生成的一个文件刷到另一个设备失败,但是可以刷MiKo导出的单文件。

edl rl dumps --skip=userdata --genxml
or
edl rf flash.bin #单文件

恢复:

edl qfil rawprogram0.xml patch0.xml .

恢复分区

刷了debian后,分区会变,这样导致有些分区不存在所以无法刷回去。这里介绍下恢复分区的方法。不过上面的edl恢复应该不需要这个操作。

备份分区

./edl gpt . --genxml
备份后有两个文件,恢复时只用gpt_main0.bin文件,恢复分区命令
./edl w gpt gpt_main0.bin
打印分区来验证下

./edl printgpt

参考

https://forum.openwrt.org/t/uf896-qualcomm-msm8916-lte-router-384mib-ram-2-4gib-flash-android-openwrt/131712/160 https://www.kancloud.cn/handsomehacker/openstick/2636505

pve-usage

这里记录pve的使用

lxc 配置

pve使用lxc来运行容器而不是docker。 docker对应的“镜像”概念在pve里叫模板(template),默认的pve容器模板是从官网下载,非常慢,可以替换成清华源。另外在模板列表里面的模板都是比较新的,老版本可以上清华源上找,例如centos7,然后通过url来上传,怎样都非常方便。

容器网络问题

有的容器模板没有安装红帽的NetworkManger,这导致创建容器时选择的DHCP网卡配置不会生效,即使给网卡设置静态ip也不行。有个解决办法是启动后再加一个静态ip的网卡,这样容器就有网络访问权限了,然后赶紧安装NetworkManger吧。

低版本容器启动不能进入console的问题

如果systemd的版本低于232,启动容器时pve提示:WARN: old systemd (< v232) detected, container won't run in a pure cgroupv2 environment!
也打不开控制台,只能通过在宿主机上执行 pct enter id来进入容器。网上有帖子说在容器里面执行软件更新后,可以恢复,也许是systemd版本更新了,但是这个方法不适用于centos7。最后只能通过修改宿主机的grub,修改启动参数:

#/etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT="systemd.unified_cgroup_hierarchy=0 quiet"
再执行update-grub命令,最后重启宿主机后问题解决。

低版本容器问题

pve的容器ENTERPOINT都是/sbin/init,跟我现在公司的管理docker方式相同。 会启动systemd,包括sshd等,对于远程访问非常方便。但是版本低的容器启动时不仅无法进入控制台,而且不会启动systemd。手动使用systemctl启动sshd时会失败,因为dbus没有启动,而启动dbus也失败。总之新版本容器就不会有这个问题,总之要配置启动参数。

对于通过/sbin/init来启动sshd的方式值得讨论,因为通过在Dockerfile中设置ENTRYPOINT service ssh restart && bash也可以有相同的效果。而公司的大量容器都启动systemd时,会启动udev服务,这时硬件故障,直接导致宿主机卡死。

map directory to lxc container

pct set 110 -mp0 /srv/music,mp=/srv/music

虚拟机配置声音

我安装过一个win7的虚拟机作为测试环境用,而且需要使用音频,在添加音频网卡时,使用了SPICE驱动,会导致虚拟机无法启动,必须将显示器的驱动也改成SPICE。然而我使用remmina来远程访问这个虚拟机,听不到声音。 最后将音频驱动改为None,然后remmina的advance标签卡的Auido output mode设置Local就能听到声音了,虽然声音质量不佳,不过勉强能用!

参考

  • https://forum.proxmox.com/threads/solved-warn-old-systemd-v232-detected-container-wont-run-in-a-pure-cgroupv2-environment.114736/
  • https://stackoverflow.com/questions/25135897/how-to-automatically-start-a-service-when-running-a-docker-container/32179054#32179054