普通视图

发现新文章,点击刷新页面。
昨天以前阁子

相机小述

作者 dfine
2020年7月19日 19:38

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

传感器类型

图像传感器主要有两种,线阵和面阵,平时看到的线阵相机和面阵相机就是因为用的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,图像的亮度、饱和度、对比度等等,由于比较常见,顾名思义,就不继续赘述。
先偷个懒,改天想到了啥,再续狗尾。

四元数与旋转矩阵

作者 dfine
2020年1月8日 16:08

在计算物体旋转时,如相机标定中的旋转矩阵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})$。

私有办公服务搭建

作者 dfine
2019年12月13日 13:47

鉴于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

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

小工具(一)

作者 dfine
2019年8月19日 01:57

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

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

FlashCards

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

终端美化LSD

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

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

Tree2dotx

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

1
tree | tree2dotx > out.dot

Graphviz

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

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

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

Gprof

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

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

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


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

EMMMM, To be continued…

Git基本用法

作者 dfine
2019年3月21日 22:01

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

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

基本文件操作

检查文件状态

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

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

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

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

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

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

基本文件操作

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

1
touch test

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

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

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

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

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

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

查看提交历史

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

撤销操作

仅修改提交信息

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

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

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

取消暂存区文件

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

1
git reset HEAD test

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

撤销对文件的修改

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

远程仓库

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

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

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

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

标签

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

列出标签

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

1
git tag -l 'v1.0.1*'

创建标签

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

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

查看标签:

1
2
3
git tag
v1.0
v1.1

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

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

查看标签:

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

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

后期上标签

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

1
git tag -a v0.8 e0c2975

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

分享标签

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

1
git push origin <tagname>

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

1
git push origin --tags

删除标签

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

1
git tag -d <tagname>

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

标签检出

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

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

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

分支

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

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

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

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

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

分支创建

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

1
git branch <branchname>

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

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

分支切换

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

1
git checkout <branchname>

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

1
git checkout -b <branchname>

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

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

分支合并

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

1
git merge <branchname>

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

删除分支

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

1
git branch -d <branchname>

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

冲突合并

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

远程分支

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

推送本地分支

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

1
git push origin test

等价于

1
git push origin test:test

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

跟踪分支

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

1
git checkout --track origin/test

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

1
git checkout -b sf origin/test

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

1
git branch -u origin/test

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

合并分支

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

删除分支

删除远程分支可以使用:

1
git push origin --delete test

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

1
git push origin :<remotebranch>

变基

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

1
2
git checkout branch1
git rebase branch2

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

1
2
git checkout branch2
git merge branch1

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

1
git rebash --onto master branch1 branch2

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

1
2
git checkout master
git merge branch2

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

1
git rebase master branch1

然后快进合并master分支:

1
2
git checkout master
git merge branch1

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

变基风险

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

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

其他操作

别名

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

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

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

储藏

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

1
git stash apply

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

1
git stash apply stash@{2}

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

取消储藏

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

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

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

1
git stash show -p | git apply -R

从储藏中创建分支

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

1
git stash branch <branchname>

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

Telegram接管聊天消息

作者 dfine
2019年3月20日 22:49

虽有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:

  1. 搜索并找到@botfather机器人,然后发送指令:/newbot
  2. 给Bot起个名字。
  3. 给机器人起用户名,以bot结尾
  4. 获取机器人的Token
  5. 设置Bot隐私权限: 默认Bot可能无法接收非/开头的消息,所以需要设置隐私权限。向该机器人发送指令/setprivacy,选择刚刚创建的机器人,点Disable即可。
  6. 允许将Bot添加进群组: 给机器人发送指令/setjoingroups,选择enable。
  7. 允许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消息。

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

作者 dfine
2019年3月5日 15:25

说来惭愧,也不记得有几次立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账号,然后将博客的源文件仓库链接至该镜像仓库,如下图所示。


Docker CI

不过也不一定要链接至博客源文件仓库,可以新建一个仓库,将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 .以及使用commitpush上传至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层层有缓存,所以第一种方式也只有第一次较慢。
Docker CI
日志
最后观察触发是否成功以及最后输出结果是否在预期内。
上述涉及到ADDCOPY区别,COPY只是简单的复制,ADD支持下载URL,并支持解压,并具有判断其ADDsrc功能。因此,在没有特殊需求时,尽量使用COPY提高效率,上述使用ADD .git/是为了让Docker Daemon判断git仓库是否更新了,如果更新了,则不使用缓存,这样,后续的git pull才能真正获取更新并最终更新到网站上。


取消缓存

此外,还可以添加.dockerignore文件来忽略一些容器中你不用的文件以提高速度。容器加载时会默认将当前目录下所有文件打包传给Docker Daemon,比如就是node_modules文件夹。
没有写的太细,长篇累牍容易给看客压力,step by step操作一番,不填坑也不会有印象,也并不会达到对容器熟悉的效果。
因为容器的方便性,我曾不断向人安利,但也许是推荐的对象不适合,用的人貌似不多,记得以前也答应过人把Docker封装环境的详细过程写写,现在算是应诺了,希望还能帮人节省一点时间。

LXD搭设服务器

作者 dfine
2018年10月12日 07:11

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

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



初始化

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

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

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

1
sudo usermod add -aG lxd ${USER}

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

1
sudo apt install zfsutils-linux bridge-utils

初始化过程如下:

1
lxd init

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

1
lxc launch ubuntu:16.04 test

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

1
lxc exec test bash

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

显卡配置

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

1
lxc config device add test gpu gpu

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

1
2
lxc exec test bash
apt update

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

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

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

网络配置

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

lxc创建网桥

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

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

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

1
sudo brctl addif lxd0 enp1s0

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

宿主机路由

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

1
sudo route add default gw 192.168.1.1

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

重新初始化

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

分配静态地址

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

1
vim /etc/network/interfaces

添加

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

重启网络服务

1
/etc/init.d/networking restart

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

修改DNS

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

修改Resolvconf配置

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

修改DHCP配置

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

1
vim /etc/dhcp/dhclient.conf

可以看到,

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

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

ssh配置

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

1
vim /etc/ssh/sshd_config

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

1
passwd root

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

1
/etc/init.d/ssh restart

挂载共享目录

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

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

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

不满就折腾小记

作者 dfine
2018年10月8日 19:40

又不记得有多久没来了。稀里糊涂的过着日子,稀里糊涂的毕了业。很多事都是历久愈艰,所以很多习惯才没法坚持下去,对于博客这种需要长期维护的事情来说也当是如此。
然而,还是又回来试图挣扎一下了。

其实这已经是第二篇博文了,第一篇写了很多废话,因为再重装系统换上了Ubuntu18.04之后,发现怎么都没法配好博客环境。很多东西都在变化,系统升级了,nodejs升级了,hexo升级了,连next啥的都升级了,当初在里面乱改插件的,现在对于重建是已经近乎绝望的心态了。

但是喜欢挣扎,删了该博客目录,重新从以前备份的博客仓库克隆下来重新搭建,还是搭不好,之前写好的文章,也不小心随着那个博客目录涅槃了。后来尝试了使用Docker来重现当时的博客环境,花了些时间,但好在成功了,以后也不必再因为博客环境再花费太多时间了。

暂别基佬紫

笔记本重装了Ubuntu18.04,首先是改了下基佬紫的登陆界面,毕竟这种颜色陪伴了我太多年,学校的校花貌似都是这种颜色,毕竟一直看着也很无聊,其实这种小事,网上教程挺多的。因为Ubuntu18.04拥抱了Gnome,里面的很多登陆或者开机都是以样式文件存储,是挺方便改的了,但目前只想改这个,其余的以后再说。

由于gnome的缘故,采用了css样式文件保存登陆界面样式,所以只需要简单修改/etc/alternatives/gdm3.css或者/usr/share/gnome-shell/theme/.ubuntu.css文件即可。这两个字虽然位置不同,实际上是一个文件,对其中一个的修改会立刻反映到另一个文件上,如果同时打开,则会有下面的警告:


theme

主要就是修改#lockDialogGroup的样式了,可以改成想要的颜色,也可以换成喜欢的图片,随便都行。


theme

另外就是安装输入法了,还是习惯用的搜狗输入法,毕竟Linux下没几个好用的,安装完之后再语言管理栏添加以下语言和输入法就可以用了,没太多要说的。
记得取消Only Show那个选项,然后添加就行。
然后安装主题,还是用tweak工具,去gnome主题页面选择喜欢的下载下来应用就行了。

还有个小问题就是,安装了Ubuntu 18.04之后,截图工具有点问题了,每次截完图直接保存了,没有跳出复制还是保存的窗口,不过好在也不是什么大问题,以后用快捷键就可以解决。


screenshot

统计计数不蒜子

回来发现站点统计也挂了,后来去不蒜子页面看了下,发现了这样一段话:

因七牛强制过期『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快速启动命令,


lookingglass

然后输入lg即可打开界面。
lookingglass

其中第一个是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.jsonpackage-lock.json文件里了,按照里面的版本,利用nvm安装,然后删掉node_modules文件夹,使用npm重新安装即可跑起来。
关于Docker的镜像构建,可以通过Docker commit和Dockerfile文件两种方式完成,前者可以直接从tty中构建,后者则可以实现自动部署。Docker教程网上太多,官方的也简明易上手,不知道有没有再写一篇关于Docker的笔记,而且网上也有很多已经写好了的镜像,是可以直接用的。
最后,使用-p映射端口后,用-v挂载博客目录,然后写了这篇文章来做个测试。

终于找到家了~

GSreamer笔记四: GUI Toolkit Integration

作者 dfine
2017年12月4日 16:23

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

需要解决的问题有:

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

关于GTK+

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

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

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

GTK+播放器示例

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

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

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

代码分析

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main(int argc, char *argv[]) {
CustomData data;
GstStateChangeReturn ret;
GstBus *bus;
/* Initialize GTK */
gtk_init (&argc, &argv);
/* Initialize GStreamer */
gst_init (&argc, &argv);
/* Initialize our data structure */
memset (&data, 0, sizeof (data));
data.duration = GST_CLOCK_TIME_NONE;
/* Create the elements */
data.playbin = gst_element_factory_make ("playbin", "playbin");
if (!data.playbin) {
g_printerr ("Not all elements could be created.\n");
return -1;
}
/* Set the URI to play */
g_object_set (data.playbin, "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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



❌
❌