阅读视图

发现新文章,点击刷新页面。
🔲 ⭐

相机小述

看了看,好像自己确实是太懒了,不过虽然这样,说的时候还是要把锅甩给疫情的,想起以前接触相机这么久了,不如就还记得的一点东西,介绍一下,水一点东西,混一次提交。
不过不知道有没有帮助,不是介绍什么拍摄三要素(快门、光圈、感光度)的控制,也不是什么九宫格拍摄法之类的,说实话,那些五花八门的拍摄方法我也不太熟悉,就讲讲相机结构。

传感器类型

图像传感器主要有两种,线阵和面阵,平时看到的线阵相机和面阵相机就是因为用的Sensor类型不同。
线阵Sensor以CCD为主,一行的数据可以到几K甚至几十K,但是高度叧有几个像素,行频很高,可以到每秒几万行,适合做非常高精度、宽画幅的扫描。
面阵Sensor,包含CCD和CMOS,单行数据宽度远小于线阵,优点是成像快,一次可以成像很多行,即单次扫描的像素高度比线阵高很多。另外开发简单,成像快,不用进行每行数据的拼接。而且价格便宜不少,这个大概是应用更广泛的决定性条件。

快门类型

CMOS

CMOS传感器的大致结构如图,由于其感光方式不同,因此有两种类型快门。



Rolling Shutter(滚动快门)

滚动快门在感光时是逐行进行的,从第一行开始,一边曝光一边输出图像,一直滚动到最后一行,模数转换器则是逐行共用。这样,实际上每一行曝光的时间点会不一样,时间上存在一个位移,这样导致每行图像会有一些位移偏差,特别是当对象是高速运动物体时更明显,会导致图像的扭曲变形。这种变形和平时拍摄运动物体的拖影不一样,拖影是由于拍摄物体运动速度太快,而且曝光时间设置太长造成,会带有一些图像模糊。滚动快门的变形是每行时间上的不同步拍摄而造成的变形,图像清晰度不会受到影响。

Global Shutter(全局快门)

为了改善这种变形,则可以在每个像素处增加采样保持单元或者模数转换器。增加采样保持单元可以短暂保存得到的模拟数据,再等待模数转换器进行逐行转换,转换期间可以继续进行拍摄。而这种方案会浪费比较多的CMOS面积来摆放这些用来短暂保存的像素,使得填充系数降低,而且采样保持单元还引入了新的噪声源。因此在每个像素处增加模数转换器则是一种新的方案(如索尼做的),每个像素采集到了就可以直接转换,不用等待,实现真正意义上的全局快门。

CCD

CCD(电荷耦合器件)的结构大致如图,其结构决定了CCD具有“免费全局快门”的优点,所有像素在同一时刻曝光,所有像素同时移入传输寄存器,曝光完成后,每个像素被串行传输到单个模数转换器中,因此其传输帧率受限于单个像素数字化速率和传感器中的像素数量。



光学尺寸

光学尺寸是指其感光区域的大小,通常高分辨率的面阵相机或者线阵相机的相机芯片尺寸要大于低分辨率的,其尺寸没有特定标准,是有其分辨率和像素大小决定的。从理论上讲,可以有无数种类型,只要价格到位。尺寸通常用感光元件的对角线来表示,单位是英寸,不过由于历史原因,这里的1英寸是16mm,不是25.4mm。常见的有1/4”、1/3”、1/2.5”、1/2.3”、1/2”、2/3”、1”、1.1”等。
介绍光学尺寸的原因是,通常为相机选择镜头的时候要考虑到,镜头靶面尺寸要配合其光学尺寸。

靶面尺寸

靶面尺寸,或者靶面直径,单位也是英寸,理想情况下,1/2”的镜头应该安装在1/2”的光学芯片上,这样可以尽可能的利用靶面,但是如果安装在2/3”的芯片上,由于感光区域大于感光范围,那么感光区域中无法感光的部分则会在最终的图像中出现暗角或者晕影。不过如果采用2/3”的 镜头匹配1/2”的芯片,则可以完全利用光学尺寸,实际上使用大的镜头可以形成更大的靶面,图像从中心到边缘的锐化可以保持一致,但这种情况下,很大一部分罢免无法使用,造成浪费。图像的大小是有光学尺寸决定的,而镜头越大,则价格越贵,如果想节约点,对于比较小的光学尺寸,还是选择较小的镜头。

镜头接口

镜头接口是连接镜头和相机的接口,有螺纹接口和卡口两类。比较常见的C口、CS口等都是螺纹接口。
最常见的C口和CS口的工业相机,接口实际上比较相似,也有其转换环,因为它们的接口直径、螺纹间距都一样,只是法兰距不同。C接口的法兰距是17.526mm,CS接口的法兰距为12.5mm。因此所谓转接环,就是一个5mm左右的垫圈了。
此外,螺纹接口还有M12、M45、M58等,具体规格在需要的时候查询即可,这里不在赘述。
至于卡口相机,平常见到的单反基本上都是卡口,如尼康的F口或者佳能的EF口,这俩外观上也不容易区分,不过F口的法兰距比EF口的要长。

分辨率

分辨率,泛指量测或显示系统对细节的分辨能力。相机制造商一般直接用像素数目表示分辨率,实际上这是分辨率上限。
因为这种情况,是当镜头能够解析像素大小时候才成立。只有使用高分辨率镜头,才能最终得到高分辨率图像。
镜头的分辨率通常通过每毫米线对数衡量,表示每毫米中可以相互分离的行的数量。每毫米线对数越多,分辨率越高,镜头质量越好。镜头分辨率确定了可以解析的像素大小,方便起见,一般情况下直接指定镜头可以解析的百万像素数,当镜头分辨率可以完全解析感光元件的所有像素点时,则可以获得最高分辨率。
表示镜头分辨率性能的指标有MTF曲线(调制传递函数),描述了镜头从图像中心到边缘的分辨率性能,通常可以找制造商要到这些曲线。

焦距

焦距是镜头光学中心和焦点之间的距离,通常长焦镜头适合拍远景,但视场小;短焦适合拍广角,常用的鱼眼或者微距镜头就是。

光圈

光圈的参数通常用F Number来表示,是焦距与光圈直径的比值,表示光圈全开时的宽度。
光圈的选择直接影响的是进光量,最终影响的是图像质量和亮度。F值越高,则光圈越小,最终感光元件获得的进光量越少,反之亦然。通常可以根据光源亮度调整。

减小光圈,可以减少相机光晕效果,景深越大,不过光圈太小,容易产生衍射模糊。

帧存和缓存

带帧存功能的相机,是指该相机内部具有保存一帧完整图像的能力,当传输带宽不够或者不稳定时,由于缓存了整个图像帧,所以仍然可以断点续传之后重建图像。
带缓存功能的相机,是指该相机内部具有缓存一部分图像数据的能力,但是无法缓存一整个帧,当传输带宽不够或者不稳定时候,有可能造成缓存溢出,最后无法重建图像从而造成丢帧等问题。
平常见到的工业相机一般都是带缓存的,不一定有帧存,在结构和价格上也有区别。只带缓存的相机结构简单,价格便宜。

此外,还有一些简单的参数,如相机图像的帧率FPS,图像的亮度、饱和度、对比度等等,由于比较常见,顾名思义,就不继续赘述。
先偷个懒,改天想到了啥,再续狗尾。

🔲 ☆

私有办公服务搭建

鉴于Microsoft Office通常体积臃肿,而且只在Windows下能用。虽然LibOffice开源且免费,适用于各个平台,但碍于接触到的多是Microsoft Office的文档,打开时经常格式很乱,于是考虑搭建一种服务,可以在浏览器中处理文档,类似于Google Docs或者Office Online.

Office服务搭建

目前已经有比较优秀的DzzOffice了,而且开源,仓库地址在此,可以在此处查看演示。
可以自己搭建一个,另外该仓库也提供了Docker部署版本。克隆仓库之后直接使用docker-compose up -d即可部署。

1
2
3
4
5
git clone https://github.com/zyx0814/dzzoffice.git
cd dzzoffice
git checkout docker
chmod 777 -R data dzz config
docker-compose up

不过目前编译,会出现一些问题:

Build php error:

1
ERROR: http://dl-cdn.alpinelinux.org/alpine/v3.4/main: temporary error (try again later)`

原因主要在两个方面: 一是本机Docker的DNS设置:

1
sudo vim /etc/docker/deamon.json

将DNS修改正确;
另一个问题是alpine镜像的DNS问题,测试一下:

1
docker run -it --rm php:7.1.0-fpm-alpine sh -c " ping dl-cdn.alpinelinux.org"

显示bad address.

在php的Dockerfile中加一行,然后重启服务

1
2
3
docker run -it --rm php:7.1.0-fpm-alpine sh -c "echo nameserver 8.8.8.8 > /etc/resolv.conf && ping dl-cdn.alpinelinux.org"
sudo systemctl daemon-reload
sudo systemctl restart docker

Php compose error:

1
2
3
4
5
6
7
composer global require --no-progress "fxp/composer-asset-plugin:~1.2"
[RuntimeException]
No composer.json present in the current directory, this may be the cause of the following exception.
[Composer\Downloader\TransportException]
Content-Length mismatch, received 549815 bytes out of the expected 1180102

同样修改Dockerfile,安装完compose之后,添加一行:

1
composer config -g repo.packagist composer https://packagist.phpcomposer.com

Build pma error:

1
/bin/bash apk not found

可能是镜像更新了,里面用的ubuntu amd64环境,是apt-get安装,修改pma的Dockerfile,指定一个稍老的版本即可。

至此,服务搭建成功。

服务配置

数据库用户名和密码在部署环境之前,可以在docker-compose.yml中配置,然后在浏览器中打开localhost开始进行配置。

登录之后,需要进行配置,添加应用,比如office,如添加onlyoffice,可以先装一个onlyoffice的服务:

1
docker run -i -t -d --name onlyoffice -p 8000:80 onlyoffice/documentserver

然后在应用库中添加onlyoffice,设置api地址,

1
http://YOUR_SERVER_ADDRESS:8000/web-apps/apps/api/documents/api.js

然后就可以编辑文档了。

绘图服务部署

类似如Visio的工具,目前体验比较好的有DrawIO,也是开源的,可以直接部署到自己的服务器上,在浏览器中绘图,快速轻便,易于分享。

1
docker run -it --rm --name="draw" -p 8080:8080 -p 8443:8443 jgraph/draw.io

在浏览器中打开指定端口地址即可开始绘制。

🔲 ⭐

小工具(一)

太久没回来了,其实很多次想写但是无法静下心来,思绪如开始停更的三月里乱飞的柳絮,总感觉经历了很多,但却没有力气吐出一个字。终于处暑,昔时聚在一起的人们也已经走得差不多了,终于也想起了,该随便写点什么了。

就推荐一些小工具吧,这段时间发现的,感觉挺有意思的。以后也不知道会不会继续,先假设是个连续剧吧,写完拖更的那种。

FlashCards

一个类似单词卡的小工具,也可以用来放代码,示例仓库FlashCards
方法很简单,直接用Docker启动,可以放在自己的电脑上,也可以放在自己的服务器上,挂一个端口,然后可以Web端远程访问。另外,单词数据可以直接上传Github仓库,环境不需要。
这样随时随地,就是一个私人的Note?
搭好之后访问大概是这样:

终端美化LSD

主要功能是将Linux下的ls命令输出结果美化一下,不同的文件类型会有不同的图标,不过目前颜色还不支持修改。大概效果如下:

仓库地址在此
不过需要安装NerdFonts

Tree2dotx

就是将树形结构描述转化为DOT描述。 DOT语言是一种文本图形描述语言,可用于画有向无向图、流程图,语法比较简单,网上一搜就有,这里不做介绍。
比如将当前目录下的文件转成关系图,使用tree2dotx工具,命令为:

1
tree | tree2dotx > out.dot

Graphviz

Graphviz(Graph Visualization Software)是一个由AT&T实验室启动的开源工具包,用于绘制DOT语言脚本描述的图形,官网在这,可以从DOT文件生成图像,常见的有png/gif/svg等。
如果将之前的树形目录保存为关系图,只需要继续将上面的命令重定向即可。

1
tree | tree2dotx | dot -Tpng -o list.png

保存为list.png文件,大致就是这个样子:

Gprof

一个代码性能分析工具,结合Gdb可以很方便的分析所写的代码。
主要方式是在使用gdb编译时加上-pg参数,然后正常运行程序,最后会出现一个gmon.out的文件,里面就是各个函数的信息。
结合Graphviz,可以得到函数关系调用图。

1
gprof -b ./test gmon.out | gprof2dot | dot -Tpng -o test.png

其中gprof2dot工具可以通过pip安装。
最后结果如下:


里面有各个函数调用次数、运行时间等情况,保存为svg也可以在浏览器中看。

EMMMM, To be continued…

🔲 ☆

Git基本用法

目前版本控制系统比较流行的就是SVN和Git了,相比较而言,Git有分布式的优势,对网络依赖性更低,但SVN简单,这一条就有很强的生存能力了。用Git已经好几年了,不过很长一段时间只是在用clone pull add commit push这些类Ctrl+C/V的命令(Office中),连操作Head指针实现Ctrl+Z/Y都没怎么用,想起去年收到了Leancloud的10X程序员笔记本,里面附页还写着几行Git命令,突然觉得有些陌生了。

也只是突然想到,回忆一下,当是补上多年前未肯作的笔记了。

基本文件操作

检查文件状态

Git检查文件状态可以使用git status,可以看到已经提交的修改和未提交的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: source/_drafts/git.md
modified: source/talks/index.md
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: source/_drafts/git.md

使用git diff可以查看尚未暂存的文件的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@@ -1,4 +1,26 @@
...
+Git检查文件状态可以使用`git status`,可以看到已经提交的修改和未提交的修改:
+On branch master
+Your branch is up to date with 'origin/master'.
+
+Changes to be committed:
+ (use "git reset HEAD <file>..." to unstage)
+
+ modified: source/_drafts/git.md
+ modified: source/talks/index.md
+
+Changes not staged for commit:
+ (use "git add <file>..." to update what will be committed)
+ (use "git checkout -- <file>..." to discard changes in working directory)
+
+ modified: source/_drafts/git.md
+
+使用`git diff`可以勘察尚未暂存的文件的修改:
\ No newline at end of file

另外加上--cached或者--staged(新版支持)参数,可以直接查看已暂存的和上次提交时的差异。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
git diff --staged
diff --git a/source/_drafts/git.md b/source/_drafts/git.md
index b220f55b..ce996f92 100644
--- a/source/_drafts/git.md
+++ b/source/_drafts/git.md
@@ -1 +1,4 @@
-title: Git用法
+title: Git基本用法
+
+目前版本控制系统比较流行的就是SVN和Git了,相比较而言,Git有分布式的优势,对网络依赖性更低,但SVN简单,这一条就有很强的生存能力了。用Git已经好几年了,不过很长一段时间只是在用`clone pull add commit push`这些类`Ctrl+C/V`的命令(Office中),连操作Head
指针实现`Ctrl+Z/Y`都没怎么用,想起去年收到了Leancloud的`10X`程序员笔记本,里面附页还写着几行Git命令
+#
\ No newline at end of file
diff --git a/source/talks/index.md b/source/talks/index.md
index d13db982..63515f47 100644

基本文件操作

除去系统自带的mv或者rm命令,Git也有自己的git mvgit rm命令,在Git仓库中,后者不仅仅是对文件做了前者的操作,也在工作目录中做了前者的操作。
git rm在删除文件后,也从跟踪文件清单中删除了该文件(使用--cached只是从暂存区中删除,使用-f同时也删除文件),以后不会再跟踪该文件,而rm命令的操作记录依然会被记录在跟踪文件清单中。
一个简单的例子,先创建一个文件:

1
touch test

此时未放入暂存区,直接删除就可以,Git也不会记录,但是如果Git已经跟踪了该文件,则直接删除状态为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
git add test
rm test
git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: source/_drafts/git.md
new file: test
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: source/_drafts/git.md
deleted: test

如果使用git rm test,可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
git rm test
git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: source/_drafts/git.md
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: source/_drafts/git.md

可以看到,test文件的记录已经被删除了。
同样,git mv也是一样的类型,git mv file1 file2相当于:

1
2
3
mv file1 file2
git rm file1
git add file2

查看提交历史

查看每次的提交历史可以直接使用git log,可以看到每次的提交记录。另外,加上-p参数可以展开每次提交的内容差异,加上-{d}可以指定显示最近次数的差异,如-2显示最近两次提交的差异。加上--since或者--until可以限制时间查询,如可以用git long --since=2.weeks显示最近两周的修改。加上--word-diff可以进行单词层面的对比,加上--graphASCII图形表示的分支合并历史。如果只想看每次提交的简略信息,可以加上-stat参数。另外,可以使用--pretty指定展示提交历史的格式,如用oneline将每个提交放在一行显示(--pretty常用参数有oneline,short,full,fuller和format(后跟指定格式))。

撤销操作

仅修改提交信息

如果提交信息写错了,或者有些文件漏掉了未添加到暂存区,可以使用amend指令重新提交:

1
2
3
git commit -m "add test"
git add test
git commit --amend

这样就完成了提交信息的修改。

取消暂存区文件

如果想取消暂存区的某个文件的暂存,有两种方法。一是上面的git rm --cached直接将文件从暂存区中删除,实际文件不受影响。另外一个是HEAD指针的操作。
HEAD可以理解为指向当前分支的指针,指向该分支最近一次的调用,操作HEAD指针即可实现版本回退等操作。
这里直接使用reset命令,将某个文件重置到最近一次提交时的状态:

1
git reset HEAD test

因为上次test未暂存,所以相当于从暂存区中取消该文件。

撤销对文件的修改

使用git checkout -- file可以撤销上次提交以来,对某个文件的所有修改,本质上是拷贝了上次提交时的该文件来覆盖它,因此对该文件做的任何修改都会消失。该命令需要谨慎使用,最好的方式是通过分支的保存进度来恢复。
Git中所有已经提交的东西基本上都是可以恢复的,但未暂存的就不属于Git恢复的范畴了。

远程仓库

Git主要是在本地修改好了再推送到远程仓库,实际上对远程仓库的操作比较少,就一些基本的推拉行为。

  1. 查看远程仓库。
    直接使用git remote即可查看当前的远程仓库,加上-v选项可以以详细模式查看。
  2. 添加远程仓库。
    直接使用git remote add <shortname> <url>,将仓库名和地址添加即可。
  3. 从远程仓库抓取数据。
    有两种需求,一种是只从远程仓库拉取数据,但并不合并到当前分支,可以使用git fetch <remotename>命令。
    另外,使用git clone获取的远程仓库会自动归于origin名下。
    另一种,需求是自动抓取并合并到当前分支,可以使用git pull命令。
  4. 推送数据到远程仓库。
    基本操作,git push <remotename> <branch>
  5. 查看远程仓库信息。

    1
    git remote show <remote-name>
  6. 远程仓库的删除和重命名。

  • 删除远程仓库: git remote rm <remotename>
  • 重命名远程仓库: git remote rename <orignname> <newname>

标签

Git可以给历史中的某个提交打上标签,以示其重要性,如v1.0等。

列出标签

列出已有标签,可以直接使用git tag命令,加上-l参数可以过滤选项。如

1
git tag -l 'v1.0.1*'

创建标签

标签分为轻量标签和附注标签,轻量标签如其名轻量,只是一个特定提交的引用,本质上是将提交校验和存储到一个文件中,没有保存其他任何信息,因此创建也比较简单。附注标签则是Git数据库中的一个完整对象,是可以被校验的。附注标签通常包含打标签者的姓名、邮件地址、日期、标签信息等,并可以使用GPG(GNU Privacy Guard)签名及验证。

  • 创建附注标签: 最简单的方式是使用tag-a选项:
1
git tag -a v1.1 -m "new test version"

查看标签:

1
2
3
git tag
v1.0
v1.1

其中,-m是存储在标签中的信息,是必填内容。使用git show也可以看到标签信息与对应的提交信息。

  • 创建轻量标签: 轻量标签的创建不需要任何选项,直接提供标签名字即可。
1
git tag v1.11

查看标签:

1
2
3
4
git tag
v1.0
v1.1
v1.11

此时用git show只能看到标签的提交信息,没有额外信息。

后期上标签

也可以对过去的提交上标签,使用git log --pretty=oneline时可以看到每次提交的校验和,如某次校验和是e0c29751bf13be3df3b5030cc589685752bd9fb6,则可以通过该校验和给该次提交打上标签:

1
git tag -a v0.8 e0c2975

实际只需要部分校验和即可。

分享标签

通常情况,git push并不会将标签推送到服务器上,需要通过显示命令才能分享标签到远程仓库。

1
git push origin <tagname>

如果要一次性推送所有本地新增标签到服务器上,则可以使用--tags参数:

1
git push origin --tags

删除标签

删除本地仓库的标签,可以使用:

1
git tag -d <tagname>

如果要同时删除远程标签,则需要使用git push <remotename> :refs/tags/<tagname>来更新远程仓库标签。

标签检出

可以使用git checkout命令查看某个标签指向的文件版本。但会使仓库处于头指针分离(“detacthed HEAD”)的状态:在”头指针分离“状态下,如果做了某些更改然后提交他们,标签不会发生变化,但新的提交不属于任何分支,也无法访问,除非确切的提交哈希。所以如果要进行更改,通常需要创建一个新分支:

1
2
git checkout -b newversion v1.12
Switched to a new branch 'newversion'

如果继续对newversion分支做改动,该分支的提交指针会继续向前移动,就不是原来的v1.12标签了。

分支

Git好用很大原因是其极具优势的分支模型,使得分支处理方式更为轻量。
在使用git commit新建一个提交对象前,Git会先计算每一个子目录的校验和,然后在Git仓库将这些目录保存为一个Tree对象,然后就可以创造一个提交对象,并包含了指向这个Tree对象的指针。Git使用blob类型的对象存储此次保存的快照。
关于Git的树结构,可以用Git官方仓库中的一张图说明:

这是首次提交后的结构图,此时Git仓库中有五个对象(五个校验和),最右侧的是三个存储文件快照的blob对象,中间是记录目录结构和blob对象索引的树对象,最左侧是包含指向书对象的指针和所有提交信息的提交对象。
此时因为是第一次提交,相当于祖先提交,提交对象中没有父对象,但之后的所有提交对象中,都会多一个父对象指针,指向上次提交。

Git分支在本质上是一个指向最新提交对象的指针,每次提交操作之后,指针都会更新到最新提交。

分支就是某个提交对象往回看的历史。

使用git branch可以列出所有的分支,加上--merged--no-merged可以显示已合并或未合并的分支。

分支创建

Git使用master作为默认的分支名,如果要创建分支,可以使用branch选项。

1
git branch <branchname>

但此时只是新建了一个分支,并未将当前工作分支切换过去。Git确定当前工作的分支是使用HEAD指针,HEAD指针指向哪个分支,当前就在哪个分支工作。

也可以使用git log -decorate命令查看各个分支当前所指的对象。

分支切换

切换分支即修改HEAD指针指向,可以使用chenkout命令实现。

1
git checkout <branchname>

在每次提交后,HEAD指针会随着当前分支一起向前移动以保证以后分支能正确切换回来。
或者直接使用命令:

1
git checkout -b <branchname>

可以在新建分支的同时切换到该分支,-b可以理解为branch,相当于:

1
2
git branch <branchname>
git checkout <branchname>

分支合并

在某个分支上进行操作,使得该分支指针向前移动后,如果要将该分支合并到其他分支,则可以切换到其他分支进行merge操作:

1
git merge <branchname>

当两个分支没有需要解决的分歧时,可以直接合并。

删除分支

当分支不再使用时,可以删除:

1
git branch -d <branchname>

对于未合并的分支,直接删除会失败,可以使用-D强制删除。

冲突合并

如果合并的两个分支,并不是直接祖先关系,两个分支在其共同祖先分支上都做了修改,如果修改没有冲突,如修改的都是不同的文件,则Git会自动新建一个提交,将共同祖先分支以及两个要合并的分支共同合并建立一个新的提交。此时Git会自行决定选取哪个提交作为最优的共同祖先。
但是如果两个不同分支都对同一个文件做了修改,在合并时就会引起冲突,因为Git不知道到底该对这个文件做如何操作。此时Git会先暂停下来,等待用户解决冲突。这种情况在平时也经常会遇到,如在本地对某个远程仓库做了修改,但是远程仓库在此之前已经在另一台电脑上做了push操作,这时使用pull操作就会自动抓取并合并到当前分支,如果存在冲突,pull时就会提示哪个文件修改冲突,并等待用户解决。此时,可以使用git status查看状态。
解决冲突后可以重新使用git add将其标记为冲突已解决。

远程分支

远程引用是指向远程仓库的指针,包括分支、标签等,可以通过git ls-remote <remotename>查看远程引用的完整列表,或者通过git remote show <remote>查看远程分支的更多信息。
远程跟踪则是指向远程分支状态的引用,只有当与远程仓库通信时,它们会自动移动。用户无法手动修改其状态。
可以使用git fetch命令将远程仓库中的内容拉取到本地,同事远程跟踪会更新到新的远程分支状态。当本地与远程的工作出现分叉之后,合并到本地分支时,依然会考虑是否有冲突的问题,解决方式和其他冲突分支合并一样。

推送本地分支

使用git push将本地分支推送到远端:

1
git push origin test

等价于

1
git push origin test:test

Git会自动将test名字展开为refs/heads/test:refs/heads/test

跟踪分支

使用checkout可以实现对分支的跟踪:

1
git checkout --track origin/test

通常可以新建一个本地分支来跟踪拉取的远程分支:

1
git checkout -b sf origin/test

也可以使用-u--set-upstream-to选项来直接设置已有的本地分支来跟踪拉取的远程分支:

1
git branch -u origin/test

另外,可以使用git branch -vv命令查看设置的所有跟踪分支。

合并分支

可以使用git fetch拉取分支后再使用git merge合并到本地分支,也可以直接使用git pull拉取并合并到本地分支。但是有时候git pull会显得有些佛性,难以理解,最简单的方式是fetchmerge的组合。

删除分支

删除远程分支可以使用:

1
git push origin --delete test

或者直接将空分支推送到远端覆盖远端分支即可:

1
git push origin :<remotebranch>

变基

这个是个有趣的用法,自从有了变基,Github就变成了Gayhub (逃 stuck_out_tongue_winking_eye )。
啊呸!当然不是这个原因。
变基是一种整合分支的方法,通常整合分支有两种方法:合并和变基。
合并(merge)之前已经经常用到了,主要就是将一个分支合并到另一个上。而变基(rebase)则是将一个分支里提交的修改在另一个分支上重放一边,也就是走别人的路,让别人说去吧。
一个基本的例子如下:

1
2
git checkout branch1
git rebase branch2

此时,Git会先找到这两个分支的分叉点(即最近共同祖先),然后从分叉点开始,将branch1所经历的操作,给branch2也体验一下。然后回到branch2,进行一次快进合并:

1
2
git checkout branch2
git merge branch1

其实就这个例子来看,变基和合并没有任何区别,但这样可以保证在向远程分支推送时保持提交历史的简洁。
另外,变基可以放到其他分支进行,并不一定非得依据分化之前的分支。可以从一个特性分支里再分出一个特性分支,然后跳过前面的特性分支,将后者与主分支进行变基,可以使用--onto选项。

1
git rebash --onto master branch1 branch2

即取出branch2分支,找到branch1branch2的分离点,然后在master分支上重放其共同祖先之后的修改。
然后就可以将变基后的分支快进合并到master分支上:

1
2
git checkout master
git merge branch2

剩下的也可以将branch1合并到master中:

1
git rebase master branch1

然后快进合并master分支:

1
2
git checkout master
git merge branch1

之后就可以删除无用的分支了。

变基风险

因为人人都可以编辑,所以一旦分支中的对象提交发布到公共仓库,就千万不要对该分支进行变基,不然其他人不得不重新将手里的工作和你的提交进行整合,接下来你也要重新拉取他们的提交进行整合,引入太多不必要的麻烦。
总之用官方一句加粗的话说:

不要对在你的仓库外有副本的分支执行变基。

其他操作

别名

和Linux的alias命令一样的意思,也是方便在git中快速操作。

1
2
3
4
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status

设置别名后,通过 git co即可实现git checkout命令。

储藏

当不想提交现在的工作状态,又想切换到别的分支进行工作,可以先将当前状态出藏起来。储藏(Stash)可以获取工作目录的中间状态——也就是修改过的被追踪的文件和暂存的变更——并将它保存到一个未完结变更的堆栈中,随时可以重新应用。
使用git stash list可以查看当前储藏的列表。
如果之后要恢复储藏的状态,可以使用:

1
git stash apply

Git则会默认恢复最近一次的储藏,如果想应用更早的储藏,则可以通过名字指定,如:

1
git stash apply stash@{2}

此时对文件的变更被重新应用,但是被暂存的文件没有重新被暂存。可以通过运行git stash apply命令时带上一个--index的选项来告诉命令重新应用被暂存的变更。
apply选项只尝试应用储藏的工作,但储藏的栈上仍然有该储藏。可以通过运行git stash drop,加上希望移除的储藏的名字来移除该储藏,或者直接通过git stash pop来重新应用储藏并在此之后快速删除栈上的储藏。

取消储藏

如果要取消之前所应用的储藏的修改,可以通过取消该储藏的补丁达到该效果:

1
git stash show -p stash@{0} | git apply -R

如果没有指定储藏名称,则会自动选择最近的储藏:

1
git stash show -p | git apply -R

从储藏中创建分支

在储藏一个工作状态后,继续在该分支上工作,最后还原储藏的时候可能会引起合并冲突,此时可以新建一个储藏分支简化工作。

1
git stash branch <branchname>

此时Git会创建一个新的分支,检出储藏工作时的所处的提交,重新应用,如果成功,则丢弃储藏。

🔲 ⭐

LXD搭设服务器

主要是想搭设几台服务器,希望用户环境能隔离,相互安装和配置环境不影响,也希望不至于发生有了sudo权限就把别人的都删了的情况。同时也希望所有用户都能使用服务器上的硬件设备如GPU,且都能上网。

如果采用虚拟机技术,则硬件只能独占,不能共享,且开销大,另外一旦确定了所需分配的资源就成了固定开销,无论虚拟机中资源利用率如何。而另一方面,容器技术的特点则是资源共享,基本不占用硬件资源,所以考虑使用容器技术来实现用户环境隔离。
目前最流行的容器技术还是Docker,但Docker更适合于单个应用环境的部署,对于用户来说,希望在相互隔离时候也能用到服务器资源,更希望是一个虚拟机,而不是一个应用环境。目前Linux上主要有LXC和LXD,Docker以前就是用的LXC的Runtime,而LXD也只是一个提供了REST API的LXC容器管理器而已,其仓库地址在此。因此打算使用LXD来搭建这个服务器。



初始化

首先是下载LXD容器,如果是Ubuntu16.04里的apt软件仓库,最高应该是2.x的版本,如果要支持LXD容器内GPU的数据处理,至少版本为3.0.好在从16.04时候引进了另一个软件包管理工具,之前一篇文章有所介绍,即使用snap软件包管理工具。
查看版本:

1
2
3
4
5
6
7
$ ▶ snap find lxd
Name Version Publisher Notes Summary
lxd-demo-server 0+git.f3532e3 stgraber - Online software demo sessions using LXD
lxd 3.6 canonical✓ - System container manager and API
nova ocata james-page - OpenStack Compute Service (nova)
satellite 0.1.2 alanzanattadev - Advanced scalable Open source intelligence platform
nova-hypervisor ocata james-page - OpenStack Compute Service - KVM Hypervisor (nova)

可以看到已经到3.6了,直接下载就行snap install lxd
安装好后应该就可以直接使用了,第一部是初始化LXD的环境,使用lxd init。如果出现permission denied之类的问题,可以加sudo,嫌麻烦可以将当前用户加入LXD组内:

1
sudo usermod add -aG lxd ${USER}

然后注销重新登录就行了。
在初始化之前,需要安装几个工具,一个是ZFS,是LXD默认的后端存储工具,另一个是Bridge管理工具,LXD自身也带网桥创建功能,默认创建网桥会自动创建局域网私有地址并分配DHCP地址至虚拟网卡。

1
sudo apt install zfsutils-linux bridge-utils

初始化过程如下:

1
lxd init

所有提示注意一下是否创建网桥时候选择no就行,其余基本可以使用默认配置。如果不用管外网远程登录,可以直接全选默认。
然后拉取一个镜像,如:

1
lxc launch ubuntu:16.04 test

拉取成功启动了就可以使用lxc list看到容器了。使用lxc exec test -- ${command}命令在容器内执行命令。如:

1
lxc exec test bash

这时可以进入容器内的bash。
然后通过配置好第一个容器,将其作为模板,制作出多个虚拟主机。

显卡配置

在此之前,需要宿主机上安装显卡驱动和CUDA,具体过程不做赘述。
先关闭容器lxc stop test,然后将显卡设备添加到容器中:

1
lxc config device add test gpu gpu

该命令是添加所有显卡,也可以手动指定显卡id。
然后启动容器,安装显卡驱动:

1
2
lxc exec test bash
apt update

可以直接参考宿主机的显卡驱动,查看一下宿主机显卡驱动版本,可以使用nvidia-smi或者sudo dpkg -l |grep nvidia查看,然后回到容器,使用apt install nvidia-XXX-dev安装。
如果安装成功,即可以使用nvidia命令查看显卡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
nvidia-smi
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 390.30 Driver Version: 390.30 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
|===============================+======================+======================|
| 0 Quadro P4000 Off | 00000000:02:00.0 Off | N/A |
| 46% 37C P0 28W / 105W | 0MiB / 8118MiB | 0% Default |
+-------------------------------+----------------------+----------------------+
| 1 Quadro P4000 Off | 00000000:03:00.0 Off | N/A |
| 46% 40C P0 28W / 105W | 0MiB / 8119MiB | 0% Default |
+-------------------------------+----------------------+----------------------+
| 2 Quadro P4000 Off | 00000000:82:00.0 Off | N/A |
| 46% 40C P0 28W / 105W | 0MiB / 8119MiB | 0% Default |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: GPU Memory |
| GPU PID Type Process name Usage |
|=============================================================================|
| No running processes found |
+-----------------------------------------------------------------------------+

CUDA版本和TensorFlow版本由用户自己选择,默认不安装。

网络配置

这个是最麻烦的,如果需要访问外网的话。目前个人方法如下:

lxc创建网桥

先使用lxc创建一个网桥,网桥地址应该与本地电脑在一个网段,这样桥接后本地其他电脑才可以远程访问该容器。假如本地各电脑IP为192.168.1.xxx,则:

1
lxc network create lxd0 ipv4.address=192.168.1.10/24

其他可使用默认配置,具体各项参数见官方说明
然后使用bridge管理工具将网桥连接至本地网卡,假如本地网卡为enp1s0,则:

1
sudo brctl addif lxd0 enp1s0

添加之后可以使用brctl show命令查看。

宿主机路由

这时可能出现宿主机无法上网的问题,原因是访问网络时,数据包都默认转发到新建网桥地址,而不是默认网关地址,所以需要添加一条路由表:

1
sudo route add default gw 192.168.1.1

可以解决本地宿主机上网问题。

重新初始化

关闭容器后再次使用lxd init初始化容器环境,主要是为容器选择默认网桥,这时只用修改一项配置Would you like to configure LXD to use an existing bridge or host interface? (yes/no) [default=no],改为yes,然后输入新建网桥名lxd0即可。

分配静态地址

然后重新启动容器并进入bash,修改网络配置文件:

1
vim /etc/network/interfaces

添加

1
2
3
4
5
auto eth0
iface eth0 inet static
address 192.168.1.11
gateway 192.168.1.1
netmask 255.255.255.0

重启网络服务

1
/etc/init.d/networking restart

如果IP还不变,那就重启宿主机。

修改DNS

通常到上一步已经可以上网了,默认域名解析服务地址是网桥地址,你也可以改为自定义的DNS地址,如114.114.114.114。最通常的方法是修改/etc/resolv.conf文件中的nameserver。但重启后会失效。以下是永久修改DNS的方法,通常在搭建过程中不需要用到。

修改Resolvconf配置

修改/etc/resolvconf/resolv.conf.d目录下的base,在里面修改DNS服务器地址即可。

修改DHCP配置

另一个方法是修改DHCP配置文件,

1
vim /etc/dhcp/dhclient.conf

可以看到,

1
2
#supersede domain-name "fugue.com home.vix.com";
#prepend domain-name-servers 127.0.0.1;

去掉前面的#,将域名服务器改成自己的就可以了。

ssh配置

如果希望用户能远程访问容器,除了网络配置之外,还需要修改一下ssh配置。默认禁止root用户登录,容器创建默认用户也是root用户,里面有个ubuntu用户,未初始化。既然虚拟主机交给用户,即把root也给用户了,所以先设置允许root用户登录,如不需要可以让用户自行更改。

1
vim /etc/ssh/sshd_config

将其中的PermitRootLogin prohibit-password改为PermitRootLogin yes,以及ChallengeResponseAuthentication no改为ChallengeResponseAuthentication yes
然后为root用户设置密码:

1
passwd root

另外可以编辑ssh登录用户的欢迎信息,通过编辑/etc/update-motd.d/目录下的00-header01-hepler-text中的内容即可完成。
最后,重启ssh服务,

1
/etc/init.d/ssh restart

挂载共享目录

最后需要在主机上创建一个文件夹,用于各个容器与主机共享,文件传输之类,虽然主机lxc已经有pull和push方法从主机和容器之间拷贝文件,但共享目录会显得更为方便,即便在容器之间也可以相互访问。

1
lxc config device add mycontainer sharedtmp disk path=/tmp/share_on_lxc source=/tmp/share_on_host

其中,pathsource的地址可以自己定义。
到这里,基本结束。

🔲 ☆

GSreamer笔记四: GUI Toolkit Integration

主要是关于如何将GStreamer集成到图形用户界面(GUI)工具箱中。基本上当GUI工具箱处理用户界面时,GStreamer主要负责媒体播放。其中两个库必须交互的部分是最有趣的两个部分,即:指导GStreamer将视频输出到GTK+的窗口中并将用户操作转发给GStreamer。

需要解决的问题有:

  • 告诉GStreamer如何将视频输出到特定窗口,而不是自己创建窗口;
  • 如何使用GStreamer的信息持续刷新GUI;
  • 如何从GStreamer的多个线程更新GUI(这是大多数GUI工具包中被禁止的操作);
  • 一个只订阅感兴趣的消息而不是通知所有人的机制。

关于GTK+

这里将使用GTK+工具包构建媒体播放器,这些概念亦适用于其他工具包如QT。
关键是告诉GStreamer将视频输出到所选择的窗口。具体机制取决于操作系统(或者窗口系统),但GStreamer为平台独立性提供了一个抽象层。这种独立性来自GstVideoOverlay接口,它允许应用程序告诉视频接收器(sink)应该接收渲染的窗口的处理程序。
Gstreamer所使用的是GObject 接口。GObject的接口是元素可以实现的一组函数,包括GstVideoOverlay等。具体介绍如下:

A GObject interface (which GStreamer uses) is a set of functions that an element can implement. If it does, then it is said to support that particular interface. For example, video sinks usually create their own windows to display video, but, if they are also capable of rendering to an external window, they can choose to implement the GstVideoOverlay interface and provide functions to specify this external window. From the application developer point of view, if a certain interface is supported, you can use it and forget about which kind of element is implementing it. Moreover, if you are using playbin, it will automatically expose some of the interfaces supported by its internal elements: You can use your interface functions directly on playbin without knowing who is implementing them!

另一个问题是,GUI工具包通常只允许主(或应用)线程来操作图形“小部件”,而GStreamer通常会派生多个线程来处理不同的任务。从回调函数中调用GTK +函数通常会失败,因为回调函数在调用线程中执行,并不需要在主线程中。这个问题可以通过回调函数在GStreamer总线上发布消息来解决: 主线程接收消息并做出相应反应。
这里已经注册了一个handle_message函数,每次在总线上出现一条消息时都会调用这个函数,这迫使我们解析每条消息,看看我们是否对其感兴趣。本例中使用了一种不同的方法来为每种消息注册一个回调,所以解析更少,代码更少。

GTK+播放器示例

一个简单的基于playbin的带GUI的媒体播放器如下:

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
#include <string.h>
#include <gtk/gtk.h>
#include <gst/gst.h>
#include <gst/video/videooverlay.h>
#include <gdk/gdk.h>
#if defined (GDK_WINDOWING_X11)
#include <gdk/gdkx.h>
#elif defined (GDK_WINDOWING_WIN32)
#include <gdk/gdkwin32.h>
#elif defined (GDK_WINDOWING_QUARTZ)
#include <gdk/gdkquartz.h>
#endif
/* Structure to contain all our information, so we can pass it around */
typedef struct _CustomData {
GstElement *playbin; /* Our one and only pipeline */
GtkWidget *slider; /* Slider widget to keep track of current position */
GtkWidget *streams_list; /* Text widget to display info about the streams */
gulong slider_update_signal_id; /* Signal ID for the slider update signal */
GstState state; /* Current state of the pipeline */
gint64 duration; /* Duration of the clip, in nanoseconds */
} CustomData;
/* This function is called when the GUI toolkit creates the physical window that will hold the video.
* At this point we can retrieve its handler (which has a different meaning depending on the windowing system)
* and pass it to GStreamer through the VideoOverlay interface. */
static void realize_cb (GtkWidget *widget, CustomData *data) {
GdkWindow *window = gtk_widget_get_window (widget);
guintptr window_handle;
if (!gdk_window_ensure_native (window))
g_error ("Couldn't create native window needed for GstVideoOverlay!");
/* Retrieve window handler from GDK */
#if defined (GDK_WINDOWING_WIN32)
window_handle = (guintptr)GDK_WINDOW_HWND (window);
#elif defined (GDK_WINDOWING_QUARTZ)
window_handle = gdk_quartz_window_get_nsview (window);
#elif defined (GDK_WINDOWING_X11)
window_handle = GDK_WINDOW_XID (window);
#endif
/* Pass it to playbin, which implements VideoOverlay and will forward it to the video sink */
gst_video_overlay_set_window_handle (GST_VIDEO_OVERLAY (data->playbin), window_handle);
}
/* This function is called when the PLAY button is clicked */
static void play_cb (GtkButton *button, CustomData *data) {
gst_element_set_state (data->playbin, GST_STATE_PLAYING);
}
/* This function is called when the PAUSE button is clicked */
static void pause_cb (GtkButton *button, CustomData *data) {
gst_element_set_state (data->playbin, GST_STATE_PAUSED);
}
/* This function is called when the STOP button is clicked */
static void stop_cb (GtkButton *button, CustomData *data) {
gst_element_set_state (data->playbin, GST_STATE_READY);
}
/* This function is called when the main window is closed */
static void delete_event_cb (GtkWidget *widget, GdkEvent *event, CustomData *data) {
stop_cb (NULL, data);
gtk_main_quit ();
}
/* This function is called everytime the video window needs to be redrawn (due to damage/exposure,
* rescaling, etc). GStreamer takes care of this in the PAUSED and PLAYING states, otherwise,
* we simply draw a black rectangle to avoid garbage showing up. */
static gboolean draw_cb (GtkWidget *widget, cairo_t *cr, CustomData *data) {
if (data->state < GST_STATE_PAUSED) {
GtkAllocation allocation;
/* Cairo is a 2D graphics library which we use here to clean the video window.
* It is used by GStreamer for other reasons, so it will always be available to us. */
gtk_widget_get_allocation (widget, &allocation);
cairo_set_source_rgb (cr, 0, 0, 0);
cairo_rectangle (cr, 0, 0, allocation.width, allocation.height);
cairo_fill (cr);
}
return FALSE;
}
/* This function is called when the slider changes its position. We perform a seek to the
* new position here. */
static void slider_cb (GtkRange *range, CustomData *data) {
gdouble value = gtk_range_get_value (GTK_RANGE (data->slider));
gst_element_seek_simple (data->playbin, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT,
(gint64)(value * GST_SECOND));
}
/* This creates all the GTK+ widgets that compose our application, and registers the callbacks */
static void create_ui (CustomData *data) {
GtkWidget *main_window; /* The uppermost window, containing all other windows */
GtkWidget *video_window; /* The drawing area where the video will be shown */
GtkWidget *main_box; /* VBox to hold main_hbox and the controls */
GtkWidget *main_hbox; /* HBox to hold the video_window and the stream info text widget */
GtkWidget *controls; /* HBox to hold the buttons and the slider */
GtkWidget *play_button, *pause_button, *stop_button; /* Buttons */
main_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
g_signal_connect (G_OBJECT (main_window), "delete-event", G_CALLBACK (delete_event_cb), data);
video_window = gtk_drawing_area_new ();
gtk_widget_set_double_buffered (video_window, FALSE);
g_signal_connect (video_window, "realize", G_CALLBACK (realize_cb), data);
g_signal_connect (video_window, "draw", G_CALLBACK (draw_cb), data);
play_button = gtk_button_new_from_icon_name ("media-playback-start", GTK_ICON_SIZE_SMALL_TOOLBAR);
g_signal_connect (G_OBJECT (play_button), "clicked", G_CALLBACK (play_cb), data);
pause_button = gtk_button_new_from_icon_name ("media-playback-pause", GTK_ICON_SIZE_SMALL_TOOLBAR);
g_signal_connect (G_OBJECT (pause_button), "clicked", G_CALLBACK (pause_cb), data);
stop_button = gtk_button_new_from_icon_name ("media-playback-stop", GTK_ICON_SIZE_SMALL_TOOLBAR);
g_signal_connect (G_OBJECT (stop_button), "clicked", G_CALLBACK (stop_cb), data);
data->slider = gtk_scale_new_with_range (GTK_ORIENTATION_HORIZONTAL, 0, 100, 1);
gtk_scale_set_draw_value (GTK_SCALE (data->slider), 0);
data->slider_update_signal_id = g_signal_connect (G_OBJECT (data->slider), "value-changed", G_CALLBACK (slider_cb), data);
data->streams_list = gtk_text_view_new ();
gtk_text_view_set_editable (GTK_TEXT_VIEW (data->streams_list), FALSE);
controls = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
gtk_box_pack_start (GTK_BOX (controls), play_button, FALSE, FALSE, 2);
gtk_box_pack_start (GTK_BOX (controls), pause_button, FALSE, FALSE, 2);
gtk_box_pack_start (GTK_BOX (controls), stop_button, FALSE, FALSE, 2);
gtk_box_pack_start (GTK_BOX (controls), data->slider, TRUE, TRUE, 2);
main_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
gtk_box_pack_start (GTK_BOX (main_hbox), video_window, TRUE, TRUE, 0);
gtk_box_pack_start (GTK_BOX (main_hbox), data->streams_list, FALSE, FALSE, 2);
main_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
gtk_box_pack_start (GTK_BOX (main_box), main_hbox, TRUE, TRUE, 0);
gtk_box_pack_start (GTK_BOX (main_box), controls, FALSE, FALSE, 0);
gtk_container_add (GTK_CONTAINER (main_window), main_box);
gtk_window_set_default_size (GTK_WINDOW (main_window), 640, 480);
gtk_widget_show_all (main_window);
}
/* This function is called periodically to refresh the GUI */
static gboolean refresh_ui (CustomData *data) {
gint64 current = -1;
/* We do not want to update anything unless we are in the PAUSED or PLAYING states */
if (data->state < GST_STATE_PAUSED)
return TRUE;
/* If we didn't know it yet, query the stream duration */
if (!GST_CLOCK_TIME_IS_VALID (data->duration)) {
if (!gst_element_query_duration (data->playbin, GST_FORMAT_TIME, &data->duration)) {
g_printerr ("Could not query current duration.\n");
} else {
/* Set the range of the slider to the clip duration, in SECONDS */
gtk_range_set_range (GTK_RANGE (data->slider), 0, (gdouble)data->duration / GST_SECOND);
}
}
if (gst_element_query_position (data->playbin, GST_FORMAT_TIME, &current)) {
/* Block the "value-changed" signal, so the slider_cb function is not called
* (which would trigger a seek the user has not requested) */
g_signal_handler_block (data->slider, data->slider_update_signal_id);
/* Set the position of the slider to the current pipeline positoin, in SECONDS */
gtk_range_set_value (GTK_RANGE (data->slider), (gdouble)current / GST_SECOND);
/* Re-enable the signal */
g_signal_handler_unblock (data->slider, data->slider_update_signal_id);
}
return TRUE;
}
/* This function is called when new metadata is discovered in the stream */
static void tags_cb (GstElement *playbin, gint stream, CustomData *data) {
/* We are possibly in a GStreamer working thread, so we notify the main
* thread of this event through a message in the bus */
gst_element_post_message (playbin,
gst_message_new_application (GST_OBJECT (playbin),
gst_structure_new_empty ("tags-changed")));
}
/* This function is called when an error message is posted on the bus */
static void error_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
GError *err;
gchar *debug_info;
/* Print error details on the screen */
gst_message_parse_error (msg, &err, &debug_info);
g_printerr ("Error received from element %s: %s\n", GST_OBJECT_NAME (msg->src), err->message);
g_printerr ("Debugging information: %s\n", debug_info ? debug_info : "none");
g_clear_error (&err);
g_free (debug_info);
/* Set the pipeline to READY (which stops playback) */
gst_element_set_state (data->playbin, GST_STATE_READY);
}
/* This function is called when an End-Of-Stream message is posted on the bus.
* We just set the pipeline to READY (which stops playback) */
static void eos_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
g_print ("End-Of-Stream reached.\n");
gst_element_set_state (data->playbin, GST_STATE_READY);
}
/* This function is called when the pipeline changes states. We use it to
* keep track of the current state. */
static void state_changed_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
GstState old_state, new_state, pending_state;
gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data->playbin)) {
data->state = new_state;
g_print ("State set to %s\n", gst_element_state_get_name (new_state));
if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED) {
/* For extra responsiveness, we refresh the GUI as soon as we reach the PAUSED state */
refresh_ui (data);
}
}
}
/* Extract metadata from all the streams and write it to the text widget in the GUI */
static void analyze_streams (CustomData *data) {
gint i;
GstTagList *tags;
gchar *str, *total_str;
guint rate;
gint n_video, n_audio, n_text;
GtkTextBuffer *text;
/* Clean current contents of the widget */
text = gtk_text_view_get_buffer (GTK_TEXT_VIEW (data->streams_list));
gtk_text_buffer_set_text (text, "", -1);
/* Read some properties */
g_object_get (data->playbin, "n-video", &n_video, NULL);
g_object_get (data->playbin, "n-audio", &n_audio, NULL);
g_object_get (data->playbin, "n-text", &n_text, NULL);
for (i = 0; i < n_video; i++) {
tags = NULL;
/* Retrieve the stream's video tags */
g_signal_emit_by_name (data->playbin, "get-video-tags", i, &tags);
if (tags) {
total_str = g_strdup_printf ("video stream %d:\n", i);
gtk_text_buffer_insert_at_cursor (text, total_str, -1);
g_free (total_str);
gst_tag_list_get_string (tags, GST_TAG_VIDEO_CODEC, &str);
total_str = g_strdup_printf (" codec: %s\n", str ? str : "unknown");
gtk_text_buffer_insert_at_cursor (text, total_str, -1);
g_free (total_str);
g_free (str);
gst_tag_list_free (tags);
}
}
for (i = 0; i < n_audio; i++) {
tags = NULL;
/* Retrieve the stream's audio tags */
g_signal_emit_by_name (data->playbin, "get-audio-tags", i, &tags);
if (tags) {
total_str = g_strdup_printf ("\naudio stream %d:\n", i);
gtk_text_buffer_insert_at_cursor (text, total_str, -1);
g_free (total_str);
if (gst_tag_list_get_string (tags, GST_TAG_AUDIO_CODEC, &str)) {
total_str = g_strdup_printf (" codec: %s\n", str);
gtk_text_buffer_insert_at_cursor (text, total_str, -1);
g_free (total_str);
g_free (str);
}
if (gst_tag_list_get_string (tags, GST_TAG_LANGUAGE_CODE, &str)) {
total_str = g_strdup_printf (" language: %s\n", str);
gtk_text_buffer_insert_at_cursor (text, total_str, -1);
g_free (total_str);
g_free (str);
}
if (gst_tag_list_get_uint (tags, GST_TAG_BITRATE, &rate)) {
total_str = g_strdup_printf (" bitrate: %d\n", rate);
gtk_text_buffer_insert_at_cursor (text, total_str, -1);
g_free (total_str);
}
gst_tag_list_free (tags);
}
}
for (i = 0; i < n_text; i++) {
tags = NULL;
/* Retrieve the stream's subtitle tags */
g_signal_emit_by_name (data->playbin, "get-text-tags", i, &tags);
if (tags) {
total_str = g_strdup_printf ("\nsubtitle stream %d:\n", i);
gtk_text_buffer_insert_at_cursor (text, total_str, -1);
g_free (total_str);
if (gst_tag_list_get_string (tags, GST_TAG_LANGUAGE_CODE, &str)) {
total_str = g_strdup_printf (" language: %s\n", str);
gtk_text_buffer_insert_at_cursor (text, total_str, -1);
g_free (total_str);
g_free (str);
}
gst_tag_list_free (tags);
}
}
}
/* This function is called when an "application" message is posted on the bus.
* Here we retrieve the message posted by the tags_cb callback */
static void application_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
if (g_strcmp0 (gst_structure_get_name (gst_message_get_structure (msg)), "tags-changed") == 0) {
/* If the message is the "tags-changed" (only one we are currently issuing), update
* the stream info GUI */
analyze_streams (data);
}
}
int main(int argc, char *argv[]) {
CustomData data;
GstStateChangeReturn ret;
GstBus *bus;
/* Initialize GTK */
gtk_init (&argc, &argv);
/* Initialize GStreamer */
gst_init (&argc, &argv);
/* Initialize our data structure */
memset (&data, 0, sizeof (data));
data.duration = GST_CLOCK_TIME_NONE;
/* Create the elements */
data.playbin = gst_element_factory_make ("playbin", "playbin");
if (!data.playbin) {
g_printerr ("Not all elements could be created.\n");
return -1;
}
/* Set the URI to play */
g_object_set (data.playbin, "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL);
/* Connect to interesting signals in playbin */
g_signal_connect (G_OBJECT (data.playbin), "video-tags-changed", (GCallback) tags_cb, &data);
g_signal_connect (G_OBJECT (data.playbin), "audio-tags-changed", (GCallback) tags_cb, &data);
g_signal_connect (G_OBJECT (data.playbin), "text-tags-changed", (GCallback) tags_cb, &data);
/* Create the GUI */
create_ui (&data);
/* Instruct the bus to emit signals for each received message, and connect to the interesting signals */
bus = gst_element_get_bus (data.playbin);
gst_bus_add_signal_watch (bus);
g_signal_connect (G_OBJECT (bus), "message::error", (GCallback)error_cb, &data);
g_signal_connect (G_OBJECT (bus), "message::eos", (GCallback)eos_cb, &data);
g_signal_connect (G_OBJECT (bus), "message::state-changed", (GCallback)state_changed_cb, &data);
g_signal_connect (G_OBJECT (bus), "message::application", (GCallback)application_cb, &data);
gst_object_unref (bus);
/* Start playing */
ret = gst_element_set_state (data.playbin, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) {
g_printerr ("Unable to set the pipeline to the playing state.\n");
gst_object_unref (data.playbin);
return -1;
}
/* Register a function that GLib will call every second */
g_timeout_add_seconds (1, (GSourceFunc)refresh_ui, &data);
/* Start the GTK main loop. We will not regain control until gtk_main_quit is called. */
gtk_main ();
/* Free resources */
gst_element_set_state (data.playbin, GST_STATE_NULL);
gst_object_unref (data.playbin);
return 0;
}

Required libraries: gstreamer-video-1.0 gtk+-3.0 gstreamer-1.0
所以此时编译需加上pkg-config --cflags --libs gstreamer-video-1.0 gtk+-3.0 gstreamer-1.0参数获取所需的头文件和库文件。
如果提示找不到gtk+-3.0,则安装。sudo apt install build-essential libgtk-3-dev
提示未安装gstreamer-video-1.0,则安装。sudo apt install libgstreamer-plugins-base1.0-dev
该例将会打开一个GTK+窗口并显示一个伴有音频的电影。媒体来自于互联网,所以窗口可能需要几秒才能显示,具体取决于网速。该窗口有一些按钮来暂停、停止和播放电影,还有个滑块显示当前位置,可以拖动或者改变它。此外,关于流的信息显示在右边的一列上。

代码分析

本例中,函数不再在使用之前定义,代码呈现的顺序也不总是和程序顺序相匹配。

1
2
3
4
5
6
7
8
#include <gdk/gdk.h>
#if defined (GDK_WINDOWING_X11)
#include <gdk/gdkx.h>
#elif defined (GDK_WINDOWING_WIN32)
#include <gdk/gdkwin32.h>
#elif defined (GDK_WINDOWING_QUARTZ)
#include <gdk/gdkquartzwindow.h>
#endif

首先需要注意的是,现在并不是与平台完全无关的了,因为我们需要为所使用的窗口系统包含适当的头文件。幸运的是,没有那么多支持的窗口系统,所以X11 for Linux,Win32 for Windows和Quartz for Mac OSX这三行足够了。本例主要有回调函数组成,这些回调函数将从GStreamer或GTK+中调用。所以先看一下主函数,其中将会用到所有的回调函数。

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
int main(int argc, char *argv[]) {
CustomData data;
GstStateChangeReturn ret;
GstBus *bus;
/* Initialize GTK */
gtk_init (&argc, &argv);
/* Initialize GStreamer */
gst_init (&argc, &argv);
/* Initialize our data structure */
memset (&data, 0, sizeof (data));
data.duration = GST_CLOCK_TIME_NONE;
/* Create the elements */
data.playbin = gst_element_factory_make ("playbin", "playbin");
if (!data.playbin) {
g_printerr ("Not all elements could be created.\n");
return -1;
}
/* Set the URI to play */
g_object_set (data.playbin, "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL);

标准的GStreamer和playbin管道创建,以及GTK+初始化。

1
2
3
4
/* Connect to interesting signals in playbin */
g_signal_connect (G_OBJECT (data.playbin), "video-tags-changed", (GCallback) tags_cb, &data);
g_signal_connect (G_OBJECT (data.playbin), "audio-tags-changed", (GCallback) tags_cb, &data);
g_signal_connect (G_OBJECT (data.playbin), "text-tags-changed", (GCallback) tags_cb, &data);

我们希望在流上出现新标签(元数据)时收到通知,为了简单起见,将处理来自相同回调函数tag_cb的所有种类标签(视频、音频和文本)。
然后创建GUI:

1
2
/* Create the GUI */
create_ui (&data);

所有的GTK+部件创建和信号注册都发生在这个函数中,它只包含GTK相关的函数调用,所以可以跳过它的定义。其所注册的信号传递用户命令,如下面在查看回调时所示。

1
2
3
4
5
6
7
8
/* Instruct the bus to emit signals for each received message, and connect to the interesting signals */
bus = gst_element_get_bus (data.playbin);
gst_bus_add_signal_watch (bus);
g_signal_connect (G_OBJECT (bus), "message::error", (GCallback)error_cb, &data);
g_signal_connect (G_OBJECT (bus), "message::eos", (GCallback)eos_cb, &data);
g_signal_connect (G_OBJECT (bus), "message::state-changed", (GCallback)state_changed_cb, &data);
g_signal_connect (G_OBJECT (bus), "message::application", (GCallback)application_cb, &data);
gst_object_unref (bus);

其中,gst_bus_add_watch函数用于注册用于接收所有的消息并发送给GStreamer总线。可以通过使用信号来达到更精细的粒度,这使得我们仅注册感兴趣的消息。
通过调用gst_bus_add_signal_watch函数,我们指导总线在每次收到一个消息时发出一个信号。信号名称是message::detail,其中‘detail’是触发信号发出的消息。例如,当总线接收到EOS消息时,将发出一个名为message::eos的信号。
例中仅使用信号描述(detail)来注册所感兴趣的消息。如果我们注册了一个消息的信号,我们将收到每个消息的通知,如gst_bus_add_watch函数所做的一样。
为了使“bus watches”工作(无论是gst_bus_add_watch还是gst_bus_add_signal_watch),必须运行GLib主循环。这种情况下,它隐藏在GTK+主循环中。

1
2
/* Register a function that GLib will call every second */
g_timeout_add_seconds (1, (GSourceFunc)refresh_ui, &data);

在将控制移交给GTK+之前,使用g_timeout_add_seconds函数来注册另一个回调函数————超时,且每秒会被调用:用其从refresh_ui函数刷新GUI。
在这之后,我们完成了建立并启动GTK+主循环。感兴趣的事件发生时,将从回调函数中重新获取控制权。每个回调函数都有不同的签名,具体取决于调用者。可以再信号的文档中查找签名(参数的含义和返回值)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* This function is called when the GUI toolkit creates the physical window that will hold the video.
* At this point we can retrieve its handler (which has a different meaning depending on the windowing system)
* and pass it to GStreamer through the VideoOverlay interface. */
static void realize_cb (GtkWidget *widget, CustomData *data) {
GdkWindow *window = gtk_widget_get_window (widget);
guintptr window_handle;
if (!gdk_window_ensure_native (window))
g_error ("Couldn't create native window needed for GstVideoOverlay!");
/* Retrieve window handler from GDK */
#if defined (GDK_WINDOWING_WIN32)
window_handle = (guintptr)GDK_WINDOW_HWND (window);
#elif defined (GDK_WINDOWING_QUARTZ)
window_handle = gdk_quartz_window_get_nsview (window);
#elif defined (GDK_WINDOWING_X11)
window_handle = GDK_WINDOW_XID (window);
#endif
/* Pass it to playbin, which implements VideoOverlay and will forward it to the video sink */
gst_video_overlay_set_window_handle (GST_VIDEO_OVERLAY (data->playbin), window_handle);
}

在应用程序生命周期的这一点上,我们知道GStreamer应该呈现视频的窗口句柄(无论是X11的XID,Window的HWND还是Quartz的NSView)。我们只需从窗口系统中检索它,并使用gst_video_overlay_set_window_handle通过GstVideoOverlay接口将其传递给playbin。playbin将定位视频接收器并将处理程序传递给它,所以它不会创建自己的窗口并使用它。playbin和GstVideoOverlay将此过程简化了许多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* This function is called when the PLAY button is clicked */
static void play_cb (GtkButton *button, CustomData *data) {
gst_element_set_state (data->playbin, GST_STATE_PLAYING);
}
/* This function is called when the PAUSE button is clicked */
static void pause_cb (GtkButton *button, CustomData *data) {
gst_element_set_state (data->playbin, GST_STATE_PAUSED);
}
/* This function is called when the STOP button is clicked */
static void stop_cb (GtkButton *button, CustomData *data) {
gst_element_set_state (data->playbin, GST_STATE_READY);
}

这三个回调函数是关于GUI的播放,暂停和停止按钮的,它们只需要将管道设置为相应的状态即可。值得注意的是,在STOP状态下,将管道状态设置为READY。可以将流水线一直带到NULL状态,但是会导致过渡慢一点,因为有些资源(如音频设备)需要重新释放重新获取。

1
2
3
4
5
/* This function is called when the main window is closed */
static void delete_event_cb (GtkWidget *widget, GdkEvent *event, CustomData *data) {
stop_cb (NULL, data);
gtk_main_quit ();
}

gtk_main_quit最终会在main中调用gtk_main_run来终止,并在这种情况下完成整个程序。这里,在停止管道(只是为了整洁)后,当主窗口关闭时调用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* This function is called everytime the video window needs to be redrawn (due to damage/exposure,
* rescaling, etc). GStreamer takes care of this in the PAUSED and PLAYING states, otherwise,
* we simply draw a black rectangle to avoid garbage showing up. */
static gboolean draw_cb (GtkWidget *widget, cairo_t *cr, CustomData *data) {
if (data->state < GST_STATE_PAUSED) {
GtkAllocation allocation;
/* Cairo is a 2D graphics library which we use here to clean the video window.
* It is used by GStreamer for other reasons, so it will always be available to us. */
gtk_widget_get_allocation (widget, &allocation);
cairo_set_source_rgb (cr, 0, 0, 0);
cairo_rectangle (cr, 0, 0, allocation.width, allocation.height);
cairo_fill (cr);
}
return FALSE;
}

当有数据流时(处于PAUSED和PLAYING状态),视频接收器负责刷新视频窗口的内容。但其他情况下不会这样,所以必须我们自己来做: 例中我们是使用一个黑色的矩形填充窗口。

1
2
3
4
5
6
7
/* This function is called when the slider changes its position. We perform a seek to the
* new position here. */
static void slider_cb (GtkRange *range, CustomData *data) {
gdouble value = gtk_range_get_value (GTK_RANGE (data->slider));
gst_element_seek_simple (data->playbin, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT,
(gint64)(value * GST_SECOND));
}

通过GStreamer和GTK+的协作,可以非常容易地实现一个复杂的GUI元素,如一个搜索条(或者允许搜索的滑块)。如果滑块被拖动到新位置,则告诉GStreamer使用gst_element_seek_simple查找该位置。 滑块已经设置,它的值代表秒。
值得注意的是,一些性能和响应可以通过不去响应所有的单个用户的搜索请求来获得。由于搜索操作需要花费一些时间,所以在允许另一个搜索操作之前,更好的办法是等待一会(如半秒钟)。否则,如果用户疯狂的拖拽滑动条,应用程序看起来可能也没有响应,因为在一个新的搜索操作在队列中之前将不会允许任何搜索。

1
2
3
4
5
6
7
/* This function is called periodically to refresh the GUI */
static gboolean refresh_ui (CustomData *data) {
gint64 current = -1;
/* We do not want to update anything unless we are in the PAUSED or PLAYING states */
if (data->state < GST_STATE_PAUSED)
return TRUE;

该函数将移动滑块以反映媒体当前的位置。如果我们不处于PLAYING状态,那么在这里没有任何事情可做(位置和持续时间查询通常会失败)。

1
2
3
4
5
6
7
8
9
/* If we didn't know it yet, query the stream duration */
if (!GST_CLOCK_TIME_IS_VALID (data->duration)) {
if (!gst_element_query_duration (data->playbin, GST_FORMAT_TIME, &data->duration)) {
g_printerr ("Could not query current duration.\n");
} else {
/* Set the range of the slider to the clip duration, in SECONDS */
gtk_range_set_range (GTK_RANGE (data->slider), 0, (gdouble)data->duration / GST_SECOND);
}
}

可以设置滑块的范围以防我们在不知情的情况下恢复clip的持续时间。

1
2
3
4
5
6
7
8
9
10
if (gst_element_query_position (data->playbin, GST_FORMAT_TIME, &current)) {
/* Block the "value-changed" signal, so the slider_cb function is not called
* (which would trigger a seek the user has not requested) */
g_signal_handler_block (data->slider, data->slider_update_signal_id);
/* Set the position of the slider to the current pipeline positoin, in SECONDS */
gtk_range_set_value (GTK_RANGE (data->slider), (gdouble)current / GST_SECOND);
/* Re-enable the signal */
g_signal_handler_unblock (data->slider, data->slider_update_signal_id);
}
return TRUE;

查询当前的管道位置,并根据滑块设置其位置。这将会触发一个value-changed信号,我们可以通过其知道用户在拖动滑块。除非用户请求它们,否则我们不希望发生这种情况,所以在此操作期间,使用g_sinal_handler_blockg_signal_handler_unblock禁用value-changed的信号发出。
该函数返回True将在之后保持其调用。如果返回FALSE,定时器将被删除。

1
2
3
4
5
6
7
8
/* This function is called when new metadata is discovered in the stream */
static void tags_cb (GstElement *playbin, gint stream, CustomData *data) {
/* We are possibly in a GStreamer working thread, so we notify the main
* thread of this event through a message in the bus */
gst_element_post_message (playbin,
gst_message_new_application (GST_OBJECT (playbin),
gst_structure_new_empty ("tags-changed")));
}

这里是该例的重点。当媒体中发现新标签时,该函数将会从streaming线程中调用,即从一个应用程序线程(或主线程)之外的线程调用。我们这里希望做的是更新GTK+的部件来反映这个新的信息,但GTK+不允许主线程之外的其它线程的操作。
解决方法是让playbin在总线上发布消息并返回给调用线程。在适当时候,主线程会接收到这个消息并更新GTK。
gst_element_post_message函数使GStreamer元素将给定的消息发送到总线。gst_message_new_application函数创建一个新的应用程序类型的消息。GStreamer消息有不同的类型,且这种特殊类型将保留给应用程序:它会通过不受GStreamer影响的总线。
类型列表可在GstMessageType文档中找到。
消息可以通过嵌入的GstStructure提供额外的信息,GstStructure是一个非常灵活的数据容器。这里使用gst_structure_new创建一个新结构体,并将其命名为tags-changed,以避免在我们想发送其它应用程序消息时发生混淆。
然后,一旦在主线程中,总线将会收到这个消息并发送message::application信号,该信号与application_cb函数关联:

1
2
3
4
5
6
7
8
9
/* This function is called when an "application" message is posted on the bus.
* Here we retrieve the message posted by the tags_cb callback */
static void application_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
if (g_strcmp0 (gst_structure_get_name (gst_message_get_structure (msg)), "tags-changed") == 0) {
/* If the message is the "tags-changed" (only one we are currently issuing), update
* the stream info GUI */
analyze_streams (data);
}
}

一旦确定它是标签变化(tag-changed)消息,则调用analyze_streams函数。其基本上从流中恢复标签,并将其写入GUI中的文本小部件中。
虽然该例代码量较大,但所需的概念很少且很容易。
最后效果图如下:



❌