小工具(三)

竟然真的有后续!!!(Again) 花里胡哨的命令行监视器bashtop 作用基本上就跟top差不多,不过视觉效果还不错。对比一下, top命令: bashtop命令: 其中,最上面是CPU资源使用情况,左侧是内存和网络资源使用情况,右边是和top一样的进程状态,可以使用方向键来选择进程,或者翻页键来翻页,按f可以filter需要找的进程。 在选中进程之后,可以使用回车键查看该进程详细信息,t和k健可以terminate或者kill进程,i可以中断进程(interrupt)。 按q退出,按ESC可以跳回主界面,如果不清楚命令或者快捷键,可以选择HELP查看,总之使用方法比top简单不少。 缺点,就是感觉没有top出来的快,可能是因为要在terminal上绘图。 文件格式转换工具xxd xxd是linux上比较老的文件格式转换工具了,经常使用vim的在打开二进制文件的时候输入的!xxd就是这个命令,一般作用是将文件转为十六进制。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Usage: xxd [options] [infile [outfile]] or xxd -r [-s [-]offset] [-c cols] [-ps] [infile [outfile]] Options: -a toggle autoskip: A single '*' replaces nul-lines. Default off. -b binary digit dump (incompatible with -ps,-i,-r). Default hex. -C capitalize variable names in C include file style (-i). -c cols format <cols> octets per line. Default 16 (-i: 12, -ps: 30). -E show characters in EBCDIC. Default ASCII. -e little-endian dump (incompatible with -ps,-i,-r). -g number of octets per group in normal output. Default 2 (-e: 4). -h print this summary. -i output in C include file style. -l len stop after <len> octets. -o off add <off> to the displayed file position. -ps output in postscript plain hexdump style. -r reverse operation: convert (or patch) hexdump into binary. -r -s off revert with <off> added to file positions found in hexdump. -s [+][-]seek start at <seek> bytes abs. (or +: rel.) infile offset. -u use upper case hex letters. -v show version: "xxd V1.10 27oct98 by Juergen Weigert". 文件转十六进制 最简单的方式是xxd file,会在终端输出转换后该文件的十六进制内容,使用重定向或者管道可以写到文件或作用于其他输入。 如果文件比较大,可能转换比较慢,而且很多并不是我们需要的话,可以使用-s或者-l参数。 -s参数如上所示,“start at bytes abs”,从指定位置开始转换。 -l参数则是指定要转换内容的长度。 二进制文件的比较 如果要比较两个二进制文件内容是否一致,可以借助于xxd命令。 例如,需要比较两个原始的yuv格式视频文件是否一致,比如两种方式通过h264解码出来的文件,平常的diff命令是无法完成的,可以先利用xxd命令转成十六进制后再进行比较。 1 2 3 xxd a.yuv > a.hex xxd b.yuv > b.hex diff a.hex b.hex 之所以举这个例子,是因为yuv是原始视频数据,比如一个1920x1080的h264编码后的视频大小只有8M多,但是其对应的解码后的yuv大小则可能有1.1G多,而使用xxd转成十六进制文件之后,体积可以达到4.5G,直接用diff命令比较费时,而且用vim打开还容易卡死。而通常只需要比较其中一部分可以判断出解码的文件是否正常,这时候就可以在xxd命令转换的时候加上-l或者-s参数,指定比较的文件位置和长度,可以很快的得到结果。 当然,比较方式也不是绝对的,对于视频文件,使用ffmpeg也可以查看许多详细信息。 应用代理工具proxychains 这个其实很早以前打算说的,不过忘记了。作用有点像chrome里的SwitchyOmega之类的扩展程序,不同的是,SwitchyOmega是针对特定网页选择本地的代理端口和协议,proxychains是针对某个命令或者应用选择本地代理端口和协议。 安装方法比较简单,ubuntu下直接使用apt命令可以安装: 1 sudo apt install proxychains 安装好之后,配置文件放在/etc目录下的proxychains.conf文件,修改里面的协议和端口即可。 使用方式是直接在命令前加上proxychains,这样只对该命令代理,不影响其他应用。 比如使用ubuntu的snap命令下载软件时,通常国内会比较慢,如果有代理,则可以使用: 1 proxychains sudo snap install xxx 其实很多工具也不是啥新鲜玩意,不过有时候确实挺实用的,不过仅仅对工具而言,没有任何性质,怎么用在于个人。

2020/8/10
articleCard.readMore

相机小述

看了看,好像自己确实是太懒了,不过虽然这样,说的时候还是要把锅甩给疫情的,想起以前接触相机这么久了,不如就还记得的一点东西,介绍一下,水一点东西,混一次提交。 不过不知道有没有帮助,不是介绍什么拍摄三要素(快门、光圈、感光度)的控制,也不是什么九宫格拍摄法之类的,说实话,那些五花八门的拍摄方法我也不太熟悉,就讲讲相机结构。 传感器类型 图像传感器主要有两种,线阵和面阵,平时看到的线阵相机和面阵相机就是因为用的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,图像的亮度、饱和度、对比度等等,由于比较常见,顾名思义,就不继续赘述。 先偷个懒,改天想到了啥,再续狗尾。

2020/7/19
articleCard.readMore

四元数与旋转矩阵

在计算物体旋转时,如相机标定中的旋转矩阵R,通常都是以三维矩阵表示,三个自由度分别是绕三个坐标轴的旋转角度,但在Unity中,或者像colmap中,都是以四元数(Quaternion)来表示三维旋转的。实际上是复平面向量与实数域矩阵的一个转换关系,这里推导一下。 二维情况 一维数轴上的复数对应于一个二维实数空间,比如一个二维空间坐标为 $(x,y)$ 的复数表示为 $x + yi$ 。 给定两个复数 $z1 = a + bi$, $z2 = c +di$, 其乘积可以表示为: $$ z1z2 = (a+bi)(c+di) = (ac-bd)+(ad+bc)i $$ 对于向量$z2$,与$z1$的乘积可以表示为矩阵形式,即: $$ \begin{bmatrix} a & -b \\ b & a \\ \end{bmatrix} \cdot z2 $$ 如果将$z2$也看做一个变换表示成矩阵形式,则 $$ z1z2 = \begin{bmatrix} a & -b \\ b & a \\ \end{bmatrix} \cdot \begin{bmatrix} c & -d \\ d & a \\ \end{bmatrix} $$ 此时满足交换律。 二维旋转 设向量模长为1,即$\sqrt[2]{a^2 + b ^2} =1$,则$a = cos\theta, b=sin\theta$, 则对一个向量$\vec{v}=x+yi$,其乘积为$\vec{v} \dot z = (xcos\theta -ysin\theta) +(xsin\theta +ycos\theta)i$, 设向量$\vec{v}$的模为r,则$\vec{v}\cdot \vec{z} = r(cos\theta_1cos\theta_2 -sin\theta_1sin\theta_2)+r(cos\theta_1sin\theta_1 + sin\theta_1cos\theta_2)i = rcos(\theta_1 +\theta_2) +rsin(\theta_1 + \theta_2) i $ 可以看出,一个单位的二维向量,或一维复数,可以表示成一个旋转变换,即逆时针旋转$\theta$角。 所以二维的旋转矩阵可以很直观的求得: $$ \begin{bmatrix} cos\theta & -sin\theta \\ sin\theta & cos\theta \\ \end{bmatrix} $$ 极坐标形式 其实将复数表示成极坐标形式,欧拉公式将三角函数和复平面关联起来,于是可以很直接的将$ e^{i\theta} = cos\theta + isin\theta$带入,角度旋转即$$ e^{i\theta_1} * e^{i\theta_2} = e^{i(\theta_1 + \theta_2)}$$ 三维情况 先看向量的旋转: 将向量$\vec{v}$绕旋转轴$\vec{u}$旋转$\theta$角。 将$\vec{v}$分解成两个正交向量的和,分别是平行于$\vec{u}$和垂直于$\vec{u}$的向量,记为$\vec{v_{||}}$和$\vec{v_\bot}$,平行的向量旋转不变,因此只需要考虑垂直向量即可。 对于$\vec{v_{||}}$,其实就是在$\vec{u}$上的正交投影,因此有$\vec{v_{||}} = \frac{\vec{u}\cdot \vec{v}}{||\vec{u}||^2} \vec{u}$,设$\vec{u}$为单位向量,则可以表示为$\vec{v_{||}} = (\vec{u}\cdot \vec{v})\vec{u}$ 。 所以,$$\vec{v_\bot} = \vec{v} - \vec{v_{||}} = \vec{v} - (\vec{u}\cdot \vec{v})\vec{u}$$ 因为$\vec{v_\bot}$和$\vec{u}$垂直,所以旋转可以转化成二维平面的旋转,构造一个向量$\vec{w} = \vec{u} \times \vec{v}$,如图所示, 所以旋转后的向量为$$\begin{aligned} \vec{v_\bot}^\prime & = \vec{v_\bot}cos\theta + \vec{w}sin\theta \\ & = \vec{v_\bot}cos\theta + \vec{u} \times \vec{v_\bot} sin\theta \end{aligned}$$ 最后旋转后的向量为 $$ \begin{aligned} \vec{v}^\prime & = \vec{v_{||}} + \vec{v_\bot}^\prime \\ & = \vec{v_{||}} + \vec{v_\bot}cos\theta + \vec{u} \times \vec{v_\bot} sin\theta \\ & = (\vec{u}\cdot \vec{v})\vec{u} + \vec{v_\bot}cos\theta + \vec{u} \times \vec{v_\bot} sin\theta \\ & = (\vec{u}\cdot \vec{v})\vec{u} + (\vec{v} - (\vec{u}\cdot \vec{v})\vec{u} )cos\theta + \vec{u} \times (\vec{v} - (\vec{u}\cdot \vec{v})\vec{u}) sin\theta \\ & = \vec{v}cos\theta +(1-cos\theta)(\vec{u}\cdot \vec{v})\vec{u} +(\vec{u}\times \vec{v}))sin\theta \end{aligned}$$ 四元数 四元数可以看做一个四元向量,或是有三个虚部的复数,如$q = a + bi+cj+dk$,也可以写成矩阵形式, $$\vec{q} = \begin{bmatrix} a \\ b \\ c \\ d \\ \end{bmatrix} $$ 如三维坐标轴的顺序,复数相乘有, $$ij =k \\ jk =i \\ ki =j$$ 令$q1 = a + bi+cj+dk$, $q2 = e + fi+gj+hk$ 则左乘$q1$可以为 $$ \begin{aligned} q1q2 & = ae + a f i + agj + ahk + \\ & bei − b f + bgk − bhj + \\ & cej − c f k − cg + chi + \\ & dek + d f j − dgi − dh \\ & = ( ae − b f − cg − dh )+ \\ & ( be + a f − dg + ch ) i \\ & ( ce + d f + ag − bh ) j \\ & ( de − c f + bg + ah ) k \end{aligned} $$ 矩阵形式可以写成, $$q1q2 = \begin{bmatrix} a & -b & -c & -d \\ b & a & -d & c \\ c & d & a & -b \\ d & -c & b & a \end{bmatrix} \begin{bmatrix} e \\ f \\ g \\ h \\ \end{bmatrix} $$ 四元数向量不满足交换律,右乘会有一些区别。 Graßmann积 将四元数的虚部表示成一个向量,即$ q1 = [a,\vec{v}]$,$q2 = [e, \vec{u}]$,其中,$\vec{v} = bi+cj+dk$,$\vec{u} = fi+gj+hk$。 则左乘$q1$可以化简成 $$q1q2 = [ ae − \vec{v} \cdot \vec{u} , a\vec{u} + e\vec{v} + \vec{v} \times \vec{u} ]$$ 这个结果也被称为 Graßmann 积。 这样,当a和e为零时,两者乘积可以写成, $$ q1q2 = [− \vec{v} \cdot \vec{u}, \vec{v} \times \vec{u} ]$$ 同纯虚数的说法,这时q1和q2叫纯四元数。 共轭性质 与二元虚数类似,四元数的共轭也是将虚部方向取反,即 $q^* = a - bi - cj - dk$,则 $$ \begin{aligned} qq^* & = [s,\vec{v}] \cdot [s,-\vec{v}] \\ & = [s^2 - \vec{v} \cdot (-\vec{v}), s(-\vec{v}) + s\vec{v} + \vec{v}\times(-\vec{v})] \\ & = [s^2 + \vec{v} \cdot \vec{v}, \vec{0}] \\ \end{aligned} $$ 实部平方与虚部平方和,即该向量的模的平方,最后虚部为零,所以 $$ \begin{aligned} qq^* & = [s^2 + \vec{v} \cdot \vec{v}, \vec{0}] \\ & = s^2 + |\vec{v}|^2 \\ & = a^2 + b^2 + c^2 +d^2 \\ \end{aligned} $$ 由于q与其共轭的积最后是个标量,为其模长,所以该乘法是满足交换律的。即$qq^* = q^*q = |q|^2$。 这样, $$ q^*q = |q|^2 \\ \frac{q^*}{|q|^2}q =1 $$ 则可以发现$q^{-1} = \frac{q^*}{|q|^2}$ 满足$q^{-1}q = qq^{-1} =1$,即为该四元数的逆。 而单位四元数的逆即为其共轭四元数。 三维旋转 旋转轴$\vec{u}$不妨设为单位向量,与之前的旋转类似, $$ \vec{v’} = \vec{v’_{||}} +\vec{v’_\bot} = \vec{v_{||}} +\vec{v’_\bot} $$ 之前计算过正交与旋转轴的向量旋转得到的结果,$$\vec{v’_\bot} = \vec{v_\bot}cos\theta + (\vec{u}\times \vec{v_\bot})sin\theta$$ 设u,v都是纯四元数,即$u = [0,\vec{u}]$,$v = [0,\vec{v}]$,两个纯四元数的Graßmann积为$$uv_\bot = [− \vec{v_\bot} \cdot \vec{u}, \vec{v_\bot} \times \vec{u} ] = [ 0, \vec{v_\bot} \times \vec{u} ] = \vec{v_\bot} \times \vec{u} $$ 也是一个纯四元数。 所以, $$ \begin{aligned} v’_\bot & = v_\bot cos\theta + (u v_\bot)sin\theta \\ & = (cos\theta + usin\theta)v_\bot \end{aligned} $$ 令四元数$q = (cos\theta + usin\theta)v_\bot$,则$ v’_\bot = qv_\bot$ 所以对于垂直于旋转轴的向量,旋转$\theta$角度之后的向量可以用四元数的乘法来获得, 用向量表示为$q = [cos\theta, \vec{u}sin\theta]$ 由于$\vec{u}$是单位向量,所以$$||q|| = cos^2\theta + ||\vec{u}||^2 sin^2\theta =1 $$ 同样的表示方式,$qqv_\bot = q(qv_\bot)$几何上表示旋转两次,因此有$qqv_\bot = (cos2\theta + usin2\theta)v_\bot $ 所以最后旋转之后的四元数, $$\begin{aligned} v’ & = v’_{||} + v’_\bot \\ & = v_{||} + qv_\bot \\ & = pp^{-1}v_{||} + ppv_\bot \\ & = pp^*v_{||} + ppv_\bot \end{aligned} $$ 其中,$p = [cos(\frac{\theta}{2}),\vec{u}sin(\frac{\theta}{2})]$,是旋转半角的单位向量,因此$q=p^2$。 交换性质: 由之前的Graßmann积,上式中,将q写成向量形式,$q = [\alpha, \beta\vec{u}]$ $$ \begin{aligned} qv_{||} & = [\alpha, \beta \vec{u}] \cdot [0,\vec{v}_{||}] \\ & = [-\beta \vec{u} \cdot \vec{v}_{||} , \alpha \vec{v}_{||} + \beta \vec{u} \times \vec{v}_{||}] \\ & = [-\beta \vec{u} \cdot \vec{v}_{||} , \alpha \vec{v}_{||}] \\ \end{aligned} $$ 右乘, $$ \begin{aligned} v_{||}q & = [0,\vec{v}_{||}] \cdot [\alpha, \beta \vec{u}] \cdot \\ & = [-\beta \vec{u} \cdot \vec{v}_{||} , \alpha \vec{v} +\vec{u} \times \vec{v}_{||}] \\ & = [-\beta \vec{u} \cdot \vec{v}_{||} , \alpha \vec{v} ] \\ & = qv_{||} \end{aligned} $$ 再看垂直部分, $$ \begin{aligned} qv_\bot & = [\alpha, \beta \vec{u}] \cdot [0,\vec{v}_{}] \\ & = [-\beta \vec{u} \cdot \vec{v}_\bot , \alpha \vec{v}_\bot + \beta \vec{u} \times \vec{v}_\bot] \\ & = [0, \alpha \vec{v}_\bot + \beta \vec{u} \times \vec{v}_\bot] \\ \end{aligned} $$ 右乘共轭, $$ \begin{aligned} v_\bot q^* & = [0,\vec{v}_\bot] \cdot [\alpha, -\beta \vec{u}] \cdot \\ & = [-\beta \vec{u} \cdot \vec{v}_\bot , \alpha \vec{v}_\bot + \beta \vec{u} \times \vec{v}_\bot] \\ & = [0, \alpha \vec{v}_\bot + \beta \vec{u} \times \vec{v}_\bot] \\ & = qv_\bot \end{aligned} $$ 所以旋转之后的结果可以化简为 $$\begin{aligned} v’ & = pp^*v_{||} + ppv_\bot \\ & = pv_{||}p^* + pv_\bot p^* \\ & = p(v_{||} + v_\bot) p^* \\ & = pvp^* \end{aligned} $$ 实际上,从计算过程可以看出,对于平行分量,乘$pp^*$,实际上是没有变化,对于垂直分量,乘$pp$,旋转了$\frac{\theta}{2} + \frac{\theta}{2} = \theta$角度。因此可以用旋转半角的四元数乘法来表示绕单位向量$\vec{u}$的旋转。 矩阵形式 单位向量$p= [cos(\frac{\theta}{2}), \vec{u}sin(\frac{\theta}{2})]$,以通用四元数方式表示为$p = a + bi + cj + dk$ 其中$a=cos(\frac{\theta}{2}), b=u_x sin(\frac{\theta}{2}),c=u_y sin(\frac{\theta}{2}), b=u_z sin(\frac{\theta}{2})$ 写成矩阵形式,之前说了四元数的矩阵形式左乘和右乘有点区别,左乘矩阵为 $$L= \begin{bmatrix} a & -b & -c & -d \\ b & a & -d & c \\ c & d & a & -b \\ d & -c & b & a \end{bmatrix} $$ 右乘的矩阵等同于左乘矩阵 $$R = \begin{bmatrix} a & -b & -c & -d \\ b & a & -d & -c \\ c & -d & a & b \\ d & c & -b & a \end{bmatrix} $$ 所以有, $$ \begin{aligned} qvq^* & = L(q)R(q^*)v \\ & = \begin{bmatrix} a & -b & -c & -d \\ b & a & -d & c \\ c & d & a & -b \\ d & -c & b & a \\ \end{bmatrix} \begin{bmatrix} a & b & c & d \\ -b & a & -d & c \\ -c & d & a & -b \\ -d & -c & b & a \\ \end{bmatrix} v \\ & = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1-2c^2-2d^2 & 2bc-2ad & 2ac+2bd \\ 0 & 2bc+2ad & 1-2b^2-2d^2 & 2cd -2ab \\ 0 & 2bd-2ac & 2ab+2cd & 1-2b^2-2c^2 \\ \end{bmatrix} v \\ \end{aligned} $$ 矩阵最外圈不会有任何影响,所以可以得出向量$\vec{v}$绕单位向量旋转轴$\vec{u}$旋转的三维矩阵变换,即 $$\vec{v’} = \begin{bmatrix} 1-2c^2-2d^2 & 2bc-2ad & 2ac+2bd \\ 2bc+2ad & 1-2b^2-2d^2 & 2cd -2ab \\ 2bd-2ac & 2ab+2cd & 1-2b^2-2c^2 \\ \end{bmatrix} \vec{v} $$ 其中$a=cos(\frac{\theta}{2}), b=u_x sin(\frac{\theta}{2}),c=u_y sin(\frac{\theta}{2}), b=u_z sin(\frac{\theta}{2})$。

2020/1/8
articleCard.readMore

小工具(二)

竟然真的有后续!!! 选中区和剪贴板工具XClip XClip工具可以非常方便的操作,清空剪贴板,高效的复制粘贴等,直接从apt商店就可以直接安装(测试所用Ubuntu环境)。 输出选中区和剪贴板内容 选中区是指用鼠标选中区域的内容,在Ubuntu下有个快捷的复制粘贴方式就是利用选中区,通常鼠标中键是粘贴选中区内容。用xclip输出选中区内容命令: 1 xclip -o 如果要读取系统剪贴板的内容,可以加个参数: 1 xclip -selection clipboard -o 或者简写为: 1 xclip -sel c -o 输出到文件 1 2 xclip -o > file xclip -sel c -o >file 读入到选中区和剪贴板 平时用鼠标和Ctrl C\V或者Ctrl Shift C\V即可完成一般的复制粘贴,但有大量文字时,满满的移动鼠标就显得比较繁琐,利用xlip可以不借助鼠标比较方便的实现。 1 2 xclip -i file xclip -sel -c -i file 其他几种形式: 1 2 3 4 5 6 echo "Hello Dog" | xclip echo "Hello Dog" | xclip -sel c xclip < file xclip -sel c < file xclip file xclip -sel c file 图像转LaTeX工具Mathpix 可以将包含公式的图像直接转化为LaTeX语法。 1 sudo snap install mathpix-snipping-tool 可以使用键盘快捷键 Ctrl+Alt+M 开始使用 Mathpix 截图,它会立即将图中公式转换为 LaTeX 语法并保存在剪贴板中。 然而现在开始收费了,免费版的一个月只能用50次。 广告拦截工具Pi-hole 与传统的浏览器广告拦截插件不同,声称是通过个人Linux硬件进行全网广告拦截,通过DNS污染实现,具体仓库在此。 安装方式很多,最直接就是curl直接拉脚本运行: 1 curl -sSL https://install.pi-hole.net | bash 安装完成后可以配置路由器来使所有的经过动态主机分配协议(DHCP)的主机使用Pi-hole作为其DNS服务器,来确保该网络所有的主机都可以进行广告拦截。配制方法在此。 如果路由器不支持DNS设置,可以禁用DHCP后,使用Pi-hole的内置DHCP服务器。

2019/12/16
articleCard.readMore

私有办公服务搭建

鉴于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 在浏览器中打开指定端口地址即可开始绘制。

2019/12/13
articleCard.readMore

小工具(一)

太久没回来了,其实很多次想写但是无法静下心来,思绪如开始停更的三月里乱飞的柳絮,总感觉经历了很多,但却没有力气吐出一个字。终于处暑,昔时聚在一起的人们也已经走得差不多了,终于也想起了,该随便写点什么了。 就推荐一些小工具吧,这段时间发现的,感觉挺有意思的。以后也不知道会不会继续,先假设是个连续剧吧,写完拖更的那种。 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…

2019/8/19
articleCard.readMore

图床搭建

周末回来发现博客里的图片全成了码云的个性403了,原来是因为很多公共的图床存活不了多少年,而且当图不再使用时,不便于回收,所以在图床选择这方面,纠结了很久。后来Github认为在其上传图片获取外链并不算滥用,但是Github在国内的速度比较慢,对于背景图来说,就是肉眼可见的卡了。好在当时发现了Coding和码云,于是在码云(Gitee)上用其Issue页上传图片做图床。 然而不幸的是,大概他们是觉得滥用了,于是被防盗链了。 图床搭建 然后又像以前一样,对比了很多现有的图床,依然没找到能安心存放的地方。于是想到手里还有VPS,便打算搭建一个了。 最开始是一个同学送的天翼云,估计是办宽带送的,结果发现80和443端口全被封了,问客服说要备案才行,想着备案就备案吧,然而当看到备案要填的资料时,立马放弃了。 另一个就是Vultr上的,国外的云主机商相比于国内,条件限制宽了不少,不用动不动就实名或者备案,虽不会发表什么不当言论,但吃相看着令人难受。 现有图床挺多的,目前打算用一个开源的荔枝图床,有现成的Docker镜像,界面比较美观,官网在这。 Docker环境安装 其实云主机还好,如果觉得官方的Docker下载太慢,也有Daocloud的CDN加速的镜像,直接一条命令就可以完成: 1 curl -sSL https://get.daocloud.io/docker | sh 等安装完成之后,就可以拉取镜像了, 1 docker pull kdelfour/lychee-docke 或者直接试运行,看看效果: 1 docker run -it -d -p 80:80 kdelfour/lychee-docke 这时,在本地浏览器输入云主机的IP,就可以看到一个基本的界面了。 完整的命令是: 1 docker run -it -d -p 80:80 -v /your-path/uploads/:/uploads/ -v /your-path/data/:/data/ -v /your-path/mysql/:/mysql/ kdelfour/lychee-docker 分别挂载上传图片的uploads文件夹、data文件夹和数据库储存的mysql文件夹,并映射80端口。 这时候再登陆该地址,会提示要初始化一些配置,可以用官方提供的配置: 1 2 3 4 url : localhost database name : lychee user name : lychee user password : lychee 然后就是创建用户名和密码了,创建成功,图床已经初步建成。 但是现在直接做图床,看到的链接都是丑陋的IP和http字段组成的地址,干干巴巴,麻麻赖赖的,一点都不圆润。 所以接下来就要盘它了。一方面是IP更换为域名,另一方面是HTTP更换为更为安全的HTTPS。 域名申请 为IP申请一个域名,然后配置DNS,将域名直接以A类指向云主机IP即可。过一段时间应该就可以在本地看到域名解析生效: 1 $dig $YourUrl 然后直接在浏览器输入域名即可访问。 HTTPS支持 要将HTTP转为HTTPS主要有两个步骤,一个是申请证书,一个是安装证书。 SSL证书申请 偶然发现了FreeSSL这个网站,申请证书是真的方便,还有一个支持各大平台的客户端KeyManager,可以直接在里面申请Let’s Encrypt证书或者TrustAsia证书,一般前者半年,后者一年,因为比较懒,所以选后者。 在里面申请证书后,会有两种方法验证,一种是DNS验证,另一种是文件验证。 对于DNS验证,它会给你一串字符,让你到DNS解析设置里添加一个TXT解析,并粘贴为该字串。但是验证结果是香港和美国通过了,大陆总是验证失败,提示CNAME超时。因此选择文件验证了。 要将文件放在网站中进行验证,需要将其拷进Docker中,或者直接Docker容器中拷出来。 1 Docker cp $YourContainer:/var/www/lychee/ www/lychee/ 然后将SSL验证文件放入该文件夹中重新挂载: 1 docker run -it -d -p 80:80 -v /root/images/uploads/:/uploads/ -v www/lychee/:/var/www/lychee/ -v data/:/data/ -v mysql/:/mysql/ kdelfour/lychee-docker 然后就可以验证成功了,之后可以生成证书并下载Nginx证书。 Nginx配置修改 接下来就是容器配置了,将HTTP转换为HTTPS。 SSL证书安装 首先将生成的证书放到网站的某个目录中,一个公钥和一个私钥。然后修改Docker容器的Nginx配置文件,也可以将其从容器中拷贝出来,再作修改: 1 docker cp $YourContainer:/etc/nginx/ nginx/ 然后进入nginx/sites-enabled目录,修改lychee文件。 加入HTTPS的端口以及SSL证书的地址: 1 2 3 4 5 6 7 8 9 listen 443 ssl; server_name localhost; keepalive_timeout 70; ssl on; ssl_certificate /etc/nginx/sslkey/server.pem; ssl_certificate_key /etc/nginx/sslkey/key.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!MD5; HTTP跳转 当使用HTTP访问时,直接跳转到HTTPS,有很多方法,这里使用497的错误码实现跳转: 在Nginx的Server配置中加上 1 error_page497https://$YourSite/ 然后重新挂载容器,此时需要指定HTTPS协议的443端口,以及Nginx配置文件目录: 1 docker run -it -d -p 80:80 -p 443:443 -v nginx/:/etc/nginx/ -v uploads/:/uploads/ -v www/lychee/:/var/www/lychee/ -v data/:/data/ -v mysql/:/mysql/ kdelfour/lychee-docker 此时即可正常访问图床,示例如下:

2019/3/26
articleCard.readMore

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 mv和git 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可以进行单词层面的对比,加上--graph以ASCII图形表示的分支合并历史。如果只想看每次提交的简略信息,可以加上-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主要是在本地修改好了再推送到远程仓库,实际上对远程仓库的操作比较少,就一些基本的推拉行为。 查看远程仓库。 直接使用git remote即可查看当前的远程仓库,加上-v选项可以以详细模式查看。 添加远程仓库。 直接使用git remote add <shortname> <url>,将仓库名和地址添加即可。 从远程仓库抓取数据。 有两种需求,一种是只从远程仓库拉取数据,但并不合并到当前分支,可以使用git fetch <remotename>命令。 另外,使用git clone获取的远程仓库会自动归于origin名下。 另一种,需求是自动抓取并合并到当前分支,可以使用git pull命令。 推送数据到远程仓库。 基本操作,git push <remotename> <branch>。 查看远程仓库信息。 1 git remote show <remote-name> 远程仓库的删除和重命名。 删除远程仓库: 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会显得有些佛性,难以理解,最简单的方式是fetch与merge的组合。 删除分支 删除远程分支可以使用: 1 git push origin --delete test 或者直接将空分支推送到远端覆盖远端分支即可: 1 git push origin :<remotebranch> 变基 这个是个有趣的用法,自从有了变基,Github就变成了Gayhub (逃 )。 啊呸!当然不是这个原因。 变基是一种整合分支的方法,通常整合分支有两种方法:合并和变基。 合并(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分支,找到branch1和branch2的分离点,然后在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会创建一个新的分支,检出储藏工作时的所处的提交,重新应用,如果成功,则丢弃储藏。

2019/3/21
articleCard.readMore

Telegram接管聊天消息

虽有Web微信的简陋功能,虽有electronic的外观封装,但每次登陆实在太麻烦,以及很多功能被限制,表情无法接收等,被鹅厂放弃的Linux用户对微信的体验大概确实不好。 关于Telegram 关于Telegram(电报),直接引用维基上的介绍: Telegram是一个跨平台的即时通信软件,它的客户端是自由及开放源代码软件,但是它的服务器是专有软件。用户可以相互交换加密与自毁消息,发送照片、影片等所有类型文件。官方提供手机版(Android、iOS、Windows Phone)、桌面版(Windows、macOS、Linux)和网页版等多种平台客户端;同时官方开放应用程序接口,因此拥有许多第三方的客户端可供选择,其中多款内置中文。 这个是俄国的社交服务VK的创始者杜洛夫兄弟的作品,Telegram Messenger LLP是独立的非营利公司,与VK也没啥关系,所有的宗旨只在于保证聊天和隐私安全。但不接受监管的软件通常过的不是太好,追求绝对的隐私安全,以至于其在各个国家遭受了被封锁的命运。其他国家暂且不论,因为不肯交出密钥,连俄罗斯媒体监管机构都请求法庭再全国范围内封锁该软件。抛开这些原因不谈,就技术方面,其良好的功能体验,开源的客户端以及开放的应用程序接口,已经领先于绝大多数同类APP了。 Tencent基本是属于放弃Linux用户的一类,后来新注册的微信号连网页版都无法使用,虽然嫌弃通常是相互的,但微信之类的产品用的人太多,粘性太大,有时候不得不用其交流,等网页版每次都要扫一下也是麻烦,所以直接使用Telegram的机器人来收发微信消息。 环境安装 聚合社交平台这方面,EFB做的不错,而且也有了现成的Docker镜像(由royx提供),使得环境搭建更为简单。 另外,需要一台能访问外网的主机,主要是能访问TG(Telegram)服务器。 然后安装Docker: 1 curl -sSL https://get.daocloud.io/docker | sh 安装好之后,就可以拉取镜像了: 1 docker pull royx/docker-efb 配置TG Bot 主要方式是通过登陆网页版微信,然后将微信消息通过Bot发送及接受。首先需要配置TG Bot: 搜索并找到@botfather机器人,然后发送指令:/newbot 给Bot起个名字。 给机器人起用户名,以bot结尾 获取机器人的Token 设置Bot隐私权限: 默认Bot可能无法接收非/开头的消息,所以需要设置隐私权限。向该机器人发送指令/setprivacy,选择刚刚创建的机器人,点Disable即可。 允许将Bot添加进群组: 给机器人发送指令/setjoingroups,选择enable。 允许Bot提供指令列表: 给机器人发送指令/setcommand,输入以下内容: 1 2 3 4 5 6 7 8 help - 显示命令列表. link - 将远程会话绑定到 Telegram 群组 chat - 生成会话头 recog - 回复语音消息以进行识别 info - 显示当前 Telegram 聊天的信息. unlink_all - 将所有远程会话从 Telegram 群组解绑. update_info - 更新群组名称和头像 extra - 获取更多功能 获取TG ID 搜索另外一个机器人@get_id_bot,点击start即可获得TG ID。 配置EFB 新建一个config.py文件保存机器人信息,输入以下内容: 1 2 3 4 5 6 7 8 9 10 11 12 13 master_channel = 'plugins.eh_telegram_master', 'TelegramChannel' slave_channels = [('plugins.eh_wechat_slave', 'WeChatChannel')] eh_telegram_master = { "token": "12345678:QWFPGJLUYarstdheioZXCVBKM", "admins": [13456782], "bing_speech_api": ["xxx", "xxx"], "baidu_speech_api": { "app_id": 0, "api_key": "xxx", "secret_key": "xxx" } } 在其中输入之前所获得的token,以及将admin后的内容换成TG ID。其余xxx的内容是语音识别API,想要的可以申请,没有的也无所谓。 然后新建一个tgdata.db文件,为空即可。 启动EFB容器 指定配置文件和数据文件的地址,启动容器: 1 2 3 4 docker run -d --restart=always --name=ehforwarderbot \ -v $(pwd)/config.py:/opt/ehForwarderBot/config.py \ -v $(pwd)/tgdata.db:/opt/ehForwarderBot/plugins/eh_telegram_master/tgdata.db \ royx/docker-efb 然后通过docker logs查看容器输出内容,应该可以看到一个二维码,用微信扫一扫即可登录。 机器人使用 登陆成功即可正常使用机器人收发微信消息,默认情况下,所有的微信消息以及公众号消息,全都是通过那个机器人发送的,看起来会比较乱。 如果需要单独跟某个人聊天,一种方法是在你创建的机器人中发送/chat 好友名,然后机器人会给一段消息,回复那个消息就可以将消息发送给指定的联系人。但是略显麻烦,聊天不多的人可以这样。 另一种方法是单读创建一个TG群组,然后将名称命名为你要聊天的好友名,将机器人拉进来。然后向你所创建的机器人发送指令/link 好友名,将与该好友的聊天绑定到你所创建的群组中,即可如微信一般发送以及接收消息,且可以发送TG的自定义贴纸表情。 (后加)接管QQ消息 此外,使用EFB工具也可以托管QQ消息,方法挺多,这里依然采用最简单的容器方法。 和接管微信消息一样,需要创建一个机器人获取Token,也可以就用微信机器人,不过为了方便管理,就直接另外创建一个机器人了。 然后直接使用EFB和酷Q的efb-qq-coolq-docker项目中的配置,仓库在这. 然后修改两个配置文件: 1 2 3 4 5 vim ehforward_config/profiles/default/blueset.telegram/config.yaml token: "你的机器人token" admins: - 你的tgid 和 1 2 3 4 vim docker-compose.yml - VNC_PASSWD=你的密码 - COOLQ_ACCOUNT=你的qq账号 执行docker-compose up -d,然后打开ip:9801完成登录操作。 但目前直接登录后,login可以成功,却无法获取到friends,借用blue-bird1的解决方法,修改bot容器中的配置: 1 2 3 docker exec -it efb-qq-coolq-docker_bot_1 /bin/ash vi /usr/local/lib/python3.6/site-packages/efb_qq_slave/Clients/CoolQ/CoolQ.py 将第329行和第512行的调用赋值改为绝对赋值: 1 res = {"good": True, "online": True} 然后重启容器即可。 1 docker restart efb-qq-coolq-docker_bot_1 此时即可正常收发QQ消息。

2019/3/20
articleCard.readMore

Hashcat密码破解

拿到了一个被加密的Excel,要求输入密码才能打开,于是尝试了下密码破解方法。 AROP破解 AROP(ADVANCED OFFICEPASSWORD RECOVERY)好像是比较主流的Office的破解工具,有收费版和免费版,区别在于密码长度是否超过4字节。 用了一个虚拟机跑了一下,字典查询没有,于是暴力破解,嗯,破解速度比较令人绝望,毕竟是CPU在跑。 将该Excel文件解压之后,可以发现里面包含DataSpace、EncryptedPackage以及EncryptionInfo等文件,打开EncryptionInfo文件,可以看到里面加密的一些信息: 1 2 <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <encryption xmlns="http://schemas.microsoft.com/office/2006/encryption" xmlns:p="http://schemas.microsoft.com/office/2006/keyEncryptor/password" xmlns:c="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate"><keyData saltSize="16" blockSize="16" keyBits="256" hashSize="64" cipherAlgorithm="AES" cipherChaining="ChainingModeCBC" hashAlgorithm="SHA512" saltValue="tLnK9YHGccHAgMyj9Nuwmg=="/><dataIntegrity encryptedHmacKey="wF7fTyyf/qhB8Vg+8fvQcspTyuR7cc2+yjIyuEAF0O8avnv0LxYhw+DaVmikzCbFDjLgFCAj6+C6m6iJdhuknA==" encryptedHmacValue="IK7Xc7/e0AVLERogsLvFl912xsbhw+oRKd/ABUwE5vw5iQtcAkM0K0rjz+gB8UlDDJlGbQx0HOKWypF3EDbqcA=="/><keyEncryptors><keyEncryptor uri="http://schemas.microsoft.com/office/2006/keyEncryptor/password"><p:encryptedKey spinCount="100000" saltSize="16" blockSize="16" keyBits="256" hashSize="64" cipherAlgorithm="AES" cipherChaining="ChainingModeCBC" hashAlgorithm="SHA512" saltValue="wisEIaAFG08tJoh3tD0Bqw==" encryptedVerifierHashInput="ZMMqH6NC6xpnsH8zBXtfyA==" encryptedVerifierHashValue="mEcTV0662f/U1+nndKsBiv+L/CsAusbw+So+pA4g8TBKq70rNYy7nkZk+tYB6M/fFZdBfRH4363GRI4m8WPk6Q==" encryptedKeyValue="xUZ5hE+Tzhim5YcUf7KOA5Z1jAG+cTaOGRd859sCkPA="/></keyEncryptor></keyEncryptors></encryption> 其中,采用的是AES算法,有盐(salt,指随机的数据,加入到哈希的过程中,加大破解难度),hash算法是SHA512,spinCount=100000经过了100000次的迭代操作,想直接逆向破解,实在太难。字典尝试无效,只能暴力破解,但是随着密码长度增加,以及字母数字、特殊字符的引入,破解难度指数增长。对于纯小写字母的6位密码,复杂度为$26^6 = 308915776$次,七位则超过了80亿次,指望这个靠CPU计算的软件,实际希望不大,跑了半天后放弃了。 Hashcat破解 Hashcat号称世界上最快的密码破解,世界上第一个和唯一的基于GPGPU规则引擎,免费多GPU(高达128个GPU),多哈希,多操作系统(Linux和Windows本地二进制文件),多平台(OpenCL和CUDA支持),多算法,资源利用率低,基于字典攻击,支持分布式破解等等。 嗯,暴力破解的话,只能考虑使用GPU跑,这时,开源的hashcat就是一个不错的选择。 获取文件hash值 使用hashcat破解office,先需要获取文件的hash值,网上有现成的工具office2join.py,然后用python运行,参数加上该office文件即可。 1 2 3 python office2john.py ../test.xlsx test.xlsx:$office$*2013*100000*256*16*c22b0421a0051b4f2d268877b43d01ab*64c32a1fa342eb1a67b07f33057b5fc8*984713574ebad9ffd4d7e9e774ab018aff8bfc2b00bac6f0f92a3ea40e20f130 可以看到,加密方式为office2013,将第一个冒号后面的字串复制到一个新文件中保存即可。 hashcat破解 知道加密方式后,需要找到对应的破解模式,首先使用--help看一下帮助: 1 ./hashcat64.bin --help 可以看到office的加密模式有以下几种: 1 2 3 4 5 6 7 8 9 9700 | MS Office <= 2003 $0/$1, MD5 + RC4 | Documents 9710 | MS Office <= 2003 $0/$1, MD5 + RC4, collider #1 | Documents 9720 | MS Office <= 2003 $0/$1, MD5 + RC4, collider #2 | Documents 9800 | MS Office <= 2003 $3/$4, SHA1 + RC4 | Documents 9810 | MS Office <= 2003 $3, SHA1 + RC4, collider #1 | Documents 9820 | MS Office <= 2003 $3, SHA1 + RC4, collider #2 | Documents 9400 | MS Office 2007 | Documents 9500 | MS Office 2010 | Documents 9600 | MS Office 2013 | Documents 所以,office2013选择-m 9600,然后开始破解: 1 ./hashcat64.bin -a 3 -m 9600 --session test -o found.txt hash.txt ?l?l?l?l?l?l?l 其中,-a表示破解模式,3是暴力破解,--session test是将该次破解进程命名,可有可无,但之后如果要中断再恢复,则可以使用进程名恢复。-o found.txt是将找到的结果输出到指定文件中,hash.txt是之前保存hash码的文件,最后是一串正则表达式,有?l?u?d?s,分别表示小写字母、大写字母、数字、和特殊字符。该例子表示暴力破解七位小写字母的密码。 但由于不确定位数,先从简单的开始,破解从1到8位的小写字母: 1 ./hashcat64.bin -a 3 -m 9600 -o found.txt hash.txt --increment --increment-min 1 --increment-max 8 ?l?l?l?l?l?l?l?l 其中,--increment-min 1 --increment-max 8即如本身含义。 然后查看一下显卡运行情况: 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 Tue Mar 12 14:57:13 2019 +-----------------------------------------------------------------------------+ | NVIDIA-SMI 410.78 Driver Version: 410.78 CUDA Version: 10.0 | |-------------------------------+----------------------+----------------------+ | GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. | |===============================+======================+======================| | 0 GeForce RTX 208... Off | 00000000:18:00.0 Off | N/A | | 90% 80C P2 247W / 250W | 2341MiB / 10989MiB | 94% Default | +-------------------------------+----------------------+----------------------+ | 1 GeForce RTX 208... Off | 00000000:3B:00.0 Off | N/A | | 90% 80C P2 248W / 250W | 2341MiB / 10989MiB | 95% Default | +-------------------------------+----------------------+----------------------+ | 2 GeForce RTX 208... Off | 00000000:86:00.0 Off | N/A | | 91% 81C P2 247W / 250W | 2341MiB / 10989MiB | 95% Default | +-------------------------------+----------------------+----------------------+ +-----------------------------------------------------------------------------+ | Processes: GPU Memory | | GPU PID Type Process name Usage | |=============================================================================| | 0 1852 C ./hashcat64.bin 2331MiB | | 1 1852 C ./hashcat64.bin 2331MiB | | 2 1852 C ./hashcat64.bin 2331MiB | +-----------------------------------------------------------------------------+ 如果中途有事,可以先暂停一下,然后跑完后再过来继续破解: 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 ./hashcat64.bin --restore Session..........: test Status...........: Cracked Hash.Type........: MS Office 2013 Hash.Target......: $office$*2013*100000*256*16*c22b0421a0051b4f2d26887...20f130 Time.Started.....: Wed Mar 13 14:28:19 2019 (9 hours, 37 mins) Time.Estimated...: Thu Mar 14 00:05:48 2019 (0 secs) Guess.Mask.......: ?l?l?l?l?l?l?l [7] Guess.Queue......: 1/1 (100.00%) Speed.#1.........: 19777 H/s (8.49ms) @ Accel:64 Loops:16 Thr:256 Vec:1 Speed.#2.........: 19260 H/s (8.66ms) @ Accel:64 Loops:16 Thr:256 Vec:1 Speed.#3.........: 19438 H/s (8.60ms) @ Accel:64 Loops:16 Thr:256 Vec:1 Speed.#*.........: 58475 H/s Recovered........: 1/1 (100.00%) Digests, 1/1 (100.00%) Salts Progress.........: 2025455616/8031810176 (25.22%) Rejected.........: 0/2025455616 (0.00%) Restore.Point....: 74645504/308915776 (24.16%) Restore.Sub.#1...: Salt:0 Amplifier:17-18 Iteration:7520-7536 Restore.Sub.#2...: Salt:0 Amplifier:0-1 Iteration:99984-100000 Restore.Sub.#3...: Salt:0 Amplifier:6-7 Iteration:53776-53792 Candidates.#1....: efmfogr -> ehdaznt Candidates.#2....: svtdyyl -> srhqbks Candidates.#3....: lnkkjnt -> ljnzibl Hardware.Mon.#1..: Temp: 77c Fan: 87% Util: 94% Core:1635MHz Mem:6800MHz Bus:16 Hardware.Mon.#2..: Temp: 77c Fan: 87% Util: 93% Core:1695MHz Mem:6800MHz Bus:16 Hardware.Mon.#3..: Temp: 77c Fan: 87% Util: 94% Core:1725MHz Mem:6800MHz Bus:16 Started: Wed Mar 13 14:27:42 2019 Stopped: Thu Mar 14 00:05:50 2019 最后大概半天吧,跑出来结果如下: 1 $office$*2013*100000*256*16*c22b0421a0051b4f2d268877b43d01ab*64c32a1fa342eb1a67b07f33057b5fc8*984713574ebad9ffd4d7e9e774ab018aff8bfc2b00bac6f0f92a3ea40e20f130:semicjj 冒号后面的就是密码。

2019/3/14
articleCard.readMore

Docker博客环境封装及自动化部署

说来惭愧,也不记得有几次立flag要把博客坚持下去的,看看上一篇的时间,一拖又是这么久了。 为了不至于彻底沦落成上班摸鱼,下班看剧的MADAO(并非在说长谷川先生),还是想去舒适区外面逛逛。自动化部署并不是什么难事,记得以前网上就可以找到一堆TravisCI的教程。不过记得去年暑假时候使用Docker封装了博客环境,以便能在新系统上使用(Ubuntu 16.04 => 18.04),同时也是为了防止博客插件以及npm的更新引起问题。 容器内构建环境 关于Docker容器的储存结构以及基本介绍,之前貌似有一篇文章已经说了一些了,这里不再赘述。 通过镜像构建容器很简单,docker run imagename即可,由于是博客,可以把本地的blog目录挂在进去,并映射里面的端口,即加上-v /Blog:/Blog 和-p 4000:4000,其他设置自己怎么喜欢怎么来。 个人是直接从Docker Hub官方仓库中的ubuntu:16.04镜像来启动容器的,可以使用docker pull,也可以使用docker run命令来启动容器。 然后是安装一些必要的软件: 1 2 3 4 5 6 7 8 apt update && apt install -y vim git python wget gcc g++ wget -qO- https://raw.github.com/creationix/nvm/v0.33.11/install.sh | sh git config --global user.email "ABCDEFG@qq.com" && \ git config --global user.name "username" && \ source ~/.profile nvm install v9.2.1 && \ npm install hexo-cli -g && \ npm install gulp -g 首先安装必要的环境,然后安装npm的包管理工具nvm,然后配置git账号,并安装特定版本的node,在安装之前先确认之前可以运行的时候的node版本即可。剩下的就是安装hexo和gulp(博客资源压缩工具,优化用)。 为了hexo能够直接deploy,配置免密登录密钥并添加到github中。 1 ssh-keygen -t rsa -P "" 拷贝/root/.ssh/id_rsa.pub文件中内容到github的SSH-KEY中即可。 然后删掉博客中的node_modules/文件夹和db.json文件,重新安装。 1 npm install 之后便可以正常在本地访问博客了。 构建镜像 容器直接构建 在之前环境配置好之后,退出容器,将该容器打包成镜像。 1 docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]] 可加参数有-a authorname 添加作者信息,-m message添加说明文字,如: 1 docker commit -a "Lilei && Hanmeimei" -m "blog image" CONTAINER_ID REPOSITORY:TAG 后面添加容器ID或者容器名都可以,然后添加你要上传到DockerHub的仓库名以及版本标签(TAG如果为空,默认为latest) 通过Dockerfile构建 将之前的指令写入Dockerfile文件,然后建立镜像即可。 1 2 3 4 5 6 7 8 9 10 11 12 FROM ubuntu:16.04 MAINTAINER gitever RUN apt update && apt install -y vim git python wget gcc g++ make && \ wget -qO- https://raw.github.com/creationix/nvm/v0.33.11/install.sh | sh ENV NVM_DIR /root/.nvm COPY ssh /root/.ssh RUN git config --global user.email "ABCDEFG@qq.com" && \ git config --global user.name "username" && \ . $NVM_DIR/nvm.sh && \ nvm install v9.2.1 && \ npm install hexo-cli -g && \ npm install gulp -g 其中MAINTAINER是作者名字,FROM是使用的镜像来源,然后安装环境,和之前一样。最后使用Docker build命令构建镜像。 需要说一点的是,通过Dockerfile构建的镜像不能直接使用ssh-keygen命令生成免密密钥,因为每次构建镜像时都会执行一次生成指令,如果之后版本需要修改,Dockerfile中需要加入其他指令,那么原来可以免密的镜像,生成后会变的无法登陆。最明显的就是使用ssh的方式的hexo deploy和github 仓库的访问,故而将已经可以免密的.ssh/文件夹直接拷贝进来。 1 docker build -t gitever/blog:ci . 使用-t指令是指定之后要上传到Docker Hub的镜像仓库名。 然后等待一会,会显示构建完成,使用docker images便可以查看之前直接在容器中构建的镜像和使用Dockerfile构建的镜像。通常使用Dockerfile构建镜像体积会更小,因为Docker的分层存储方式,由于在容器内通常会做很多多余的无用指令,所以直接commit构建的体积很容易变得臃肿。 之后就是上传镜像: 1 docker push gitever/blog:ci Docker自动化部署 其实在镜像制作完成后,即可以使用镜像启动容器,然后使用博客环境了。上传镜像之后,可以在不同电脑上使用该博客环境,也不会有环境冲突的问题。但是每次都要挂载本地目录到容器中(因为博客目录体积较大,直接放入容器中体积太大,而且博客会更新,容器不能保存,只能重新制作镜像,使得效率低下)。也许是觉得在不同电脑上都要下载镜像启动容器显得麻烦,或者觉得每次都要手动generate、push和deploy显得麻烦,便开始打算使用Docker自动化部署。 首先去DockerHub创建一个仓库用来自动化部署,仓库创建需要绑定github账号,然后将博客的源文件仓库链接至该镜像仓库,如下图所示。 不过也不一定要链接至博客源文件仓库,可以新建一个仓库,将Dockerfile上传至该仓库,每次push该仓库触发博客自动更新也可以。直接使用博客源文件仓库则是每次写好文章push上去便直接触发更新了。 然后在本地的博客源文件(或新仓库)添加一个Dockerfile记录要自动更新的指令。 1 2 3 4 5 6 7 8 9 10 FROM gitever/blog:ci MAINTAINER gitever ADD .git/ /.git/ RUN git clone YourRepoUrl /Blog &&\ . $NVM_DIR/nvm.sh && \ cd /Blog && \ npm install && \ hexo cle && \ gulp && \ hexo d 然后git add .以及使用commit和push上传至git仓库即可触发。 其中,ADD一个变化的值,保证之后的构建不使用缓存,不然即使仓库更新了,容器里的仓库也不会更新。 其实不用每次git clone,git的优点就是差异性存储,所以可以之前依然可以使用缓存,节省时间,将后续操作设置成不使用缓存。 1 2 3 4 5 6 7 8 9 10 11 12 FROM gitever/blog:ci MAINTAINER gitever RUN git clone YourRepoUrl /Blog &&\ . $NVM_DIR/nvm.sh && \ npm install && \ ADD .git/ /.git/ RUN cd /Blog && \ . $NVM_DIR/nvm.sh && \ git pull && \ hexo cle && \ gulp && \ hexo d 另一种方式,直接将当前仓库中的文件添加到容器中: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 FROM gitever/blog:ci MAINTAINER gitever RUN mkdir /Blog COPY _config.yml /Blog COPY package* /Blog/ COPY gulpfile.js /Blog COPY scaffolds/ /Blog/scaffolds RUN cd /Blog && \ . $NVM_DIR/nvm.sh && \ npm install ADD themes/ /Blog/themes ADD source/ /Blog/source ADD .git/ /Blog/.git/ RUN cd /Blog && \ . $NVM_DIR/nvm.sh && \ git pull && \ hexo cle && \ gulp && \ hexo d 这种方式,每次修改文件后都将整个文件夹添加到容器中,文件夹比较大的话会花比较多的时间。另一方面,因为Docker层层有缓存,所以第一种方式也只有第一次较慢。 最后观察触发是否成功以及最后输出结果是否在预期内。 上述涉及到ADD和COPY区别,COPY只是简单的复制,ADD支持下载URL,并支持解压,并具有判断其ADD的src功能。因此,在没有特殊需求时,尽量使用COPY提高效率,上述使用ADD .git/是为了让Docker Daemon判断git仓库是否更新了,如果更新了,则不使用缓存,这样,后续的git pull才能真正获取更新并最终更新到网站上。 此外,还可以添加.dockerignore文件来忽略一些容器中你不用的文件以提高速度。容器加载时会默认将当前目录下所有文件打包传给Docker Daemon,比如就是node_modules文件夹。 没有写的太细,长篇累牍容易给看客压力,step by step操作一番,不填坑也不会有印象,也并不会达到对容器熟悉的效果。 因为容器的方便性,我曾不断向人安利,但也许是推荐的对象不适合,用的人貌似不多,记得以前也答应过人把Docker封装环境的详细过程写写,现在算是应诺了,希望还能帮人节省一点时间。

2019/3/5
articleCard.readMore

小聊乐理

很久啦,不止一个人说我写的太无聊,确实我又不是什么有趣的主儿。孤芳自赏,历来惯了。不过生活多点味道也挺好,技术博客里也不需要太多技术,毕竟技术应该寄生在人身上,而不是博客里。 ——今天偶尔尝试一下别的内容了。 不知道有没有人对音乐感兴趣,然而不为盈利,也非是为了悦人而作,也只是突然想写,写便罢。 节拍 拍子是音乐里最基础的节奏单位,经常听到的电音里面的那些”咚-咚-咚–”就是一拍一拍的。一定数量的拍可以构成节,如传统的 $\frac{4}{4}$ 下面的4表示4分音符为一拍,上面的4表示一小节拍4下。 另外,拍子的速度则是每分钟拍多少下,即beats per minute(bpm)。时值则是音符持续时间长短,比如2分拍的时值就是4分拍的2倍。 琴键 钢琴一共88键,其实结构很简单,如果从数学的角度考虑,其实也都很容易记住。我画了一下: 恩,画得好累~虽然花了点时间,但实际上手绘就比较快了,钢琴一共88个键,左音低,右音高。其中,最右边的是小字5组C,即($C^5$),最左边的则是大字2组A,即($A^2$)。中间从小字组往左分别是大字组和大字一组,往右是小字一组到小字五组。每组为CDEFGAB七个白健,键盘中相邻两个键相差为半音,如B和C之间相差即为半音,而C和D之间,由于中间存在一个黑键,因此相差为一个全音。 黑键没有独立的名字,分别根据相邻的白健命名,如C和D之间的黑键称为C#或者D♭,其中sharp(#)表示升调,♭符号表示降调。 中央C 关于中央C,即最中间组的C,图中从大字二组到小字五组一共有九个组,中央组就是第五组了,也就是小字一组C,有的也称之为$C^5$。 音程 音程,即两个音之间的高低关系,可以理解为两个音之间的距离。与两个音之间的半音数有关。 半音数和音程之间的关系,如下表所示: 半音数0123456789101112 音程纯一度小二度大二度小三度大三度纯四度增四/减五度纯五度小六度大六度小七度大七度纯八度 嗯,可以简要从数学方面分析一下。 为此,我再画两个组,从C到A,重复两次: 虽然说规律一致,我还是一个一个分析吧: 图中最远的距离是从C回到C,即CDEFGABC,看做数列的话,项数为8,因此最高度数是8。由于钢琴这8度一组,七个白键五个黑键,不断循环。不管怎样,8度必然是这么多白健和黑键,半音数就是$7+5=12$。之所以叫纯八度,是为了和大小增减音程区分开来。非纯音程的原因通常是黑键位置导致的实际值不等于整度数。 同时按两次相同位置的C,他们之间的半音数为0,项数为1,只有一度,也不存在由于黑键导致的大于或者等于1的度数,因此是纯一度。 再看从C到D,度数为2,但是中间有一个黑键,因此半音数相差为1+1=2,此时称之为大二度。物理上,两个白键之间最多有一个黑键。因此半音数相差为1或者2,为了区分这两种情况,同样是2度音程,有黑键情况,半音数较多称之为大二度,无黑键情况称之为小二度。 度数为3的情况,如从C到E,中间有两个黑键,半音数为$2+2=4$,而物理上,度数为3的情况也有两种,一种是中间有一个黑键,一种是中间有两个黑键,这就导致了实际半音数会相差1。同样的方法,以大小度数来记,三个白键之间最多有两个黑键,此时称之为大三度;最少有一个黑键,此时称之为小三度。 度数为4的情况,如从C到F,中间有两个黑键,此时半音数为3+2=5。其实通常情况下四个白键之间都有两个黑键。只有一种情况,就是从F到B,中间有三个黑键,此时半音数为6。因此,当半音数为5,即通常情况下,半音数为5,度数为4的称之为纯四度。半音数为6,度数为4的称之为增四度。 度数为5的情况,与度数为4的情况类似。如从C到G,中间通常都有三个黑键,半音数为4+3=7。称之为纯五度。只有一种情况,即,从D到G,中间只有两个黑键,半音数为6,但由于度数为6,所以此时称之为减五度。 度数为6的情况,与度数为3的情况类似。如从C到A,中间有4个黑键,半音数为5+4=9。而度数为6的情况,白键之间的黑键数量也有两种,如从E到C,中间有三个黑键,半音数为8。同样的记法,半音数8,度数为6的称之为小六度;半音数9,度数为6的称之为大六度。 最后看度数为7的情况,如从C到A。也是类似,七个白键之间的黑键可能为4或者5,因此半音数可能为10或者11。半音数为10,度数为7的称之为小七度;半音数为11,度数为7的称之为大七度。 表中,增四度和减五度之间的距离都是6个半音,即三个全音,因此增四度减五度都成为三全音。 音程转位 音程转位就是从一个组的调转到下一个组的该调。如CDE转位后为EFGABC。从数列方面看,也有一些规律。 因为七个白键一组,所以从一个音会到下个组的本音,之间项数为8,距离为7。但转位之后,如从C-E与E-C,中E出现两次,所以最后一共有9个度数。故转位之后度数之和为9。而这一组白键中,黑键数量永远为5,因此转位之后,音程互补。对于大音程,转为之后成为小音程。增音程,转位之后成为减音程。 协和音程 主观感受吧,大致是觉得听起来比较协调,就觉得是协和音程。 主要分为4类: 完全协和音程纯一度、纯八度 协和音程纯四度、纯五度 不完全协和音程大/小三度、大/小六度 不协和音程大/小二度、大/小七度、增四度、减五度 调 音调,记得初中物理里解释为声音频率的高低,其实大致也是这种感觉吧。直观的感觉是高音轻短细,低音中长粗。 图中琴键每七个音阶一组,即CDEFGAB,调有大小之分。 大调 以一个C大调为例,如表所示: C大调CDEFGABC 距离全音全音半音全音全音全音全音半音 选取任意一组琴键,图中可以看到,一个大调包含了七个白健,五个黑键,除了中间的EF两个白健之间没有黑键,其余的从C开始到B,中间都有黑键。因此除了EF之间是相差半音,其余都是相差全音。另外,由于一组七个循环都是从白健开始,首位白健之间也是没有黑键的,因此如果从C回到C,最后B和C之间也是相差半音。大调特征即是如此,即三和七为半音,其余为全音,所谓的“全全半全全全半”。 以此方式,则D大调为: D大调DEF#GABC#D 距离全音全音半音全音全音全音全音半音 表中F和C处为了保持半音和全音的距离,因此写作了F#和C#。虽然F#和G♭是一样的,但在这里,为了保持相邻两音度数为2,因此只能写作F#。如果写作G♭,则G♭和G之间只有一度音,少了F调,无法成为全音。C#的原因也是一样。(主要是写法不同) 另外,有个关于升号调和降号调的一个表格: 升号调GDAEBF#C# 降号调FB♭E♭A♭D♭G♭C♭ 以此方式,可以看一下F大调: F大调FGAB♭CDEF 距离全音全音半音全音全音全音全音半音 以上述方式理解或者直接查升号调降号调表格,中可以看到,F属于降号调表示。 稳定音 各音之间,稳定性是有差异的,最稳定的音为主音,如C大调主音为C,各音的稳定性如下: 最稳定1536247最不稳定 稳定音稳定音稳定音不稳定音不稳定音不稳定音不稳定音 而各音会有倾向性,即不稳定的音听起来会倾向于进行到稳定音上。其中2级倾向于进行到1级,4级倾向于进行到3级,6级倾向于进行到5级,7级倾向于进行到1级。 和弦 和弦是一些音的结合,先看几个基本的三度叠置和弦: 大三和弦: 如从C-E-G,C和E之间存在两个黑键,三度白键之间最多也就两个黑键,因此CE之间是大三度。E和G之间有一个黑键,故EG是小三度。这种大三度+小三度的和弦称之为大三和弦,称之为$Cmaj$,简写为C。其中,C称之为根音,E和G相对于根音分别是3和5度(算上根音本身),因此称之为三音和五音。 小三和弦: 与大三和弦相对。如从D-F-A,D和F之间是小三度,F和A之间是大三度,DFA则被成为小三和弦。记作$Cminor$,简写为Dm。即小三度+大三度。 减三和弦: 如从B-D-F,是小三度+小三度,称之为减三和弦,记作Bdim,简写为$B^。$。 从琴键结构可以看出,只算白键的话,两个大三度不可能连续在一起,即大三度+大三度,通常采用黑键表示,即所谓的增三和弦(增三和弦属于极不协和的和弦)。 大七和弦(大大七和弦): 如从C-E-G-B,即大三度+小三度+大三度,此时称之为大七和弦,或称为大大七和弦。简写为CM7。 属七和弦:如GBDF,大三和弦+小七度,即大三度+小三度+小三度。写作Gdom7,简写为G7。 小大七和弦:如EGBD#,小三和弦+大七度,即小三度+大三度+大三度。记作${Emin}^{Maj7}$,间协作${Em}^{M7}$。 和弦转位 将三音或者五音、七音放在最下面时(即作为低音),构成和弦转位。 如CEG和弦: 和弦转位CEGEGCGCE 位置原位第一转位第二转位 记法CC/EC/G 同样的,如大七和弦: 和弦转位CEGBEGBCGBCEBCGE 位置原位第一转位第二转位第三转位 记法CM7CM7/ECM7/GCM7/B 五度循环圈 看这名字,可以大致想一下,一组键包括7个白键,5个黑键,加起来是12个键,如果要五度循环,那么最小公倍数为60,因此应该是12组,从C开始算,最后才能回到C,完成一个循环。 纯五度协和程度仅次于纯一度和纯八度。五度循环圈如图所示: 图中字母可以看做和弦根音,也可以看作单个的音,也可以看作调式主音。如从C大调的所有纯五度音程为:C-G,G-D,D-A,A-E,E-B,F-C。与五度循环圈中位置一致。 外圈和内圈作等音转换,如C#等于D♭,圈里有三个半音。通过五度循环圈,可以很容易的写出和弦音。 调式音级 调式七个音级使用罗马数字表示,每个调式和音级关系如下表: CDEFGABC IIIIIIIVVVIVIII 主上主中下属属下中导主 主要可以由琴键理解。C上方是D,因此D称之为上主音。C下方是B,称为导音。然后主音上方纯五度,即G处是属音。C下方纯五度为下属音,即F处。主音和属音中间的音称为中音,即E处。主音和下属音中间的称为下中音,即A处。 小调 自然小调 通常大调与小调区别,再直观感受上是,大调比较欢快、明朗,小调忧郁、悲伤。再音阶结构上区别如下: C大调CDEFGABC c小调CDE♭FGA♭B♭C IIIIIIIVVVIVIII 全半全全半全全 如表中所示,大调和小调的区别在III级音和VI、VII级处,其中最主要的区别在于三级音,也成为调式特性音。主音相同,三级音的特性是使小调悲伤的主要原因。 此外,音阶结构相对于大调的“全全半全全全半”,为“全半全全半全全”。 关系大小调 比较一下C大调和a小调: C大调CDEFGAB a小调ABCDEFG a小调再琴键上全是白健,构成音和C大调一样,只是主音不同。此时这两者称之为关系大小调:a小调是C大调的关系小调,C大调是a小调的关系大调。 通常构成音相同的大小调称为关系大小调,大调六级音为小调主音,小调三级音为大调主音。还是用数学方法证明一下: 小调音阶全半全全半全全 大调音阶全全半全全全半 将大小调音阶对比,可以看出,在平移两度之后,小调音阶与大调音阶一致。从表中可以得到,大调的关系小调在其下方小三度处(相差2度)。 小调调号使用关系大调调号。画在五度循环圈里如下: 自然音程和变化音程 自然音程:大调(或者自然小调)中,任何两个音构成的音程都属于自然音程。 之前所分析的音程均为自然音程,共四大类,14种。 自然音程 纯音程纯一度纯四度纯五度纯八度 大音程大二度大三度大六度大七度 小音程小二度小三度小六度小七度 三全音增四度减五度 除了自然音程之外的,都属于变化音程,也有四类: 增音程(除增四度) 减音程(除减五度) 倍增音程 倍减音程 具体关系可由图表示: 图中可以看出,纯音程和大音程,在度数不变情况下,增加一个半音可得到增音程,再增加一个半音,可以得到倍增音程。如下表所示: F-G大二度 F-G#增二度 F-Gx倍增二度 减音程和倍减音程类似:对于纯音程和小音程,在度数不变情况下,减少一个半音可以得到减音程,在减少一个半音可以得到倍减音程。 G#-A小二度 G#-A♭减二度 G#-A♭♭倍减二度 纯音程可以变为增音程、倍增音程和减音程、倍减音程。但由于纯一度的半音数为0,所以纯一度没有减音程和倍减音程。 小调变体 小调与大调区别主要在于音阶,自然小调变体有和声小调和旋律小调。 IIIIIIIVVVIVIII a自然小调ABCDEFGA a和声小调ABCDEFG#A a旋律小调ABCDEF#G#A 大调与小调之间的关系: IIIIIIIVVVIVIII C大调CDEFGABC c自然小调CDE♭FGA♭B♭C c和声小调CDE♭FGA♭BC c旋律小调CDE♭FGABC 从表中可以看出,大调与同主音的主要区别在三级音,将大调三级音降半音,可以得到旋律小调,再将六级音降半音可以得到和声小调,再将七级音降半音就可以得到自然小调了。

2018/11/17
articleCard.readMore

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-header和01-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 其中,path和source的地址可以自己定义。 到这里,基本结束。

2018/10/12
articleCard.readMore

Time Machine

Docker是个好东西,或者说容器是个好东西。 毕竟回忆里的昨天,我再也回不去。 但容器可以。 Docker存储方式 还是先说一下Docker容器的储存结构,容器镜像采用的是分层存储的方式,下面是一个Ubuntu16.04的镜像结构。 也可以在命令行观察,在拉取的时候也可以看到结果: 1 2 3 4 5 6 7 8 9 $ ▶ docker pull ubuntu:16.04 16.04: Pulling from library/ubuntu 3b37166ec614: Already exists 504facff238f: Already exists ebbcacd28e10: Already exists c7fb3351ecad: Already exists 2e3debadcbf7: Already exists Digest: sha256:45ddfa61744947b0b8f7f20b8de70cbcdd441a6a0532f791fd4c09f5e491a8eb Status: Downloaded newer image for ubuntu:16.04 其中,每层表示的是与上一层的差异,而不是直接操作底层镜像,这样,当使用其他基于此镜像制作的镜像时,就不必整个拉取或者复制过来,因为它们很多的底层镜像是一样的。Docker镜像采用的是共享存储方式,当拉取一个镜像时,会首先获取所有层的信息,如果该镜像层本地已经有了,就不用下载,只需要下载所需要的镜像层。 在使用镜像建立容器时候,会在最上面一层镜像上建立一个可写层,即容器层。当在容器中所有的操作都会被保存在这个可写层,如果直接删除容器,则可写层就会被删除,即使利用相同镜像重新建立容器,之前的所有操作也不会被保存。镜像层都是只读的,基于此安全性,所有的容器都可以访问底层镜像,所以一次可以利用同一镜像建立多个容器。最后完成修改封装成新的容器的时候,也只是在原来的镜像层之上又加了一层而已。镜像的这种共享存储方式可以极大地提高资源利用效率,而差异存储也是文件管理的主流之选。 说到这个,想起来目前有个PWD(Play with Docker)的网站,可以直接在里面体验docker,地址在这。进去就可以创建一个Docker playground。 利用Docker运行CUDA和TensorFlow 在电脑上配置CUDA或者TensorFlow啥的,经常因为各种版本不同导致一大堆问题,于是就想看看可不可以利用Docker去解决这个问题,每次直接打开封装好的镜像就行了,让Docker里的环境去使用GPU,不用去配环境,也不用在电脑上装啥别的软件。然后发现NVIDIA也在Docker上稍微封装了一下,弄了个Nvidia-docker命令,基本命令与docker命令一样,唯一的区别是普通的Docker无法使用GPU,所以Nvidia-docker等效于命令docker --runtime=nvidia。 要使用GPU,首先也要安装好显卡驱动,怎么安装这里不做赘述,通常安装成功是可以看到的。 当然,Docker肯定要先装好。 Nvidia Docker 然后就是安装Nvidia封装的Docker来调用GPU了,具体可以参照NVIDIA/nvidia-docker页面。 如果之前有安装1.0版本的Nvidia-docker的,需要先卸载: 1 2 3 # If you have nvidia-docker 1.0 installed: we need to remove it and all existing GPU containers docker volume ls -q -f driver=nvidia-docker | xargs -r -I{} -n1 docker ps -q -a -f volume={} | xargs -r docker rm -f sudo apt-get purge -y nvidia-docker 没有的话可以直接略过。然后添加仓库地址重定向到镜像源文件中,再更新软件源。 1 2 3 4 5 6 7 # Add the package repositories curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | \ sudo apt-key add - distribution=$(. /etc/os-release;echo $ID$VERSION_ID) curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | \ sudo tee /etc/apt/sources.list.d/nvidia-docker.list sudo apt-get update 然后直接安装即可。 1 2 3 # Install nvidia-docker2 and reload the Docker daemon configuration sudo apt-get install -y nvidia-docker2 sudo pkill -SIGHUP dockerd CUDA测试 首先运行一个cuda的镜像,进入bash中, 1 2 3 docker run --runtime=nvidia -it --name cuda --rm nvidia/cuda:9.0-base /bin/bash root@c1d523d61051:/# root@c1d523d61051:/# 这里将容器命名为cuda方便操作,需要选择runtime为nvidia,或者直接使用nvidia-docker命令。然后输入nvidia-smi就可以看到是否成功调用显卡了。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 root@c1d523d61051:/# nvidia-smi Thu Oct 11 07:51:19 2018 +-----------------------------------------------------------------------------+ | NVIDIA-SMI 384.130 Driver Version: 384.130 | |-------------------------------+----------------------+----------------------+ | GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. | |===============================+======================+======================| | 0 GeForce GTX 1080 Off | 00000000:01:00.0 On | N/A | | 0% 44C P8 14W / 200W | 867MiB / 8110MiB | 0% Default | +-------------------------------+----------------------+----------------------+ +-----------------------------------------------------------------------------+ | Processes: GPU Memory | | GPU PID Type Process name Usage | |=============================================================================| +-----------------------------------------------------------------------------+ TensorFlow测试 刚刚也说了,如果要使用GPU,需要在docker命令中加上–runtime=nvidia或者直接使用nvidia-docker命令。这里就直接使用nvidia-docker命令了, 1 2 $ ▶ nvidia-docker run -it -d --name tensor -p 8888:8888 tensorflow/tensorflow 9c7db93b36788acf61a20f52cb187f32e0d6018f7e8da031a30fa135252a4896 查看容器内信息, 1 2 3 4 5 6 7 8 9 10 11 docker logs tensor [I 08:01:55.452 NotebookApp] Writing notebook server cookie secret to /root/.local/share/jupyter/runtime/notebook_cookie_secret [I 08:01:55.465 NotebookApp] Serving notebooks from local directory: /notebooks [I 08:01:55.465 NotebookApp] The Jupyter Notebook is running at: [I 08:01:55.465 NotebookApp] http://(9c7db93b3678 or 127.0.0.1):8888/?token=64e73aaba8febd5539fae22201c7b7cea1b8578cc1413850 [I 08:01:55.465 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation). [C 08:01:55.466 NotebookApp] Copy/paste this URL into your browser when you connect for the first time, to login with a token: http://(9c7db93b3678 or 127.0.0.1):8888/?token=64e73aaba8febd5539fae22201c7b7cea1b8578cc1413850 打开浏览器窗口,输入localhost:8888/?token=64e73aaba8febd5539fae22201c7b7cea1b8578cc1413850,可以看到一个Jupyter的界面。 在里面可以编辑及运行python程序,或者使用终端操作: 1 2 3 4 5 6 7 8 9 10 $ ▶ docker exec -it tensor /bin/bash root@9c7db93b3678:/notebooks# ls 1_hello_tensorflow.ipynb 2_getting_started.ipynb 3_mnist_from_scratch.ipynb BUILD LICENSE root@9c7db93b3678:/notebooks# python python python2 python2.7 python3 python3.5m python-config python2-config python2.7-config python3.5 python3m root@9c7db93b3678:/notebooks# python Python 2.7.12 (default, Dec 4 2017, 14:50:18) [GCC 5.4.0 20160609] on linux2 Type "help", "copyright", "credits" or "license" for more information. 关于Docker的Runtime 一个容器运行需要制定规范、Runtime、管理和定义工具、镜像仓库、运行OS等环节。容器的Runtime是容器运行时的一些规范,主要任务是和操作系统的kernel协作来提供容器的运行环境,由OCI(Open Container Initiative,由Google,Docker、CoreOS、IBM、微软、红帽等于2015年联合发起的组织)维护。主要包括容器的文件系统包(Filesystem Bundle),容器的运行和生存周期Runtime and Lifecycle),容器配置文件(Container Configuration file),以及Linux的运行和配置文件(Linux Runtime, Linux Container Configuration)等。目前Linux上最原始的容器Runtime是LXC,即Linux Container,最初Docker也是用LXC作为Runtime,后来Docker基于libcontainer开发了自己的Runtime,即runC。谷歌也基于Docker的Runtime发布了Kubernetes,后来CoreOS开发了独立的rkt作为运行容器的Runtime。 而与容器相对的就是虚拟机了,目前虚拟机的Runtime如runV,看名字就知道是要与runC分庭抗礼的。此外Intel也弄了一个Clear containers的Runtime,也可以对接容器。基于Hyper runV和Clear containers,Openstack又新起了一个Kata Containers,目前已经可以在snap商店看到了,才出来没多久,地址在这。 利用Docker搭建私有云盘 安装 这里使用的是一个开源的云存储方案OwnCloud来搭建私有云盘。 首先可以搜一下Dockerhub中的镜像,docker search owncloud可以看到结果: 其中第一个就是官方的镜像了,直接docker pull owncloud:8.1拉取就行。或者也可以直接docker run,本地没有它会去Dockerhub下载。 1 docker run -d -p 80:80 owncloud:8.1 其中,-d表示后台运行,-p用来映射端口。也可以直接用-it前台打开tty直接操作。 这时候可以在浏览器中看到了,输入localhost就可以看到登陆界面,大致是下面的样子: 运行配置 其中数据保存在/var/www/html/data目录中,默认是使用SQLite用于数据存储,但对于较大的或者使用桌面客户端同步文件时,并不推荐SQLite,可以考虑最流行的MySQL。其他数据库需要外部安装。 在运行时可以使用-v选项来将本地磁盘挂载到容器中数据保存的位置,即/var/www/html/中。 1 -v /<mydatalocation>:/var/www/html 分的更细一点,可以添加三项,设置命令: 1 2 3 -v /<mydatalocation>/apps:/var/www/html/apps installed / modified apps -v /<mydatalocation>/config:/var/www/html/config local configuration -v /<mydatalocation>/data:/var/www/html/data the actual data of your ownCloud 数据库配置 外部数据库配置有几种方法,第一个是使用Owncloud自己提供的OCC工具(OwnCloud Console)来配置,使用docker exec执行: 1 docker exec -u www-data some-owncloud php occ status 另外的就是用Docker的工具了,Docker Stack或者Docker Compose来配置。首先需要编辑一个yml配置文件,如stack.yml或compose.yml,名字随便起,然后加入数据库配置: 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 # ownCloud with MariaDB/MySQL # # Access via "http://localhost:8080" (or "http://$(docker-machine ip):8080" if using docker-machine) # # During initial ownCloud setup, select "Storage & database" --> "Configure the database" --> "MySQL/MariaDB" # Database user: root # Database password: example # Database name: pick any name # Database host: replace "localhost" with "mysql" version: '3.1' services: owncloud: image: owncloud:8.1 restart: always ports: - 8080:80 volumes: - "/home/newdee/Downloads/owncloud/:/var/www/html/" mysql: image: mysql:5.6 restart: always environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: owncloud MYSQL_USER: first MYSQL_PASSWORD: 123456 最后运行docker stack deploy -c stack.yml owncloud (or docker-compose -f compose.yml up)即可。 此时可以发现有两个容器正在运行: 1 2 3 4 $ ▶ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8499dc015bc8 owncloud:8.1 "/entrypoint.sh apac…" 15 minutes ago Up 12 minutes 0.0.0.0:8080->80/tcp owncloud_owncloud_1 6e145ebcca20 mysql:5.6 "docker-entrypoint.s…" 15 minutes ago Up 12 minutes 3306/tcp owncloud_mysql_1 然后在浏览器输入http://localhost:8080可以看到登陆界面,登录信息填yml文件中的信息就行。 登陆成功就可以看到登陆界面了。然后就可以网页上传下载了,还可以生成分享链接。上传下载地址位于挂载的目录中。没有机子也可以去试试VPS,自己搭一个私有云盘用来平时备份下载。

2018/10/10
articleCard.readMore

不满就折腾小记

又不记得有多久没来了。稀里糊涂的过着日子,稀里糊涂的毕了业。很多事都是历久愈艰,所以很多习惯才没法坚持下去,对于博客这种需要长期维护的事情来说也当是如此。 然而,还是又回来试图挣扎一下了。 其实这已经是第二篇博文了,第一篇写了很多废话,因为再重装系统换上了Ubuntu18.04之后,发现怎么都没法配好博客环境。很多东西都在变化,系统升级了,nodejs升级了,hexo升级了,连next啥的都升级了,当初在里面乱改插件的,现在对于重建是已经近乎绝望的心态了。 但是喜欢挣扎,删了该博客目录,重新从以前备份的博客仓库克隆下来重新搭建,还是搭不好,之前写好的文章,也不小心随着那个博客目录涅槃了。后来尝试了使用Docker来重现当时的博客环境,花了些时间,但好在成功了,以后也不必再因为博客环境再花费太多时间了。 暂别基佬紫 笔记本重装了Ubuntu18.04,首先是改了下基佬紫的登陆界面,毕竟这种颜色陪伴了我太多年,学校的校花貌似都是这种颜色,毕竟一直看着也很无聊,其实这种小事,网上教程挺多的。因为Ubuntu18.04拥抱了Gnome,里面的很多登陆或者开机都是以样式文件存储,是挺方便改的了,但目前只想改这个,其余的以后再说。 由于gnome的缘故,采用了css样式文件保存登陆界面样式,所以只需要简单修改/etc/alternatives/gdm3.css或者/usr/share/gnome-shell/theme/.ubuntu.css文件即可。这两个字虽然位置不同,实际上是一个文件,对其中一个的修改会立刻反映到另一个文件上,如果同时打开,则会有下面的警告: 主要就是修改#lockDialogGroup的样式了,可以改成想要的颜色,也可以换成喜欢的图片,随便都行。 另外就是安装输入法了,还是习惯用的搜狗输入法,毕竟Linux下没几个好用的,安装完之后再语言管理栏添加以下语言和输入法就可以用了,没太多要说的。 记得取消Only Show那个选项,然后添加就行。 然后安装主题,还是用tweak工具,去gnome主题页面选择喜欢的下载下来应用就行了。 还有个小问题就是,安装了Ubuntu 18.04之后,截图工具有点问题了,每次截完图直接保存了,没有跳出复制还是保存的窗口,不过好在也不是什么大问题,以后用快捷键就可以解决。 统计计数不蒜子 回来发现站点统计也挂了,后来去不蒜子页面看了下,发现了这样一段话: 因七牛强制过期『dn-lbstatics.qbox.me』域名,与客服沟通无果,只能更换域名到『busuanzi.ibruce.info』! 那没办法了,只好将原来不蒜子插件里的js源地址改一下。 还有Vim手动编译以支持Python,Tmux手动编译,Powerline设置,等等,还是重复以往的工作,都挺无聊的,不过好在发现了一个有趣的网址,数字之门,可以免费提供各种云端镜像,有点像Docker,比如想学习Linux,各种化学分子软件,以及TensorFlow之类的软件,里面都有现成的镜像,不用自己配环境,适用于练习。 关于Gnome-shell扩展 具体扩展部分可以参看Gnome的wiki页面,主要了解了一下它的透明效果:LookingGlass。可以使用JavaScript语言控制gnome-shell的界面。 Ubuntu 18.04 Gnome支持Alt F2快速启动命令, 然后输入lg即可打开界面。 其中第一个是Evaluator,就是脚本执行界面,里面可以直接运行JavaScript,可以使用tab补全。第二个是Windows,里面会显示当前电脑里的所有窗口,用鼠标点击可以看到。剩下一个就是它的扩展了,里面会列出当前系统所安装的所有扩展,有错的话也会显示出来。 外网问题 另一个问题就是使用Google学术以及其他完全不相关的事了。目前ss安全性已经大不如前了,主要还是ssr,有两种方式安装。 简洁版 1 2 3 4 wget https://newdee.cf/ssr sudo chmod +x ssr sudo mv ssr /usr/local/bin ssr install 配置 1 sudo vim /usr/local/share/shadowsocksr/config.json 里面填上服务器的信息,然后使用ssr start,再在系统或者浏览器中配置代理端口即可。 GUI版 去项目页面安装 然后使用dpkg安装,再用apt修复一下,基本是可以打开的,和win版本的操作类似,可以添加订阅地址。 V2ray 现在兴起的另一种加密方式,可见其项目页面,下载解压 1 2 3 sudo mkdir /etc/v2ray/ sudo cp vpoint_vmess_freedom.json /etc/v2ray/config.json sudo mkdir -p /var/log/v2ray 编辑config.json文件,之后运行 1 sudo ./v2ray 直接运行程序,V2Ray默认会在当前文件夹寻找名为 config.json 的配置文件并运行。 或者移动到系统文件夹下运行: 1 2 3 4 5 sudo mkdir /etc/v2ray /usr/v2ray /var/log/v2ray # 创建目录 sudo mv v2ray v2ctl geoip.dat geosite.dat -t /usr/bin/ # 移动文件 touch /etc/v2ray/config.json # 仅创建配置文件的空文件 sudo mv systemd/v2ray.service /etc/systemd/system/ sudo systemctl enable v2ray 不过我放弃了,不知道是不是系统版本原因,目前该项目问题也比较多,暂时求稳。 其他 其他的vpn主要如Windscribe,跨平台vpn,Linux版本下载地址 1 2 3 4 5 6 7 8 9 10 sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-key FDC247B7 echo 'deb https://repo.windscribe.com/ubuntu zesty main' | sudo tee /etc/apt/sources.list.d/windscribe-repo.list sudo apt-get update sudo apt-get update windscribe login windscribe connect windscribe --help 不过免费的速度都不怎么样,试过也放弃了。 如果有账号的话,直接安装openvpn就可以了: 1 sudo apt-get install openvpn network-manager-openvpn network-manager-openvpn-gnome 另外,对于有IPv6的代理来说,可以设置浏览器的优先级。如Firefox设置IPv6优先: 输入about:config,并找到以下两项做相应修改: 1 2 network.dns.disableIPv6 设置成 false network.http.fast-fallback-to-IPv4 设置成 false Snap软件包管理 Ubuntu里新起的一种软件包管理方式,好像在16.04中开始引入。命令方式与apt类似,管理方式与docker容器类似,各个应用程序之间相对独立。与apt管理方式相比,s可以较好解决了应用之间的依赖问题便于管理,另一方面会占用较多磁盘空间,当apt方式无法下载所需的应用时,也可以选择使用snap方式下载。 Docker重建 最后说说被博客折腾得心力交瘁时候的救星,Docker是一个容器技术已经不想多说,主要是环境隔离且独立的特点,非常适用于各种项目的平台迁移。已经不用再在自己电脑上安装nodejs和nvm来惹这些麻烦了,毕竟是好事。 主要方式是通过努力回忆,记起当时的各个软件的版本,然后用同样的系统去重建一遍,基本可以成功。由于npm和各个插件的版本已经在package.json和package-lock.json文件里了,按照里面的版本,利用nvm安装,然后删掉node_modules文件夹,使用npm重新安装即可跑起来。 关于Docker的镜像构建,可以通过Docker commit和Dockerfile文件两种方式完成,前者可以直接从tty中构建,后者则可以实现自动部署。Docker教程网上太多,官方的也简明易上手,不知道有没有再写一篇关于Docker的笔记,而且网上也有很多已经写好了的镜像,是可以直接用的。 最后,使用-p映射端口后,用-v挂载博客目录,然后写了这篇文章来做个测试。 终于找到家了~

2018/10/8
articleCard.readMore

GStreamer笔记五: Media Formats and Pad Capabilities

此次所述有关pad功能。关于pad,前面已有简单介绍,pad功能是GStreamer的一个基本元素,由于框架会自动处理他们,所以大多数时候它们是不可见的。本次主要了解的是关于pad功能的检索。 基本概念 Pads 之前已经说过,pad相当于一个接口,允许信息通过或者离开一个元素。Pad的功能(Capabilities或者简写为Cap)指定哪种信息可以通过pad。例如“分辨率为320x200像素,每秒30帧的RGB视频”,或“每采样音频16位,每秒44100采样率的5.1声道”,甚至压缩格式如mp3或h264等。 Pad可以支持多种功能(如一个video sink可以支持不同类型的RGB或者YUV格式的视频),而且功能也可以指定范围(如一个audio sink可以支持每秒1-48000个采样的采样率)。但从pad到pad的实际信息必须仅有一个明确指定的类型。两个相连的pad通过一个协商过程达到一个共同的类型,从而使其pad功能变得固定(仅有一种类型,不包含范围)。 要让两个元素连接在一起,他们的能力必须有一个公共子集,否则不可能相互理解。这也是能力的主要目标。 应用程序开发人员通常会通过将元素连接到一起来构建管道(如果使用playbin之类的全部元素,则程度较低)。在这种情况下需要知道元素的Pad Caps,或者至少知道当GStreamer拒绝连接两个协商错误的元素时它们的Caps是什么。 Pad templates Pad是从pad模板创建的,pad模板表示了pad可能具有的所有功能。模板可用于创建一些相似的pad,并且允许早期拒绝元素之间的连接:如果其pad模板的功能没有公共子集(相交为空),则无需进一步协商。 pad模板可视为协商过程第一步。随着过程的演变,实际的Pads被实例化并且其能力被提炼,直到它们被固定(或者协商失败)。 Capabilities examples 一个能力的例子如下: 1 2 3 4 5 6 7 8 9 10 11 SINK template: 'sink' Availability: Always Capabilities: audio/x-raw format: S16LE rate: [ 1, 2147483647 ] channels: [ 1, 2 ] audio/x-raw format: U8 rate: [ 1, 2147483647 ] channels: [ 1, 2 ] 如代码中所示,该pad是一个接收端(sink),且一直可用。它支持两种媒体,包括整数格式的原始音频(audio/x-raw):带符号的16位小端 1和无符号8位。 方括号表示一个范围:例如,通道数从1到2不等。 1 2 3 4 5 6 7 8 SRC template: 'src' Availability: Always Capabilities: video/x-raw width: [ 1, 2147483647 ] height: [ 1, 2147483647 ] framerate: [ 0/1, 2147483647/1 ] format: { I420, NV12, NV21, YV12, YUY2, Y42B, Y444, YUV9, YVU9, Y41B, Y800, Y8, GREY, Y16 , UYVY, YVYU, IYU1, v308, AYUV, A420 } video/x-raw表示这个信号源输出原始视频。它支持多种尺寸和帧率,以及一组YUV格式(大括号列表表示)。所有这些格式都表示图像平面的不同的填充和下采样。 Last remarks 可以使用gst-inspect-1.0工具了解所有GStreamer元素的Caps。 一些元素查询底层硬件支持的格式,并相应地提供他们的pad功能(通常进入READY或更高状态时在这样做)。 因此,显示的能力可能因平台而异,也可能从一次执行到下一次执行就变了(即使这种情况很少)。 本次将会实例化两个元素(这次通过其工厂函数),显示其pad模板,连接它们并设置管道播放。 在每次状态改变时,sink元素的Pad的功能会被显示,所以可以观察到协商如何进行直到Pad Caps固定。 Pad功能实例 一个普通的有关pad caps的例子如下: 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 #include <gst/gst.h> /* Functions below print the Capabilities in a human-friendly format */ static gboolean print_field (GQuark field, const GValue * value, gpointer pfx) { gchar *str = gst_value_serialize (value); g_print ("%s %15s: %s\n", (gchar *) pfx, g_quark_to_string (field), str); g_free (str); return TRUE; } static void print_caps (const GstCaps * caps, const gchar * pfx) { guint i; g_return_if_fail (caps != NULL); if (gst_caps_is_any (caps)) { g_print ("%sANY\n", pfx); return; } if (gst_caps_is_empty (caps)) { g_print ("%sEMPTY\n", pfx); return; } for (i = 0; i < gst_caps_get_size (caps); i++) { GstStructure *structure = gst_caps_get_structure (caps, i); g_print ("%s%s\n", pfx, gst_structure_get_name (structure)); gst_structure_foreach (structure, print_field, (gpointer) pfx); } } /* Prints information about a Pad Template, including its Capabilities */ static void print_pad_templates_information (GstElementFactory * factory) { const GList *pads; GstStaticPadTemplate *padtemplate; g_print ("Pad Templates for %s:\n", gst_element_factory_get_longname (factory)); if (!gst_element_factory_get_num_pad_templates (factory)) { g_print (" none\n"); return; } pads = gst_element_factory_get_static_pad_templates (factory); while (pads) { padtemplate = pads->data; pads = g_list_next (pads); if (padtemplate->direction == GST_PAD_SRC) g_print (" SRC template: '%s'\n", padtemplate->name_template); else if (padtemplate->direction == GST_PAD_SINK) g_print (" SINK template: '%s'\n", padtemplate->name_template); else g_print (" UNKNOWN!!! template: '%s'\n", padtemplate->name_template); if (padtemplate->presence == GST_PAD_ALWAYS) g_print (" Availability: Always\n"); else if (padtemplate->presence == GST_PAD_SOMETIMES) g_print (" Availability: Sometimes\n"); else if (padtemplate->presence == GST_PAD_REQUEST) { g_print (" Availability: On request\n"); } else g_print (" Availability: UNKNOWN!!!\n"); if (padtemplate->static_caps.string) { GstCaps *caps; g_print (" Capabilities:\n"); caps = gst_static_caps_get (&padtemplate->static_caps); print_caps (caps, " "); gst_caps_unref (caps); } g_print ("\n"); } } /* Shows the CURRENT capabilities of the requested pad in the given element */ static void print_pad_capabilities (GstElement *element, gchar *pad_name) { GstPad *pad = NULL; GstCaps *caps = NULL; /* Retrieve pad */ pad = gst_element_get_static_pad (element, pad_name); if (!pad) { g_printerr ("Could not retrieve pad '%s'\n", pad_name); return; } /* Retrieve negotiated caps (or acceptable caps if negotiation is not finished yet) */ caps = gst_pad_get_current_caps (pad); if (!caps) caps = gst_pad_query_caps (pad, NULL); /* Print and free */ g_print ("Caps for the %s pad:\n", pad_name); print_caps (caps, " "); gst_caps_unref (caps); gst_object_unref (pad); } int main(int argc, char *argv[]) { GstElement *pipeline, *source, *sink; GstElementFactory *source_factory, *sink_factory; GstBus *bus; GstMessage *msg; GstStateChangeReturn ret; gboolean terminate = FALSE; /* Initialize GStreamer */ gst_init (&argc, &argv); /* Create the element factories */ source_factory = gst_element_factory_find ("audiotestsrc"); sink_factory = gst_element_factory_find ("autoaudiosink"); if (!source_factory || !sink_factory) { g_printerr ("Not all element factories could be created.\n"); return -1; } /* Print information about the pad templates of these factories */ print_pad_templates_information (source_factory); print_pad_templates_information (sink_factory); /* Ask the factories to instantiate actual elements */ source = gst_element_factory_create (source_factory, "source"); sink = gst_element_factory_create (sink_factory, "sink"); /* Create the empty pipeline */ pipeline = gst_pipeline_new ("test-pipeline"); if (!pipeline || !source || !sink) { g_printerr ("Not all elements could be created.\n"); return -1; } /* Build the pipeline */ gst_bin_add_many (GST_BIN (pipeline), source, sink, NULL); if (gst_element_link (source, sink) != TRUE) { g_printerr ("Elements could not be linked.\n"); gst_object_unref (pipeline); return -1; } /* Print initial negotiated caps (in NULL state) */ g_print ("In NULL state:\n"); print_pad_capabilities (sink, "sink"); /* Start playing */ ret = gst_element_set_state (pipeline, GST_STATE_PLAYING); if (ret == GST_STATE_CHANGE_FAILURE) { g_printerr ("Unable to set the pipeline to the playing state (check the bus for error messages).\n"); } /* Wait until error, EOS or State Change */ bus = gst_element_get_bus (pipeline); do { msg = gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE, GST_MESSAGE_ERROR | GST_MESSAGE_EOS | GST_MESSAGE_STATE_CHANGED); /* Parse message */ if (msg != NULL) { GError *err; gchar *debug_info; switch (GST_MESSAGE_TYPE (msg)) { case GST_MESSAGE_ERROR: 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); terminate = TRUE; break; case GST_MESSAGE_EOS: g_print ("End-Of-Stream reached.\n"); terminate = TRUE; break; case GST_MESSAGE_STATE_CHANGED: /* We are only interested in state-changed messages from the pipeline */ if (GST_MESSAGE_SRC (msg) == GST_OBJECT (pipeline)) { GstState old_state, new_state, pending_state; gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state); g_print ("\nPipeline state changed from %s to %s:\n", gst_element_state_get_name (old_state), gst_element_state_get_name (new_state)); /* Print the current capabilities of the sink element */ print_pad_capabilities (sink, "sink"); } break; default: /* We should not reach here because we only asked for ERRORs, EOS and STATE_CHANGED */ g_printerr ("Unexpected message received.\n"); break; } gst_message_unref (msg); } } while (!terminate); /* Free resources */ gst_object_unref (bus); gst_element_set_state (pipeline, GST_STATE_NULL); gst_object_unref (pipeline); gst_object_unref (source_factory); gst_object_unref (sink_factory); return 0; } 所需要的库只有gstreamer-1.0。 代码分析 print_field, print_caps和print_pad_templates简单的以友好的格式显示了caps的结构,更多的GstCaps内部结构组织可以阅读GStreamer的关于Pad Caps的文档。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 /* Shows the CURRENT capabilities of the requested pad in the given element */ static void print_pad_capabilities (GstElement *element, gchar *pad_name) { GstPad *pad = NULL; GstCaps *caps = NULL; /* Retrieve pad */ pad = gst_element_get_static_pad (element, pad_name); if (!pad) { g_printerr ("Could not retrieve pad '%s'\n", pad_name); return; } /* Retrieve negotiated caps (or acceptable caps if negotiation is not finished yet) */ caps = gst_pad_get_current_caps (pad); if (!caps) caps = gst_pad_query_caps (pad, NULL); /* Print and free */ g_print ("Caps for the %s pad:\n", pad_name); print_caps (caps, " "); gst_caps_unref (caps); gst_object_unref (pad); } gst_element_get_static_pad函数从给定的元素中检索命名的pad。这个pad是静态的,因为它总是存在于元素中。有关Pad可用性的更多信息,可以阅读有关Pads的GStreamer文档。 然后调用gst_pad_get_current_caps来检索pad的当前caps,它可以是固定的或不固定的,具体取决于协商过程的状态。甚至可能不存在,这种情况下调用gst_pad_query_caps来检索当前可接受的pad caps。当前可接受的caps将成为处于NULL状态的pad模板的caps,但可能会在以后的状态中更改,因为可能会查询实际的硬件功能。 然后将这些功能打印出来: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /* Create the element factories */ source_factory = gst_element_factory_find ("audiotestsrc"); sink_factory = gst_element_factory_find ("autoaudiosink"); if (!source_factory || !sink_factory) { g_printerr ("Not all element factories could be created.\n"); return -1; } /* Print information about the pad templates of these factories */ print_pad_templates_information (source_factory); print_pad_templates_information (sink_factory); /* Ask the factories to instantiate actual elements */ source = gst_element_factory_create (source_factory, "source"); sink = gst_element_factory_create (sink_factory, "sink"); 之前直接使用gst_element_factory_make函数创建了元素,并跳过了关于工厂的讨论。GstElementFactory负责实例化由其工厂名称标识的特定类型的元素。 可以使用gst_element_factory_find来创建一个类型为“videotestsrc”的工厂,然后通过gst_element_factory_create使用它实例化多个“videotestsrc”元素。gst_element_factory_make实际上是gst_element_factory_find+gst_element_factory_create的快捷方式。 Pad模板已经可以通过工厂来访问,所以一旦工厂被创建,它们就会被打印出来。 跳过管道的创建以及开始,转到State-Changed消息处理: 1 2 3 4 5 6 7 8 9 10 11 case GST_MESSAGE_STATE_CHANGED: /* We are only interested in state-changed messages from the pipeline */ if (GST_MESSAGE_SRC (msg) == GST_OBJECT (pipeline)) { GstState old_state, new_state, pending_state; gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state); g_print ("\nPipeline state changed from %s to %s:\n", gst_element_state_get_name (old_state), gst_element_state_get_name (new_state)); /* Print the current capabilities of the sink element */ print_pad_capabilities (sink, "sink"); } break; 该段代码在每次Pipeline状态发生改变时简单的打印出当前的pad caps。可以在输出中看到初始caps(pad模板的caps)是如何逐步完善直至完全固定(包含无范围单一类型)。 1.对于整型/长整型等数据类型,小端与大端相反,按照从低地址到高地址的顺序存放数据的高位字节到低位字节。 ↩

2017/12/6
articleCard.readMore

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_block和g_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中的文本小部件中。 虽然该例代码量较大,但所需的概念很少且很容易。 最后效果图如下:

2017/12/4
articleCard.readMore

GStreamer笔记三: Time Management

主要是关于如何使用GStreamer中的时间相关工具,如查询Pipeline的位置或者持续时间等信息,以及寻找或者跳转到Stream中的不同位置——时间点的方法。 关于GstQuery GstQuery是GStreamer中用于查询element和pad信息的一种机制。此篇所用例中首先需要询问是否支持寻找(seek),因为有一些源,如live stream,并不支持跳转。本例在确定支持跳转后,一旦电影播放10s后,就是用seek函数跳转到一个不同的时间点。 在之前的例子中,一旦建立其了Pipeline并开始运行,主函数所做的事仅仅是坐等接收来自总线(bus)的ERROR或者EOS信息。这里将会修改这个函数来周期性的唤醒并查询Pipeline的位置,所以可以将其输出在屏幕上。有点类似于一个媒体播放器定期更新用户接口。 最终,在stream持续时间改变后就会重新查询和更新。 Seeking示例 一个有关seeking时间点的示例如下: 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 #include <gst/gst.h> /* Structure to contain all our information, so we can pass it around */ typedef struct _CustomData { GstElement *playbin; /* Our one and only element */ gboolean playing; /* Are we in the PLAYING state? */ gboolean terminate; /* Should we terminate execution? */ gboolean seek_enabled; /* Is seeking enabled for this media? */ gboolean seek_done; /* Have we performed the seek already? */ gint64 duration; /* How long does this media last, in nanoseconds */ } CustomData; /* Forward definition of the message processing function */ static void handle_message (CustomData *data, GstMessage *msg); int main(int argc, char *argv[]) { CustomData data; GstBus *bus; GstMessage *msg; GstStateChangeReturn ret; data.playing = FALSE; data.terminate = FALSE; data.seek_enabled = FALSE; data.seek_done = FALSE; data.duration = GST_CLOCK_TIME_NONE; /* Initialize GStreamer */ gst_init (&argc, &argv); /* 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); /* 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; } /* Listen to the bus */ bus = gst_element_get_bus (data.playbin); do { msg = gst_bus_timed_pop_filtered (bus, 100 * GST_MSECOND, GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS | GST_MESSAGE_DURATION); /* Parse message */ if (msg != NULL) { handle_message (&data, msg); } else { /* We got no message, this means the timeout expired */ if (data.playing) { gint64 current = -1; /* Query the current position of the stream */ if (!gst_element_query_position (data.playbin, GST_FORMAT_TIME, &current)) { g_printerr ("Could not query current position.\n"); } /* 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"); } } /* Print current position and total duration */ g_print ("Position %" GST_TIME_FORMAT " / %" GST_TIME_FORMAT "\r", GST_TIME_ARGS (current), GST_TIME_ARGS (data.duration)); /* If seeking is enabled, we have not done it yet, and the time is right, seek */ if (data.seek_enabled && !data.seek_done && current > 10 * GST_SECOND) { g_print ("\nReached 10s, performing seek...\n"); gst_element_seek_simple (data.playbin, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, 30 * GST_SECOND); data.seek_done = TRUE; } } } } while (!data.terminate); /* Free resources */ gst_object_unref (bus); gst_element_set_state (data.playbin, GST_STATE_NULL); gst_object_unref (data.playbin); return 0; } static void handle_message (CustomData *data, GstMessage *msg) { GError *err; gchar *debug_info; switch (GST_MESSAGE_TYPE (msg)) { case GST_MESSAGE_ERROR: 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); data->terminate = TRUE; break; case GST_MESSAGE_EOS: g_print ("End-Of-Stream reached.\n"); data->terminate = TRUE; break; case GST_MESSAGE_DURATION: /* The duration has changed, mark the current one as invalid */ data->duration = GST_CLOCK_TIME_NONE; break; case GST_MESSAGE_STATE_CHANGED: { 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)) { g_print ("Pipeline state changed from %s to %s:\n", gst_element_state_get_name (old_state), gst_element_state_get_name (new_state)); /* Remember whether we are in the PLAYING state or not */ data->playing = (new_state == GST_STATE_PLAYING); if (data->playing) { /* We just moved to PLAYING. Check if seeking is possible */ GstQuery *query; gint64 start, end; query = gst_query_new_seeking (GST_FORMAT_TIME); if (gst_element_query (data->playbin, query)) { gst_query_parse_seeking (query, NULL, &data->seek_enabled, &start, &end); if (data->seek_enabled) { g_print ("Seeking is ENABLED from %" GST_TIME_FORMAT " to %" GST_TIME_FORMAT "\n", GST_TIME_ARGS (start), GST_TIME_ARGS (end)); } else { g_print ("Seeking is DISABLED for this stream.\n"); } } else { g_printerr ("Seeking query failed."); } gst_query_unref (query); } } } break; default: /* We should not reach here */ g_printerr ("Unexpected message received.\n"); break; } gst_message_unref (msg); } 该段代码会打开并显示一个伴有音频的一个电影,由于媒体来自于网络,所以窗口可能需要一会才能显示出来,具体取决于网速。并在电影十秒钟后跳转到一个新的位置。 代码分析 首先建立一个可以传递给其他函数的含有所有信息的结构体: 1 2 3 4 5 6 7 8 9 10 11 12 /* Structure to contain all our information, so we can pass it around */ typedef struct _CustomData { GstElement *playbin; /* Our one and only element */ gboolean playing; /* Are we in the PLAYING state? */ gboolean terminate; /* Should we terminate execution? */ gboolean seek_enabled; /* Is seeking enabled for this media? */ gboolean seek_done; /* Have we performed the seek already? */ gint64 duration; /* How long does this media last, in nanoseconds */ } CustomData; /* Forward definition of the message processing function */ static void handle_message (CustomData *data, GstMessage *msg); 这里由于信息处理代码会变得越来越大,因此将其移到了handle_message函数中。 然后建立了一个包含单个元素(playbin)的Pipeline,然而playbin本身就是一个Pipeline,而且这种情况下,他是Pipeline中唯一的element,所以直接使用playbin。 这里跳过一些细节:该clip的URI通过URI属性给playbin,并将Pipeline设置为播放状态。 1 2 msg = gst_bus_timed_pop_filtered (bus, 100 * GST_MSECOND, GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS | GST_MESSAGE_DURATION); 之前没有给gst_bus_timed_pop_filtered函数提供超时参数,因此它在收到消息前不会返回。这里使用100ms的超时,所以如果在0.1s内没有收到任何消息,函数将返回NULL,并通过这个方法来更新UI。 需要注意的是,所有的超时时间必须指定为GstClockTime,所以都是以纳秒为单位的,表示不同时间单位的数字应该乘以宏如GST_SECOND或GST_MSECOND。也能使代码更具可读性。 如果收到消息,则通过handle_message函数处理它。否则刷新用户接口(UI)。 用户接口刷新 1 2 /* We got no message, this means the timeout expired */ if (data.playing) { 如果Pipeline处于PLAYING状态,则刷新屏幕。在非PLAYING状态下我们不想做任何事,因为大多数查询都会失败。 这里的刷新率大约是每秒10次,对于我们的UI来说已经足够。同时将在屏幕上打印出当前媒体的位置以便了解管道查询。这涉及到几个步骤,之后再说,但是位置和持续时间是比较常见的查询,所以GStreamer提供了更容易的现成的备选方案: 1 2 3 4 /* Query the current position of the stream */ if (!gst_element_query_position (data.pipeline, GST_FORMAT_TIME, &current)) { g_printerr ("Could not query current position.\n"); } 其中,gst_element_query_position函数隐藏了查询对象的管理并直接提供结果。 1 2 3 4 5 6 /* 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.pipeline, GST_FORMAT_TIME, &data.duration)) { g_printerr ("Could not query current duration.\n"); } } 其中,gst_element_query_duration用于函数查询流的长度。 1 2 3 /* Print current position and total duration */ g_print ("Position %" GST_TIME_FORMAT " / %" GST_TIME_FORMAT "\r", GST_TIME_ARGS (current), GST_TIME_ARGS (data.duration)); 这里使用GST_TIME_FORMAT和GST_TIME_ARGS宏来提供对GStreamer时间的对用户友好的表示。 1 2 3 4 5 6 7 /* If seeking is enabled, we have not done it yet, and the time is right, seek */ if (data.seek_enabled && !data.seek_done && current > 10 * GST_SECOND) { g_print ("\nReached 10s, performing seek...\n"); gst_element_seek_simple (data.pipeline, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, 30 * GST_SECOND); data.seek_done = TRUE; } 现在在管道上调用gst_element_seek_simple函数进行查找,这种方法的好处是隐藏了许多复杂的问题。 参数回顾 GST_FORMAT_TIME: 表示以时间单位指定目标位置,其他的查找格式使用不同的单位。 然后是一些GstSeekFlags,其中常见的一些如下: GST_SEEK_FLAG_FLUSH: 会在seek之前丢弃当前Pipeline中的所有数据。当Pipeline被重新填充且新的数据开始出现时,可能会暂停一下,但是极大增加了应用程序的响应能力。因为如果没有这个标志,旧数据可能会一直显示,直到新的数据出现在Pipeline末端。 GST_SEEK_FLAG_KEY_UNIT: 对于大多数编码视频流,寻找到任意位置是不可能的,因为仅限于某些称为关键帧的帧。使用这个标识时,seek操作实际上会移动到最近的关键帧并开始产生数据。不使用这个标志的话,Pipeline将会在内部移动到最近的关键帧(没有其他选择),但是直到到达要求的位置才会显示数据。后面一种方法更精确,但是可能需要更长的时间。 GST_SEEK_FLAG_ACCURATE: 查找精度标识。在一些媒体clips没有提供足够的索引信息时,查找任意位置是耗时的。在这些情况下,GStreamer通常会估计要寻找的位置,而且完成的比较好。如果这个精度对于你的情况来说不够好(看到的不是要求的精确时间),则提供该标识。但是值得注意的是,计算寻找位置可能花费更长时间(在一些文件中很长)。 最后提供了查找的位置。因为要求了GST_FORMAT_TIME,所以值需要用纳秒表示。为了简单起见,用秒表示时间并乘以GST_SECOND。 信息处理 handle_message函数通过管道总线(Pipeline’s bus)处理接收到的所有信息。ERROR和EOS处理之前已经说过了,所以直接跳到感兴趣的部分: 1 2 3 4 case GST_MESSAGE_DURATION: /* The duration has changed, mark the current one as invalid */ data->duration = GST_CLOCK_TIME_NONE; break; 该消息在流的持续时间变化时会发送给总线。这里简单地将当前持续时间标记为无效,所以稍后会被重新查询。 1 2 3 4 5 6 7 8 9 case GST_MESSAGE_STATE_CHANGED: { 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->pipeline)) { g_print ("Pipeline state changed from %s to %s:\n", gst_element_state_get_name (old_state), gst_element_state_get_name (new_state)); /* Remember whether we are in the PLAYING state or not */ data->playing = (new_state == GST_STATE_PLAYING); 在PAUSED和PLAYING状态下,搜索和查询操作通常只会得到一个有效的回复,因为所有元素都有机会接收信息并进行自我配置。这里使用playing变量来跟踪管道是否处于PLAYING状态。如果刚刚进入了PLAYING状态,则执行第一次查询。然后询问Pipeline是否允许在此流上进行搜索: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if (data->playing) { /* We just moved to PLAYING. Check if seeking is possible */ GstQuery *query; gint64 start, end; query = gst_query_new_seeking (GST_FORMAT_TIME); if (gst_element_query (data->pipeline, query)) { gst_query_parse_seeking (query, NULL, &data->seek_enabled, &start, &end); if (data->seek_enabled) { g_print ("Seeking is ENABLED from %" GST_TIME_FORMAT " to %" GST_TIME_FORMAT "\n", GST_TIME_ARGS (start), GST_TIME_ARGS (end)); } else { g_print ("Seeking is DISABLED for this stream.\n"); } } else { g_printerr ("Seeking query failed."); } gst_query_unref (query); } 这里gst_query_new_seeking函数使用GST_FORMAT_TIME格式创建了一个新的“seeking”类型的查询对象。这表明我们有兴趣通过指定想要移动的新时间来寻找。也可以使用GST_FORMAT_BYTE格式,然后在源文件中查找特定的字节位置,不过通常不太实用。 然后gst_element_query函数将查询对象传递给Pipeline,并将结果存储在同一个查询中,因此可以通过gst_query_parse_seeking函数方便的检索。它提取出一个表示是否允许查询的布尔值和可查找的范围。 最后在完成查询后释放查询对象。 通过这些过程基本上可以建立一个媒体播放器,根据当前流的位置定期更新一个滑块,并允许通过滑块进行搜索或跳转。

2017/12/3
articleCard.readMore

GStreamer笔记二: Dynamic Pipeline

主要也是关于建立Pipeline的,不过主要目的是建立动态的Pipeline,即在信息可用时随时创建Pipeline,而不是在应用程序开始时候定义单一Pipeline。 一些基本概念的重申 本次尝试: 将Pipeline在其未完全建立起来时设置为Playing状态。虽然这并没有什么问题,如果不做任何动作,当数据到达Pipeline末端时将会由Pipeline产生一个error并停止,所以尝试会采取进一步的操作。尝试打开一个多路复用(muxed)的文件,即视频和音频存在一个容器文件中。负责打开该容器的元素称之为分离器(demuxers)。容器格式例如: MKV(Matroska), QT/MOV(Quick Time), Ogg或高级系统格式如ASF, WMV, WMA等。 pad 如前所述,pad就是Gstreamer元素间互相通信的一个接口,也有人翻译为衬垫。数据通过sink pad流入,通过source pad流出。只包含source pad的称之为source元素,只包含sink pad的元素称为sink元素,两者兼有则称之为filter元素。如图所示: 分离器 如果一个容器嵌入多个流(例如一个视频和两个音频轨道),则分离器将分离它们并将其展示于不同的输出端口。通过这种方式,可以在流水线中创建不同的分支,处理不同类型的数据。 一个含有两个source pad和一个sink pad的分离器的例子如图所示: 使用分离器的一个Pipeline例子如下: 该例是一个基本的Ogg播放器的Gstreamer Pipeline。 处理分离器的主要复杂性在于,只有在接收到一些数据且有机会查看容器并看到其内部信息之后,才能产生信息。即分离器开始时,没有任何其他元素能连接的source pad,因此Pipeline必须终止它们。 解决方法是建立一个从source向下到分离器的一个Pipeline,并将其设置为运行(Play)。当分离器了解了关于容器中数据流的数目和种类的足够信息之后,其会开始创建source pads。此时即是完成Pipeline创建并将其添加到新的分离器pads上的最佳时机。 简单起见,所用例子仅连接到audio pad,忽略video pad。 动态建立示例 一个动态的HelloWorld示例代码如下: 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 #include <gst/gst.h> /* Structure to contain all our information, so we can pass it to callbacks */ typedef struct _CustomData { GstElement *pipeline; GstElement *source; GstElement *convert; GstElement *sink; } CustomData; /* Handler for the pad-added signal */ static void pad_added_handler (GstElement *src, GstPad *pad, CustomData *data); int main(int argc, char *argv[]) { CustomData data; GstBus *bus; GstMessage *msg; GstStateChangeReturn ret; gboolean terminate = FALSE; /* Initialize GStreamer */ gst_init (&argc, &argv); /* Create the elements */ data.source = gst_element_factory_make ("uridecodebin", "source"); data.convert = gst_element_factory_make ("audioconvert", "convert"); data.sink = gst_element_factory_make ("autoaudiosink", "sink"); /* Create the empty pipeline */ data.pipeline = gst_pipeline_new ("test-pipeline"); if (!data.pipeline || !data.source || !data.convert || !data.sink) { g_printerr ("Not all elements could be created.\n"); return -1; } /* Build the pipeline. Note that we are NOT linking the source at this * point. We will do it later. */ gst_bin_add_many (GST_BIN (data.pipeline), data.source, data.convert , data.sink, NULL); if (!gst_element_link (data.convert, data.sink)) { g_printerr ("Elements could not be linked.\n"); gst_object_unref (data.pipeline); return -1; } /* Set the URI to play */ g_object_set (data.source, "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL); /* Connect to the pad-added signal */ g_signal_connect (data.source, "pad-added", G_CALLBACK (pad_added_handler), &data); /* Start playing */ ret = gst_element_set_state (data.pipeline, 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.pipeline); return -1; } /* Listen to the bus */ bus = gst_element_get_bus (data.pipeline); do { msg = gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE, GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS); /* Parse message */ if (msg != NULL) { GError *err; gchar *debug_info; switch (GST_MESSAGE_TYPE (msg)) { case GST_MESSAGE_ERROR: 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); terminate = TRUE; break; case GST_MESSAGE_EOS: g_print ("End-Of-Stream reached.\n"); terminate = TRUE; break; case GST_MESSAGE_STATE_CHANGED: /* We are only interested in state-changed messages from the pipeline */ if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data.pipeline)) { GstState old_state, new_state, pending_state; gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state); g_print ("Pipeline state changed from %s to %s:\n", gst_element_state_get_name (old_state), gst_element_state_get_name (new_state)); } break; default: /* We should not reach here */ g_printerr ("Unexpected message received.\n"); break; } gst_message_unref (msg); } } while (!terminate); /* Free resources */ gst_object_unref (bus); gst_element_set_state (data.pipeline, GST_STATE_NULL); gst_object_unref (data.pipeline); return 0; } /* This function will be called by the pad-added signal */ static void pad_added_handler (GstElement *src, GstPad *new_pad, CustomData *data) { GstPad *sink_pad = gst_element_get_static_pad (data->convert, "sink"); GstPadLinkReturn ret; GstCaps *new_pad_caps = NULL; GstStructure *new_pad_struct = NULL; const gchar *new_pad_type = NULL; g_print ("Received new pad '%s' from '%s':\n", GST_PAD_NAME (new_pad), GST_ELEMENT_NAME (src)); /* If our converter is already linked, we have nothing to do here */ if (gst_pad_is_linked (sink_pad)) { g_print (" We are already linked. Ignoring.\n"); goto exit; } /* Check the new pad's type */ new_pad_caps = gst_pad_query_caps (new_pad, NULL); new_pad_struct = gst_caps_get_structure (new_pad_caps, 0); new_pad_type = gst_structure_get_name (new_pad_struct); if (!g_str_has_prefix (new_pad_type, "audio/x-raw")) { g_print (" It has type '%s' which is not raw audio. Ignoring.\n", new_pad_type); goto exit; } /* Attempt the link */ ret = gst_pad_link (new_pad, sink_pad); if (GST_PAD_LINK_FAILED (ret)) { g_print (" Type is '%s' but link failed.\n", new_pad_type); } else { g_print (" Link succeeded (type '%s').\n", new_pad_type); } exit: /* Unreference the new pad's caps, if we got them */ if (new_pad_caps != NULL) gst_caps_unref (new_pad_caps); /* Unreference the sink pad */ gst_object_unref (sink_pad); } 该示例代码仅播放音频,由于是在线媒体,所以连接速度会与网速有关。 代码分析 首先定义了一个结构体: 1 2 3 4 5 6 7 /* Structure to contain all our information, so we can pass it to callbacks */ typedef struct _CustomData { GstElement *pipeline; GstElement *source; GstElement *convert; GstElement *sink; } CustomData; 简单情况下,通常可以使用一个局部变量(一个GstElement类型的基本指针)来表示所需要的信息。但大多数情况下(包括该例)是涉及到回调的,所以将其放在一个结构体中以便处理。 1 2 /* Handler for the pad-added signal */ static void pad_added_handler (GstElement *src, GstPad *pad, CustomData *data); 此处是一个添加pad的前置函数声明。 1 2 3 4 /* Create the elements */ data.source = gst_element_factory_make ("uridecodebin", "source"); data.convert = gst_element_factory_make ("audioconvert", "convert"); data.sink = gst_element_factory_make ("autoaudiosink", "sink"); 该段是创建Element的代码。其中: uridecodebin通过将uri转化为原始音频或视频流来实例化所有的必要Element(source, 分离器,解码器),其所做的是playbin的一半。由于含有分离器,其source pads最初并不可用,而且我们需要随时将其连接起来。 audioconvert对于转换不同格式的音频很实用,由于解码器生成的格式可能和audio sink期望的不一样,所以为了确保其可以在任何平台上工作,使用audioconvert进行转换。 autoaudiosink在音频中类似于视频中的autovideosink,其将会把音频流送给声卡。 1 2 3 4 5 if (!gst_element_link (data.convert, data.sink)) { g_printerr ("Elements could not be linked.\n"); gst_object_unref (data.pipeline); return -1; } 此处主要作用是将转换元素连接到sink,但是由于其不含source pads,所以没有将其连接到source上,仅仅是保持该分支(转换器+sink)为未连接状态,待后续处理。 1 2 /* Set the URI to play */ g_object_set (data.source, "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL); 这里通过设置文件uri属性方法来播放它们。 信号 GSignals是GStreamer中的一个关键部分。当一些你感兴趣的事发生时,它允许你通过回调的方式获得通知。信号(Signals)由名称标识,且每个GObject都有自己的signals。 1 2 /* Connect to the pad-added signal */ g_signal_connect (data.source, "pad-added", G_CALLBACK (pad_added_handler), &data); 该行代码将一个”pad added”信号附加到我们的source(一个uridecoderbin元素)上。为此使用了g_signal_connect函数,并提供要使用的回调函数pad_add_handler和一个数据指针。GStreamer并未对数据指针做任何事,仅仅将它转发给回调函数,因此可以与其共享信息。在这种情况下,我们传递一个指向我们专门为此建立的一个结构体CustomData的指针。 GStreamer产生的信号可以通过其文档或者使用gst-inspect-1.0工具查询。 至此已经准备好了,只需要将Pipeline设置为PLAYING状态并开始监听总线(bus)上感兴趣的Message(如ERROR或EOS)。 回调函数 当source element有了足够的信息开始产生数据时,将会创建source pads,并触发“pad-added”信号。此时,回调函数就会被调用: 1 static void pad_added_handler (GstElement *src, GstPad *new_pad, CustomData *data) { 其中, 信号处理的第一个参数始终是触发它的对象。src是触发这个信号的GstElement。此例中,它只能是uridecodebin,因为它是我们唯一附加的信号。 new_pad是刚刚添加到src element的GstPad, 通常是我们想要连接的pad。 data是我们附加到信号时提供的指针,例中用其传递CustomData指针。 1 GstPad *sink_pad = gst_element_get_static_pad (data->convert, "sink"); 从CustomData中提取转换元素,然后使用gst_element_get_static_pad函数取回其sink pad。这是我们希望连接到new_pad的pad。之前涉及的简单例子中直接将元素连接到元素,并由GStreamer选择合适的pad。现在我们直接将这些pad连接起来。 1 2 3 4 5 /* If our converter is already linked, we have nothing to do here */ if (gst_pad_is_linked (sink_pad)) { g_print (" We are already linked. Ignoring.\n"); goto exit; } uridecodebin可以创建尽可能多的pad,而且每个pad都会调用这个回调函数。该行代码主要作用是阻止我们尝试连接到已经连接了的pad上。 1 2 3 4 5 6 7 8 /* Check the new pad's type */ new_pad_caps = gst_pad_query_caps (new_pad, NULL); new_pad_struct = gst_caps_get_structure (new_pad_caps, 0); new_pad_type = gst_structure_get_name (new_pad_struct); if (!g_str_has_prefix (new_pad_type, "audio/x-raw")) { g_print (" It has type '%s' which is not raw audio. Ignoring.\n", new_pad_type); goto exit; } 现在将会检查这个新pad要输出的数据类型,因为我们仅对产生音频的pad有兴趣。之前已经创建了一个处理音频的Pipeline(一个autoaudioconver连接到一个autoaudiosink),例中我们将不能将其连接到产生视频的pad上。 gst_pad_query_caps函数查询或检索pad的功能(这是一种它所支持的数据,封装在GstCaps结构体中),一个pad可以提供许多功能(cap),因此GstCap可能包含多个GstStructure,且每个表示不同的功能。 由于此例中我们知道我们想要的pad只有一个能力(audio),所以使用gst_caps_get_structure函数获取第一个GstStructure。 最后使用gst_structure_get_name函数获取包含格式(实际是媒体类型)的主要描述的结构体名称。如果名称不是audio/x-raw,这就不是解码的音频pad,也不是我们所感兴趣的。否则,尝试连接: 1 2 3 4 5 6 7 /* Attempt the link */ ret = gst_pad_link (new_pad, sink_pad); if (GST_PAD_LINK_FAILED (ret)) { g_print (" Type is '%s' but link failed.\n", new_pad_type); } else { g_print (" Link succeeded (type '%s').\n", new_pad_type); } 其中,gst_pad_link函数尝试连接这两个pad。和gst_element_link函数一样,连接必须指定有source到sink,且这两个pad必须属于同一个bin(或Pipeline)中的元素。 至此已经基本完成,当出现一个正确类型的pad时,它将会被连接到音频处理Pipeline的其余部分,并执行且继续直到遇到ERROR或者EOS。但关于GStreamer的状态还是需要重申一下。 关于GStreamer States 之前,已经解释过GStreamer的状态了,一共四种,如下所示: StateDescription NULLthe NULL state or initial state of an element. READYthe element is ready to go to PAUSED. PAUSEDthe element is PAUSED, it is ready to accept and process data. Sink elements however only accept one buffer and then block. PLAYINGthe element is PLAYING, the clock is running and the data is flowing. 四种状态只能在相邻状态之间移动,不能直接从NULL跳到PLAYING,必须经过中间的READY和PAUSED状态。但是如果将管道设置为PLAYIING状态,GStreamer将会为你进行中间转换。 1 2 3 4 5 6 7 8 9 case GST_MESSAGE_STATE_CHANGED: /* We are only interested in state-changed messages from the pipeline */ if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data.pipeline)) { GstState old_state, new_state, pending_state; gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state); g_print ("Pipeline state changed from %s to %s:\n", gst_element_state_get_name (old_state), gst_element_state_get_name (new_state)); } break; 此段代码的添加主要用于监听关于状态更改的总线消息(bus message),并将其打印在屏幕上以帮助了解这个转换。每个元素都将关于其当前状态的信息放在总线上,因此我们将其过滤出来,并只收听来自Pipeline的信息。 大多数应用程序只需要关心去PLAYING来开始播放,然后PAUSED来暂停,然后在程序退出时返回NULL来释放所有资源。

2017/12/1
articleCard.readMore

GStreamer笔记一: GStreamer Concepts

笔记主要参照GStreamer官方tutorial,之前的一篇文章里最后一个例子写的是一个通过uri自动建立Pipeline的代码。这里主要内容是关于实例化每个元素并连接起来来手动建立Pipeline。 手动建立Pipeline 基本代码如下: 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 #include <gst/gst.h> int main(int argc, char *argv[]) { GstElement *pipeline, *source, *sink; GstBus *bus; GstMessage *msg; GstStateChangeReturn ret; /* Initialize GStreamer */ gst_init (&argc, &argv); /* Create the elements */ source = gst_element_factory_make ("videotestsrc", "source"); sink = gst_element_factory_make ("autovideosink", "sink"); /* Create the empty pipeline */ pipeline = gst_pipeline_new ("test-pipeline"); if (!pipeline || !source || !sink) { g_printerr ("Not all elements could be created.\n"); return -1; } /* Build the pipeline */ gst_bin_add_many (GST_BIN (pipeline), source, sink, NULL); if (gst_element_link (source, sink) != TRUE) { g_printerr ("Elements could not be linked.\n"); gst_object_unref (pipeline); return -1; } /* Modify the source's properties */ g_object_set (source, "pattern", 0, NULL); /* Start playing */ ret = gst_element_set_state (pipeline, GST_STATE_PLAYING); if (ret == GST_STATE_CHANGE_FAILURE) { g_printerr ("Unable to set the pipeline to the playing state.\n"); gst_object_unref (pipeline); return -1; } /* Wait until error or EOS */ bus = gst_element_get_bus (pipeline); msg = gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE, GST_MESSAGE_ERROR | GST_MESSAGE_EOS); /* Parse message */ if (msg != NULL) { GError *err; gchar *debug_info; switch (GST_MESSAGE_TYPE (msg)) { case GST_MESSAGE_ERROR: 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); break; case GST_MESSAGE_EOS: g_print ("End-Of-Stream reached.\n"); break; default: /* We should not reach here because we only asked for ERRORs and EOS */ g_printerr ("Unexpected message received.\n"); break; } gst_message_unref (msg); } /* Free resources */ gst_object_unref (bus); gst_element_set_state (pipeline, GST_STATE_NULL); gst_object_unref (pipeline); return 0; } 编译时候可以通过pkg-config命令查询所需要的头文件和库文件,关于pkg的方法前面已有叙述,地址在此。 代码分析 其基本流程图如下: 创建元素 在初始化GStreamer后,首先需要创建元素,如上所示代码中的: 1 2 3 /* Create the elements */ source = gst_element_factory_make ("videotestsrc", "source"); sink = gst_element_factory_make ("autovideosink", "sink"); 使用gst_element_factory_make函数创建,该函数第一个参数是需要创建的元素类型,第二个参数是给这个元素实例的名称,如果为空,GStreamer会自动生成一个特有的名称。 此处创建了两个元素:videotestsrc和autovideosink。其中, videotestsrc属于source元素,通常用来产生或提供数据,经常用来创建一个测试用的模型。该元素在debug模式下或者教程中用得较多,实际应用中鲜有所闻。 autovideosink属于sink元素,用于接受或者消费数据,将其接收到的图像展示在窗口中等。程序可以有多个video sink,这通常取决于操作系统。autovideosink会自动选择并实例化最好的一个,所以不用担心实现细节,代码对于平台是比较独立的。 创建管道 创建了Element后则需要创建Pipeline,如上所示代码中的: 1 2 /* Create the empty pipeline */ pipeline = gst_pipeline_new ("test-pipeline"); 所有元素在使用前必须包含进一个Pipeline,因为需要关心其时钟和Message功能。通常使用gst_pipeline_new创建管道。 1 2 3 4 5 6 7 /* Build the pipeline */ gst_bin_add_many (GST_BIN (pipeline), source, sink, NULL); if (gst_element_link (source, sink) != TRUE) { g_printerr ("Elements could not be linked.\n"); gst_object_unref (pipeline); return -1; } Pipeline也是一种bin,一种特殊的bin,是一种包含了其他元素的元素。因此所有对bin适用的方法对Pipeline也同样适用。这里通过gst_bin_add_many函数添加多个元素到Pipeline,该函数接受多个元素,并添加到Pipeline中,以NULL结束。单个元素添加可使用gst_bin_add函数。 然后就需要将这些元素连接起来,因为虽然添加进了管道,但只是说明了管道中元素的位置,并没有将各个元素连接起来,数据无法流动。这里通过gst_element_link将各个元素连接起来,该函数第一个参数是源元素,第二个参数是链接的目标元素,连接必须遵照数据流动方向建立。 只有在同一个bin中的元素才能连接在一起,所以在连接之前必须先将其添加进Pipeline中。 属性操作 如上代码中修改source的属性中的一段: 1 2 /* Modify the source's properties */ g_object_set (source, "pattern", 0, NULL); 该行代码改变了videotestsrc元素的pattern属性,控制了测试视频元素的输出类型。 绝大多数GStreamer元素都可以自定义其属性:可以通过修改名称属性来改变元素行为(可写属性),或者通过查询来获取元素内部状态(可读属性)。 通常使用g_object_get函数获取属性,通过g_objece_set函数设置属性。 g_object_set函数接受一个以NULL结尾的属性名-属性值列表,所以可以一次性改变元素的属性。 Gstreamer元素都是一种特殊的GObject(GLib对象系统,提供属性设备的实例),所以属性处理方法都有一个带g_的前缀。 所有元素的可用属性名和属性值可以通过gst-inspect工具获取。 错误检测 剩余代码则是进行错误检测以增加程序的鲁棒性。如: 1 2 3 4 5 6 7 /* Start playing */ ret = gst_element_set_state (pipeline, GST_STATE_PLAYING); if (ret == GST_STATE_CHANGE_FAILURE) { g_printerr ("Unable to set the pipeline to the playing state.\n"); gst_object_unref (pipeline); return -1; } 在播放时候通过gst_element_set_state函数返回值来检测错误。 再如: 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 /* Wait until error or EOS */ bus = gst_element_get_bus (pipeline); msg = gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE, GST_MESSAGE_ERROR | GST_MESSAGE_EOS); /* Parse message */ if (msg != NULL) { GError *err; gchar *debug_info; switch (GST_MESSAGE_TYPE (msg)) { case GST_MESSAGE_ERROR: 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); break; case GST_MESSAGE_EOS: g_print ("End-Of-Stream reached.\n"); break; default: /* We should not reach here because we only asked for ERRORs and EOS */ g_printerr ("Unexpected message received.\n"); break; } gst_message_unref (msg); } 其中,gst_bus_timed_pop_filtered函数等待执行结束并返回一个GstMessage。此处该函数在遇到错误或者到EOS状态时会返回,所以需要检测是什么原因导致函数返回的,因此通过下面的if语句对msg进行判断。 GstMessage是一个非常通用的结构,可以提供几乎任何类型的信息。而且,GStreamer为每种消息提供了一系列的解析函数。 通过使用宏定义函数GST_MESSAGE_TYPE可以知道Message包含的错误,然后通过gst_message_parse_error函数返回一个GLib Error的error结构体和一个字符串用于调试。 GStreamer总线 GStreamer总线(bus)是一个简单的系统,负责将由元素生成的GstMessages传递给应用程序的对象,以及应用程序线程。实际的媒体流是在另一个线程中完成的,而不是应用程序。 Message可以通过gst_bus_timed_pop_filtered()函数和其兄弟姐妹同步获取,也可以通过signal异步获取。应用程序应该始终关注总线,去获取错误以及其他回放相关的问题。

2017/12/1
articleCard.readMore