跳转至

2024

连词和复合句

英语中,句子(sentence)通常由好个子句子组成,这叫复合句(compound sentense), 由连词和子语句(Clause)、短语(Phase)、单词(Word)组成。

子句和短语

子句包含完整的主谓, 但短语没有主和谓, 有好多个phase组成。比较长度 Clause > Phase > Word, word很好区别。 短语是不完整的, 而子语句包含了完整的主谓(宾)。

Phrase: Meows so loudly
Clause: That cat meows so loudly

子句当名词

  • She completely understood everything he said. 拆成主谓宾'She understood XXX', 'everything he said' 是宾语XXX, 是一个名词
  • They remembered what the keynote speaker covered. 同上

子句当副词

  • She fixed the sink without facing difficulty. 拆成主谓宾'She fixed XXX'。'without facing difficulty'修饰动词'fix', 是副词
  • The pool was installed after they built the deck. 拆成主谓 'The poll was adj'。 'after they built the deck'是修饰形容词(或was), 是副词

子句当形容词

  • My friend who rides a motorcycle said it’s a great weekend to ride. 拆成主谓宾'XXX said XXX'。 'who rides a motorcycle' 形容词,形容'My Friend'
  • No matter what time of day we visit, we always run into the dog that barks. 同上, 形容'dog'

子句当名词和形容词时,比较好看出子句的主谓(宾), 而当副词

短句是几个单词组成,可以当很多成分

  • She runs every Sunday. 当副词
  • She was taller than all of her classmates. 当形容词
  • My small dog barks at ducks. 名词

连接词

连接词用来连接子句和短语

一致关系

for, and, nor, but, or, yet, so

FANBOYS

配对关系, 搭配用

both/and, either/or, neither/nor, not only/but, whether/or

主从关系

after, although, as, as if, as long as, as much as, as soon as, as though, because, before, by the time, even if, even though, if, in order that, in case, in the event that, lest, now that, once, only, only if, provided that, since, so, supposing, that, than, though, till, unless, until, when, whenever, where, whereas, wherever, whether or not, while

按功能分连接词

表示强调

indeed

表示递进

what's more, furthermore

表示转折

however, but

表示原因结果

because, as a result

yet 相关

yet 一直让我脑子转不过弯, 不知道怎么翻译成中文。例如歌名'Are you bored yet?', 没yet是‘你无聊吗?’。 当有yet时我以为翻译成’你还无聊吗?‘, 而结果翻译成‘你觉得无聊了吗?’

gptchat解释“yet表示提问者预期如此,也就是提问者预期被提问者无聊了。”, 所以正确理解应该是: “你应该无聊了吧?” 显然这里yet是副词,修饰bored。

当副词, 修饰形容词和动词

any yet 但是 nor yet 也不 ; 又不 ; 也没有 but yet 但还是 not yet 还没 why is there yet another problem

当连词, 连接子句和短语

Yet she 而她已经 Yet Moralistic 但注重道德

参考

https://www.grammarly.com/blog/parts-of-speech/conjunctions/ https://www.grammarly.com/blog/sentences/phrases/ https://www.grammarly.com/blog/grammar/clauses/

linux子进程使用方法

Linux的exec(3)是一系列函数。之前总是稀里糊涂地用,这次搞清楚了。

       #include <unistd.h>

       extern char **environ;

       int execl(const char *pathname, const char *arg, ...
                       /*, (char *) NULL */);
       int execlp(const char *file, const char *arg, ...
                       /*, (char *) NULL */);
       int execle(const char *pathname, const char *arg, ...
                       /*, (char *) NULL, char *const envp[] */);
       int execv(const char *pathname, char *const argv[]);
       int execvp(const char *file, char *const argv[]);
       int execvpe(const char *file, char *const argv[], char *const envp[]);

规律

这6个函数可以分为两组'execl组'和'execv组'。 后缀p表示从$PATH中查找程序, 后缀e表示最后一个参数是数组,里面是环境变量。

验证

这里通过c语言和python调用命令cat hello.txt, 验证每个函数。

#include <unistd.h>

int main(){
  int pid=-1;
  pid=fork();
  if(pid == 0)
  execl("/usr/bin/cat", "cat", "hello-execl.txt", NULL);

  pid=fork();
  if(pid == 0)
  execlp("cat", "cat", "hello-execlp.txt", NULL);

  pid=fork();
  if(pid == 0)
  execle("/usr/bin/cat", "cat", "hello-execle.txt", NULL, (char*[]){NULL});

  pid=fork();
  if(pid == 0)
  execv("/usr/bin/cat",  (char*[]){"cat", "hello-execv.txt", NULL});

  pid=fork();
  if(pid == 0)
  execvp("cat",  (char*[]){"cat", "hello-execvp.txt", NULL});

  pid=fork();
  if(pid == 0)
  execve("/usr/bin/cat",  (char*[]){"cat", "hello-execve.txt", NULL}, (char*[]){NULL});
}
注意当使用选项参数时,选项和参数需要分开成两个字符串。例如grep -n 10 "target" file命令, 调用方法是
execlp("grep", "grep", "-n", "10",  "target", "file", NULL);

使用环境变量,要用sh来拓展ENV1符号

execle("/usr/bin/sh", "sh","-c" ,"echo $ENV1", NULL, (char*[]){"ENV1=HELLO", NULL});

python 的os库的方法更全,且不需要带NULL

>>> [method for method in dir(os) if method.startswith("exec")]
['execl', 'execle', 'execlp', 'execlpe', 'execv', 'execve', 'execvp', 'execvpe']

实现cat hello.txt

import os
os.execlp("cat", "cat", "hello.txt")

使用环境变量, 同样要用sh来拓展ENV1符号

import os

env = os.environ.copy()
env["ENV1"]="hello"
os.execlpe("sh", "sh", "-c", "echo $ENV1", env)

实用替代方法

exec族的函数都会替换当前进程的堆栈,所以需要用fork生成新的进程。更方便的方法是在c中用system, python中用subprocess

arduino-cli 使用

esp32中,使用了arduino-cli来编译和上传代码。这里记录下arduino-cli操作arduino nano的方法和问题

编译

说也非常奇怪,Linux的java GUI版本arduino编译提示找不到头文件'SoftwareSerial.h', 这个库是arduino自带的。但是不使用GUI的编译按钮而用GUI的输出中的命令编译又正常。

这是从GUI的IDE的编译上传的输出日志,得到的编译上传命令, 手动执行却正常。

cd ~/Arduino
BUILD_PATH=$(mktemp -d)
arduino-builder -verbose -compile -hardware /usr/share/arduino/hardware -tools /usr/share/arduino/hardware/tools/avr -libraries /
./Arduino/libraries  -build-cache ./arduino_cache -build-path $BUILD_PATH  -fqbn=arduino:avr:uno  -prefs=build.warn_data_percentage=75 ./TestSerial/TestSerial.ino
avrdude -C/etc/avrdude.conf -v -patmega328p -carduino -P/dev/ttyUSB0 -b115200 -D -Uflash:w:${BUILD_PATH}/TestSerial.ino.hex:i 

用arduino-cli编译也正常

arduino-cli compile --fqbn  arduino:avr:nano   TestSerial 

上传失败

但是用arduino-cli编译正常,上传失败,提示

arduino-cli upload --fqbn arduino:avr:uno --port /dev/ttyUSB0 TestSerial                                                                                                            
avrdude: error at /etc/avrdude.conf:402: syntax error                                                                                                                                           
avrdude: error reading system wide configuration file "/etc/avrdude.conf"                                                                                                                       
Failed uploading: uploading error: exit status 1   

后来用verbos输出对比GUI版本的命令,发现两个命令用的avrdude不一样。 一个是/usr/bin/avrdude, 一个是$HOME/.arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17/bin/avrdude。两个的版本不一样, 而后者使用了前者的配置文件 /etc/avrdude.conf。所以出现syntax error

arduino-cli upload -v  --fqbn arduino:avr:uno --port /dev/ttyUSB0 TestSerial                                                                                                        
"/home/jimery/.arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17/bin/avrdude" "-C/etc/avrdude.conf" -v -V -patmega328p -carduino "-P/dev/ttyUSB0" -b115200 -D "-Uflash:w:/home/jimery/.ca
che/arduino/sketches/57AA4FA906132DC236F48CCE34943FA0/TestSerial.ino.hex:i"                                                                                                                     

avrdude: Version 6.3-20190619                                                                                                                                                                   
         Copyright (c) 2000-2005 Brian Dean, http://www.bdmicro.com/                                                                                                                            
         Copyright (c) 2007-2014 Joerg Wunsch                                                   

         System wide configuration file is "/etc/avrdude.conf"                                                                                                                                  
avrdude: error at /etc/avrdude.conf:402: syntax error                                                                                                                                           
avrdude: error reading system wide configuration file "/etc/avrdude.conf"                                                                                                                       
Failed uploading: uploading error: exit status 1 

最后全部卸载了GUI包括avrdude, 再将$HOME目录的avrdude配置复制到/etc/avrdude.conf, 问题解决了。

玩转ESP32

ESP32的生态比较好,类似arduino, 但是集成了wifi和蓝牙,有的甚至集成了gps,功能很强大而且便宜。最近淘宝买了一个ESP32-USB-Geek, 带一个LCD屏幕和几个扩展接口,还有闪存口可以读写。

esp32-usb-geek.png.png

编译烧录

ESP32 有3种方式: 官方的idf, mpy, arduino。

官方工具

官方的idf分vscode插件和python脚本, vscode中没有成功,不过idf.py可以编译和烧录。

根据官方文档写的下载安装idf,解压后执行脚本。安装脚本依赖python-venv, 而我最常用vituralenv

bash v5.3.1/esp-idf/install.sh

进入淘宝店例子, 用cmake管理的。 在跟目录执行

idf.py set-target esp32-s3

这种方式支持非常复杂的配置

idf.py menuconfig

编译刷入

idf.py build

idf.py -p /dev/ttyACM0 flash

官方的方式最能了解物理设备了。看日志发现他生成了bin文件,并从0位置刷入bin文件。

mpy thonny ide

最好用pip安装最新的。 将python上传到esp后,点运行就可以了,还能在REPL中仿真调试,非常方便。

arduino

arduino ide gui版本编译失败,而且因为是Linux版是用java写的,操作非常慢。改用arduio-cli脚本成功, 官方文档也说了arduino-cli非常强大。这里说脚本使用方法。

初始化配置
  1. 首先下载压缩包,解压到PATH目录下。

  2. 初始化配置, arduino-cli config init 可以生成配置文件'~/.arduino15/arduino-cli.yaml'。

  3. 修改配置文件, 根据淘宝老板说的,ESP32的板子要从另外第三方源中, 因为这个源是githubcontent需要梯子才能访问,修改成如下

$ cat ~/.arduino15/arduino-cli.yaml

board_manager:

additional_urls: ['https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json']



network:

proxy: 'socks5://ip:port'
安装板子库和依赖库

当没有下载板子时,查看连接的设备会显示Unknown

$ arduino-cli board list

Port Protocol Type Board Name FQBN Core

/dev/ttyACM0 serial Serial Port (USB) Unknown

显示搜索所有板子(包括在添加的第三方源里的), 这里搜索ESP32核心板, 淘宝老板说,我的开发板叫 ESP32S3 Dev Module

$ arduino-cli board listall ESP32S3 Dev Module

ESP32S3 Dev Module esp32:esp32:esp32s3

安装板子module, 比较大要等待一会。 tips: 板子的包叫module, 软件依赖库叫library

$ arduino-cli core install esp32:esp32

搜索已安装的安装核心板

$ arduino-cli core search esp32

ID Version Name

arduino:esp32 2.0.18-arduino.5 Arduino ESP32 Boards

esp32:esp32 3.0.7 esp32
编译

编译淘宝店的例子LCD_Button,要在LCD_Button外面执行

arduino-cli compile --fqbn esp32:esp32:esp32s3 LCD_Button

上传到板子

arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:esp32s3 LCD_Button

有时候失败了,要按住Boot键插入电脑再松开,就能正常上传。

esptool.py v4.6

Serial port /dev/ttyACM0

Connecting...

Traceback (most recent call last):

...

OSError: [Errno 71] Protocol error

Failed uploading: uploading error: exit status 1

看这个日志,原来arduino也是调用的官方的idf工具的esptool脚本。 重新插入USB, 再次查看已连接的开发板就不会显示Unknown了

$ arduino-cli board list

Port Protocol Type Board Name FQBN Core

/dev/ttyACM0 serial Serial Port (USB) ESP32 Family Device esp32:esp32:esp32_family esp32:esp32

连接串口UART

烧录了淘宝店给的串口收发例子,代码非常简单, 然后用我的USB转ttl工具测试成功。

import machine



uart = machine.UART(1, baudrate=115200, tx=machine.Pin(43), rx=machine.Pin(44))



def send_data(data):

uart.write(data)



def receive_data():

if uart.any():

data = uart.read()

print("Received data:",data)




while True:

send_data("Hello UART")

receive_data()

然而这里是不对的, 串口转TTL和ESP的USB都要连接到电脑上。单连一个都是进了mpy的repl。

usb-ttl-esp32-uart.png.png

用 wpasupplicant 管理wifi连接

用树莓派管理wifi连接的方法有 : * 通过配置文件/etc/wpa_supplicant/wpa_supplicant.conf * raspi-config * 其它工具iwctl nmtui等

后来发现配置文件管理wifi的优先级不生效,不能选择网络,当然可以用nmtui很方便,不过要安装新包。后来发现了wpa_cli工具很方便,来自安装包wpasupplicant

wpa_cli

先要选择网络接口,树莓派就是wlan0。 * 查看可用网络 wpa_cli -i wlan0 list_networks 这个命令会打印可用网络的id和名称 * 选择某个网络 wpa_cli -i wlan0 select_networks <网络id>

wpa_cli 还有交互模式

参考

https://superuser.com/a/759153

pve中给windows扩容

当初创建windows时给的40G硬盘,就一个分区,后来装了一些软件,发现不够用了,就第一次扩容,然后加了另一个分区。后来有些东西必须放在C盘里,这次扩容就比较复杂些。

第一次扩容

pve的web界面点击硬盘的resize可以修改硬盘大小,这个硬盘实际上是一个lvm块设备,而对于虚拟机Windows而言就是硬盘变大了。

硬盘加大后,进入windows磁盘管理,发现Windows已经有3个区:efi,C盘,恢复分区。为了方便直接加了一个D盘。

备注:pve的虚拟机安装在块设备上,而qemu是用文件(raw or qcow2),所以qemu的虚拟机可以复制移动,不过pve支持异地部署。

第二次扩容

这次扩容是为了增加C盘大小,而C盘后面有恢复分区和D分区。不得不在gparted 中执行: 1. 删除D分区 2. 移动恢复分区到磁盘末尾 3. 拓展C盘大小。 gparted操作很方便,不过windows启动不成功,通过windows安装iso修复之后正常了,恢复分区也还在。

virtual vnc 可视化打开gparted

因为pve没有连接显示器,只能用虚拟vnc,远程连接到pve桌面。

安装tigervnc服务端

pve只需要安装服务端,tigervnc-standalone-server。本地电脑安装客户端,选择很多例如 remmina。

启动

创建密码后启动vnc 服务端失败,提示没有桌面。原因是没有安装桌面环境更没有启动桌面,好在tigervnc 支持指定可视化进程(xVNC就不支持这种方式)。

tigervncserver -xstartup /usr/sbin/gparted  --  /dev/dm-15

这里直接给的gparted可执行文件路径和参数,--表示后面的参数是给gparted的,不是给tigervncserver的,/dev/dm-15是要操作的windows磁盘,也能在gparted界面里面再选。

remmina设置窗口大小

tigervncserver默认启动1024x768的窗口,当内容多时不方便。重新启动

tigervncserver -geometry 1920x1080 -xstartup /usr/sbin/gparted  --  /dev/dm-15

但是重新连接发现窗口还是很小,以为参数不生效。后来发现这时候窗口右下角可以拖动,手动拖成满屏

gparted问题

启动gparted后出现没有光标,后面按了鼠标右键就恢复X光标了。arch wiki里面有解决办法

sed和vim替换文本的小区别

替换招行的账单中发现vim和sed区别

在vim中 1. :%s/\n/,/g 2. :% s/\t,\t,\t,\t,/\r/g 用sed: cat bill.txt| sed -z 's/\n/,/g' | sed 's/\t,\t,\t,\t,/\n/g'

可见

  1. vim中匹配换行符用\n, 而替换用\r。替换成\n在vim中显示成^@
  2. sed 中的换行符统一用\n,这比vim好,但sed 中不能直接用\n匹配,还得加上 -z 参数。

进位位和溢出位(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