关于DM368的H264视频编码过程(中)
接上篇
三、DM368视频编码程序
这部分中仅抽取了例程中有关视频编码部分的功能,删除了演示程序的一些demo结构体、处理过程,并添加了注释。
main.c主线程
/*
* main.c
*/
#include <stdio.h> //基本输入输出
#include <pthread.h> //线程相关操作
#include <signal.h> //提供外部信号中断
#include <sys/time.h> //setpriority设置优先级
#include <sys/resource.h>//setpriority设置优先级
#include <xdc/std.h>
//调用编解码器引擎
#include <ti/sdo/ce/trace/gt.h>
#include <ti/sdo/ce/CERuntime.h>
//调用达芬奇多媒体应用程序接口
#include <ti/sdo/dmai/Dmai.h>
#include <ti/sdo/dmai/Fifo.h>
#include <ti/sdo/dmai/Pause.h>
#include <ti/sdo/dmai/Sound.h>
#include <ti/sdo/dmai/VideoStd.h>
#include <ti/sdo/dmai/Capture.h>
#include <ti/sdo/dmai/BufferGfx.h>
#include <ti/sdo/dmai/Rendezvous.h>
//组件框架
#include <ti/sdo/fc/rman/rman.h>
#include "video.h"
#include "capture.h"
#include "writer.h"
#include "ctrl.h"
#include "demo.h"
/* 初始化级别 */
#define LOGSINITIALIZED 0x1
#define CAPTURETHREADCREATED 0x40
#define WRITERTHREADCREATED 0x80
#define VIDEOTHREADCREATED 0x100
/* 线程优先级 */
#define VIDEO_THREAD_PRIORITY sched_get_priority_max(SCHED_FIFO) - 1
//程序运行指定的参数结构体
typedef struct Args {
VideoStd_Type videoStd; //指定编码的视频标准:类型
Capture_Input videoInput; //视频输入源
Char *videoEncoder;//指定保存的视频文件编码格式(从传入的后缀名得到)
Int32 imageWidth; //图像宽度
Int32 imageHeight; //图像高度
Int videoBitRate;//视频位率(控制量化精度):数值
} Args;
#define DEFAULT_ARGS {VideoStd_1080P_30, Capture_Input_CAMERA, "h264enc", 1920, 1080, -1}
/* 此应用程序的全局变量声明 */
GlobalData gbl = GBL_DATA_INIT;
void HandleSIG(int signo)
{
if(SIGINT==signo||SIGTERM==signo)
{
gblSetQuit();//退出程序
}
}
/******************************************************************************
* main
******************************************************************************/
Int main(void)
{
Args args = DEFAULT_ARGS;//整个视频编码过程中涉及到的参数,程序运行时传入
Uns initMask = 0;//初始化掩码配置,每一位用于存储各线程状态,用于程序退出时指示是否需要回收资源
Int status = EXIT_SUCCESS;//程序退出时使用的返回值
Pause_Attrs pAttrs = Pause_Attrs_DEFAULT;//暂停对象的属性设置
Rendezvous_Attrs rzvAttrs = Rendezvous_Attrs_DEFAULT;//“会合”线程组的属性设置
Fifo_Attrs fAttrs = Fifo_Attrs_DEFAULT;//捕获和编码线程间使用的FIFO属性设置
Rendezvous_Handle hRendezvousCapStd = NULL;//“会合”线程组——捕获器
Rendezvous_Handle hRendezvousInit = NULL;//“会合”线程组——初始化
Rendezvous_Handle hRendezvousWriter = NULL;//“会合”线程组——写线程
Rendezvous_Handle hRendezvousCleanup = NULL;//“会合”线程组——清除操作
Pause_Handle hPauseProcess = NULL;//创建的暂停对象句柄
struct sched_param schedParam; //视频线程优先级参数相关
pthread_t captureThread;//图像捕获线程
pthread_t writerThread; //写出线程
pthread_t videoThread; //视频线程
CaptureEnv captureEnv; //捕获线程环境(资源、函数、参数等)
WriterEnv writerEnv; //写线程环境(资源、函数、参数等)
VideoEnv videoEnv; //视频线程环境(资源、函数、参数等)
CtrlEnv ctrlEnv; //控制线程环境(资源、函数、参数等)
Int numThreads; //需要同步的线程数,提供给“会合”线程组创建和清空时使用
pthread_attr_t attr; //线程自定义调度属性
Void *ret; //操作执行时使用的返回值
signal(SIGINT, HandleSIG); //注册Ctrl+C信号处理函数
signal(SIGTERM, HandleSIG);//注册无参数kill信号处理函数
system("/etc/init.d/loadmodule-rc restart");//重新挂载模块-?
/* 清空线程环境 */
Dmai_clear(captureEnv);
Dmai_clear(writerEnv);
Dmai_clear(videoEnv);
Dmai_clear(ctrlEnv);
/* 初始化互斥量以保护全局数据 */
pthread_mutex_init(&gbl.mutex, NULL);
/* 将整个过程的优先级设置为max(需要root) */
setpriority(PRIO_PROCESS, 0, -20);
/* 初始化编解码器引擎运行 */
CERuntime_init();
/* 初始化Davinci多媒体应用程序接口(interface) */
Dmai_init();
initMask |= LOGSINITIALIZED;
/* 创建暂停对象 */
hPauseProcess = Pause_create(&pAttrs);
if (hPauseProcess == NULL) {
ERR("Failed to create Pause object\n");
cleanup(EXIT_FAILURE);
}
/* 确定需要同步的线程数 */
numThreads = 1;
/* 创建使线程初始化和清理同步的对象 */
hRendezvousCapStd = Rendezvous_create(2, &rzvAttrs);
hRendezvousInit = Rendezvous_create(numThreads, &rzvAttrs);
hRendezvousCleanup = Rendezvous_create(numThreads, &rzvAttrs);
hRendezvousWriter = Rendezvous_create(2, &rzvAttrs);
if (hRendezvousCapStd == NULL || hRendezvousInit == NULL ||
hRendezvousCleanup == NULL || hRendezvousWriter == NULL) {
ERR("Failed to create Rendezvous objects\n");
cleanup(EXIT_FAILURE);
}
/* 初始化线程属性变量 */
if (pthread_attr_init(&attr)) {
ERR("Failed to initialize thread attrs\n");
cleanup(EXIT_FAILURE);
}
/* 强制线程使用自定义调度属性 */
if (pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED)) {
ERR("Failed to set schedule inheritance attribute\n");
cleanup(EXIT_FAILURE);
}
/* 将线程设置为fifo实时调度 */
if (pthread_attr_setschedpolicy(&attr, SCHED_FIFO)) {
ERR("Failed to set FIFO scheduling policy\n");
cleanup(EXIT_FAILURE);
}
/* 创建视频线程 */
{
/* 创建捕获(进程使用的)FIFO */
captureEnv.hInFifo = Fifo_create(&fAttrs);
captureEnv.hOutFifo = Fifo_create(&fAttrs);
if (captureEnv.hInFifo == NULL || captureEnv.hOutFifo == NULL) {
ERR("Failed to open display fifos\n");
cleanup(EXIT_FAILURE);
}
/* 创建捕获线程 */
captureEnv.hRendezvousInit = hRendezvousInit;
captureEnv.hRendezvousCapStd = hRendezvousCapStd;
captureEnv.hRendezvousCleanup = hRendezvousCleanup;
captureEnv.hPauseProcess = hPauseProcess;//创建的暂停对象句柄
captureEnv.videoStd = args.videoStd;//赋值视频标准
captureEnv.videoInput = args.videoInput;//赋值视频输入源
captureEnv.imageWidth = args.imageWidth;//赋值图像宽度
captureEnv.imageHeight = args.imageHeight;//赋值图像高度
if (pthread_create(&captureThread, NULL, captureThrFxn, &captureEnv)) {
ERR("Failed to create capture thread\n");
cleanup(EXIT_FAILURE);
}
initMask |= CAPTURETHREADCREATED;
/*
* 捕获线程检测到视频标准后,使其可用于其他线程。
* 捕获线程将设置缓冲区的分辨率以在环境中进行编码
* (如果用户未通过分辨率,则从视频标准派生)。
*/
Rendezvous_meet(hRendezvousCapStd);
/* 创建writer(使用的)FIFO */
writerEnv.hInFifo = Fifo_create(&fAttrs);
writerEnv.hOutFifo = Fifo_create(&fAttrs);
if (writerEnv.hInFifo == NULL || writerEnv.hOutFifo == NULL) {
ERR("Failed to open display fifos\n");
cleanup(EXIT_FAILURE);
}
/* 设置视频线程优先级 */
schedParam.sched_priority = VIDEO_THREAD_PRIORITY;
if (pthread_attr_setschedparam(&attr, &schedParam)) {
ERR("Failed to set scheduler parameters\n");
cleanup(EXIT_FAILURE);
}
/* 创建视频线程 */
videoEnv.hRendezvousInit = hRendezvousInit;
videoEnv.hRendezvousCleanup = hRendezvousCleanup;
videoEnv.hRendezvousWriter = hRendezvousWriter;
videoEnv.hPauseProcess = hPauseProcess;//创建的暂停对象句柄
videoEnv.hCaptureOutFifo = captureEnv.hOutFifo;//捕获线程输出FIFO
videoEnv.hCaptureInFifo = captureEnv.hInFifo;//捕获线程输入FIFO
videoEnv.hWriterOutFifo = writerEnv.hOutFifo;//写线程输出FIFO
videoEnv.hWriterInFifo = writerEnv.hInFifo;//写线程输入FIFO
videoEnv.videoEncoder = args.videoEncoder;//编码器实例类型
videoEnv.videoBitRate = args.videoBitRate;//视频比特率
videoEnv.imageWidth = captureEnv.imageWidth;//编码视频宽度
videoEnv.imageHeight = captureEnv.imageHeight;//编码视频高度
videoEnv.engineName = "encode";//引擎名称
videoEnv.videoFrameRate = 30000;//视频帧率
if (pthread_create(&videoThread, &attr, videoThrFxn, &videoEnv)) {
ERR("Failed to create video thread\n");
cleanup(EXIT_FAILURE);
}
initMask |= VIDEOTHREADCREATED;
/*
* 在启动编码器线程之前,等待在视频线程中创建编解码器
* (否则我们不知道要使用哪种缓冲区大小)。
*/
Rendezvous_meet(hRendezvousWriter);
/* 创建writer线程 */
writerEnv.hRendezvousInit = hRendezvousInit;
writerEnv.hRendezvousCleanup = hRendezvousCleanup;
writerEnv.hPauseProcess = hPauseProcess;//创建的暂停对象句柄
writerEnv.outBufSize = videoEnv.outBufSize;//输出缓冲区大小
if (pthread_create(&writerThread, NULL, writerThrFxn, &writerEnv)) {
ERR("Failed to create writer thread\n");
cleanup(EXIT_FAILURE);
}
initMask |= WRITERTHREADCREATED;
}
/* 主线程成为控制线程 */
ctrlEnv.hRendezvousInit = hRendezvousInit;
ctrlEnv.hRendezvousCleanup = hRendezvousCleanup;
ctrlEnv.hPauseProcess = hPauseProcess;//创建的暂停对象句柄
ctrlEnv.engineName = "encode";//引擎名称
ret = ctrlThrFxn(&ctrlEnv);
if (ret == THREAD_FAILURE) {
status = EXIT_FAILURE;
}
cleanup:
/* 确保其他线程不在等待初始化完成 */
if (hRendezvousCapStd) Rendezvous_force(hRendezvousCapStd);
if (hRendezvousWriter) Rendezvous_force(hRendezvousWriter);
if (hRendezvousInit) Rendezvous_force(hRendezvousInit);
if (hPauseProcess) Pause_off(hPauseProcess);
/* 等到其他线程终止 */
if (initMask & VIDEOTHREADCREATED) {
if (pthread_join(videoThread, &ret) == 0) {
if (ret == THREAD_FAILURE) {
status = EXIT_FAILURE;
}
}
}
if (initMask & WRITERTHREADCREATED) {
if (pthread_join(writerThread, &ret) == 0) {
if (ret == THREAD_FAILURE) {
status = EXIT_FAILURE;
}
}
}
if (writerEnv.hOutFifo) {
Fifo_delete(writerEnv.hOutFifo);
}
if (writerEnv.hInFifo) {
Fifo_delete(writerEnv.hInFifo);
}
if (initMask & CAPTURETHREADCREATED) {
if (pthread_join(captureThread, &ret) == 0) {
if (ret == THREAD_FAILURE) {
status = EXIT_FAILURE;
}
}
}
if (captureEnv.hOutFifo) {
Fifo_delete(captureEnv.hOutFifo);
}
if (captureEnv.hInFifo) {
Fifo_delete(captureEnv.hInFifo);
}
if (hRendezvousCleanup) {
Rendezvous_delete(hRendezvousCleanup);
}
if (hRendezvousInit) {
Rendezvous_delete(hRendezvousInit);
}
if (hPauseProcess) {
Pause_delete(hPauseProcess);
}
/*
* 过去,有些情况下,每次读取文件或写入文件时,系统内存每次都会连续减少28个字节。
* 这是供应用程序重新捕获该内存的(SDOCM00054899)
*/
system("sync");//强制更新磁盘信息
system("echo 3 > /proc/sys/vm/drop_caches");//清空缓存
pthread_mutex_destroy(&gbl.mutex);//销毁全局变量中的互斥锁
exit(status);
}
很长么?知足吧,原来的演示程序更长,main的任务主要有这些:
1.在演示程序中接收并处理启动时传递的参数以便设置到相应的编解码线程中去,本程序中删除;
2.初始化编解码器引擎和Dmai(Davinci多媒体应用程序接口);
3.设置各线程的参数并创建这些线程:
——捕获器线程captureThrFxn
——视频编码线程videoThrFxn
——码流输出线程writerThrFxn
4.主函数转为控制线程,转的方法是运行Ctrl.c中的ctrlThrFxn()函数,目的是执行用于程序和外界的指令交互等于编码无关的动作。
在上述过程中还会设置一些标志位,以便退出时有选择地销毁线程。
在这个过程中对于Rendezvous组函数的作用,我不是很了解,看其所在位置应该是用于线程间同步的,所以例程中怎么放、怎么用,我们照搬就行。
capture.c捕获器线程
/*
* capture.c
*/
#include <xdc/std.h>
#include <ti/sdo/dmai/Fifo.h>
#include <ti/sdo/dmai/Pause.h>
#include <ti/sdo/dmai/BufTab.h>
#include <ti/sdo/dmai/Capture.h>
#include <ti/sdo/dmai/VideoStd.h>
#include <ti/sdo/dmai/BufferGfx.h>
#include <ti/sdo/dmai/Rendezvous.h>
#include <ti/sdo/dmai/Dmai.h>
#include "capture.h"
#include "demo.h"
/* 为捕获驱动程序缓冲 */
#define NUM_CAPTURE_BUFS 3
/* 捕获线程的管道中的缓冲区数 */
/* 注意:它需要匹配video.c管道尺寸 */
#define VIDEO_PIPE_SIZE 4
/* UYVY格式的黑色 */
#define UYVY_BLACK 0x10801080
int capture_fd;
/******************************************************************************
* CapBuf_blackFill
* 注意:此函数使用x,y参数来计算缓冲区的偏移量。
******************************************************************************/
Void CapBuf_blackFill(Buffer_Handle hBuf)
{
switch (BufferGfx_getColorSpace(hBuf)) {
case ColorSpace_YUV422PSEMI:
{
Int8 *yPtr = Buffer_getUserPtr(hBuf);
Int32 ySize = Buffer_getSize(hBuf) / 2;
Int8 *cbcrPtr = yPtr + ySize;
Int bpp = ColorSpace_getBpp(ColorSpace_YUV422PSEMI);
Int y;
BufferGfx_Dimensions dim;
UInt32 offset;
BufferGfx_getDimensions(hBuf, &dim);
offset = dim.y * dim.lineLength + dim.x * bpp / 8;
for (y = 0; y < dim.height; y++) {
memset(yPtr + offset, 0x0, dim.width * bpp / 8);
yPtr += dim.lineLength;
}
for (y = 0; y < dim.height; y++) {
memset(cbcrPtr + offset, 0x80, dim.width * bpp / 8);
cbcrPtr += dim.lineLength;
}
break;
}
case ColorSpace_YUV420PSEMI:
{
Int8 *yPtr = Buffer_getUserPtr(hBuf);
Int32 ySize = Buffer_getSize(hBuf) * 2 / 3;
Int8 *cbcrPtr = yPtr + ySize;
Int bpp = ColorSpace_getBpp(ColorSpace_YUV420PSEMI);
Int y;
BufferGfx_Dimensions dim;
BufferGfx_getDimensions(hBuf, &dim);
yPtr += dim.y * dim.lineLength + dim.x * bpp / 8;
for (y = 0; y < dim.height; y++) {
memset(yPtr, 0x0, dim.width * bpp / 8);
yPtr += dim.lineLength;
}
cbcrPtr += dim.y * dim.lineLength / 2 + dim.x * bpp / 8;
for (y = 0; y < dim.height / 2; y++) {
memset(cbcrPtr, 0x80, dim.width * bpp / 8);
cbcrPtr += dim.lineLength;
}
break;
}
case ColorSpace_UYVY:
{
Int32 *bufPtr = (Int32*)Buffer_getUserPtr(hBuf);
Int bpp = ColorSpace_getBpp(ColorSpace_UYVY);
Int i, j;
BufferGfx_Dimensions dim;
BufferGfx_getDimensions(hBuf, &dim);
bufPtr += (dim.y * dim.lineLength + dim.x * bpp / 8) / sizeof(Int32);
/* 确保显示缓冲区是4字节对齐的 */
assert((((UInt32) bufPtr) & 0x3) == 0);
for (i = 0; i < dim.height; i++) {
for (j = 0; j < dim.width / 2; j++) {
bufPtr[j] = UYVY_BLACK;
}
bufPtr += dim.lineLength / sizeof(Int32);
}
break;
}
case ColorSpace_RGB565:
{
memset(Buffer_getUserPtr(hBuf), 0, Buffer_getSize(hBuf));
break;
}
default:
{
ERR("Unsupported color space (%d) for _Dmai_blackFill\n",
BufferGfx_getColorSpace(hBuf));
break;
}
}
}
/******************************************************************************
* 捕获线程入口函数
******************************************************************************/
Void *captureThrFxn(Void *arg)
{
CaptureEnv *envp = (CaptureEnv *) arg;//传入给线程的参数结构体
Void *status = THREAD_SUCCESS; //线程状态返回值
Capture_Attrs cAttrs = Capture_Attrs_DM365_DEFAULT;//捕获线程参数默认值
BufferGfx_Attrs gfxAttrs = BufferGfx_Attrs_DEFAULT; //缓冲区默认属性
Capture_Handle hCapture = NULL; //创建的捕获器实例句柄
BufTab_Handle hBufTab = NULL; //要与捕获驱动程序一起使用的缓冲区表
BufTab_Handle hFifoBufTab = NULL; //要与视频编码线程一起使用的缓冲区表
Buffer_Handle hDstBuf, hCapBuf, hBuf;//收回的缓冲区、捕获缓冲区、填充缓冲区?
BufferGfx_Dimensions capDim; //缓冲器Gfx尺寸
Int32 width, height, bufSize;//宽高临时变量用于分辨率合法性检测、缓冲区大小临时变量
Int fifoRet; //对FIFO操作的执行返回值
ColorSpace_Type colorSpace = ColorSpace_YUV420PSEMI;//颜色空间类型定义
Int bufIdx; //对缓冲区操作的循环计数变量
Int numCapBufs; //捕获器缓冲数量,由NUM_CAPTURE_BUFS宏定义决定
/*分辨率合法性检测和缓冲区对齐*/
if (envp->imageWidth > 0 && envp->imageHeight > 0)
{
if (VideoStd_getResolution(envp->videoStd, &width, &height) < 0)
{
ERR("Failed to calculate resolution of video standard\n");
cleanup(THREAD_FAILURE);
}
//用户分辨率不能大于视频标准的分辨率,否则报错
if (width < envp->imageWidth && height < envp->imageHeight)
{
ERR("User resolution (%ldx%ld) larger than detected (%ldx%ld)\n",
envp->imageWidth, envp->imageHeight, width, height);
cleanup(THREAD_FAILURE);
}
/*
* 捕获驱动程序提供32字节对齐的数据。
* 我们将捕获和视频缓冲区对齐32个字节,以执行零拷贝编码。
*/
envp->imageWidth = Dmai_roundUp(envp->imageWidth,32);
capDim.x = 0;
capDim.y = 0;
capDim.height = envp->imageHeight;
capDim.width = envp->imageWidth;
capDim.lineLength = BufferGfx_calcLineLength(capDim.width, colorSpace);
}
else
{
/* 在给定色彩空间的情况下计算视频标准的尺寸 */
if (BufferGfx_calcDimensions(envp->videoStd, colorSpace, &capDim) < 0)
{
ERR("Failed to calculate Buffer dimensions\n");
cleanup(THREAD_FAILURE);
}
/*
* 捕获驱动程序提供32字节对齐的数据。
* 我们将捕获和视频缓冲区对齐32个字节,以执行零拷贝编码。
*/
capDim.width = Dmai_roundUp(capDim.width,32);
envp->imageWidth = capDim.width;
envp->imageHeight = capDim.height;
}
gfxAttrs.dim.height = capDim.height;
gfxAttrs.dim.width = capDim.width;
gfxAttrs.dim.lineLength = Dmai_roundUp(BufferGfx_calcLineLength(gfxAttrs.dim.width, colorSpace), 32);
gfxAttrs.dim.x = 0;
gfxAttrs.dim.y = 0;
gfxAttrs.colorSpace = colorSpace;
if (colorSpace == ColorSpace_YUV420PSEMI)//根据色彩空间计算缓冲区大小
{
bufSize = (gfxAttrs.dim.lineLength * gfxAttrs.dim.height * 3 / 2);
}
else
{
bufSize = gfxAttrs.dim.lineLength * gfxAttrs.dim.height * 2;
}
numCapBufs = NUM_CAPTURE_BUFS;
/* 创建要与捕获驱动程序一起使用的缓冲区表 */
hBufTab = BufTab_create(numCapBufs, bufSize, BufferGfx_getBufferAttrs(&gfxAttrs));
if (hBufTab == NULL)
{
ERR("Failed to create buftab\n");
cleanup(THREAD_FAILURE);
}
/* 创建一个缓冲区表以用于将Fifo初始化为视频线程 */
hFifoBufTab = BufTab_create(VIDEO_PIPE_SIZE, bufSize, BufferGfx_getBufferAttrs(&gfxAttrs));
if (hFifoBufTab == NULL)
{
ERR("Failed to create buftab\n");
cleanup(THREAD_FAILURE);
}
/* 向主线程报告视频标准和图像大小 */
Rendezvous_meet(envp->hRendezvousCapStd);
/* 创建捕获设备驱动程序实例 */
cAttrs.numBufs = NUM_CAPTURE_BUFS;//捕获缓冲区数量
cAttrs.videoInput = envp->videoInput;//传递视频输入源
cAttrs.videoStd = envp->videoStd;//传递视频标准
cAttrs.colorSpace = colorSpace;//指定颜色空间
cAttrs.captureDimension = &gfxAttrs.dim;
/* 创建捕获设备驱动程序实例 */
hCapture = Capture_create(hBufTab, &cAttrs);
if (hCapture == NULL)
{
ERR("Failed to create capture device. Is video input connected?\n");
cleanup(THREAD_FAILURE);
}
capture_fd = Capture_get_fd(hCapture);
printf("---------------------capture_fd = %d\r\n", capture_fd);
for (bufIdx = 0; bufIdx < VIDEO_PIPE_SIZE; bufIdx++)
{
/* 将视频缓冲区排队以进行主线程处理 */
hBuf = BufTab_getFreeBuf(hFifoBufTab);
if (hBuf == NULL)
{
ERR("Failed to fill video pipeline\n");
cleanup(THREAD_FAILURE);
}
/* 用黑色填充缓冲区 */
CapBuf_blackFill(hBuf);
/* 发送缓冲区到视频线程进行编码 */
if (Fifo_put(envp->hOutFifo, hBuf) < 0)
{
ERR("Failed to send buffer to display thread\n");
cleanup(THREAD_FAILURE);
}
}
/* 发出信号,表明initiaprintf(“获取捕获缓冲区\ n”);的完成已完成并等待其他线程 */
Rendezvous_meet(envp->hRendezvousInit);
while (!gblGetQuit())
{
/* 捕获一帧存到hCapBuf */
if (Capture_get(hCapture, &hCapBuf) < 0)
{
ERR("Failed to get capture buffer\n");
cleanup(THREAD_FAILURE);
}
/* 发送hCapBuf到视频线程进行编码 */
if (Fifo_put(envp->hOutFifo, hCapBuf) < 0)
{
ERR("Failed to send buffer to display thread\n");
cleanup(THREAD_FAILURE);
}
/* 从视频线程获取编码完成的缓冲区hDstBuf */
fifoRet = Fifo_get(envp->hInFifo, &hDstBuf);
if (fifoRet < 0)
{
ERR("Failed to get buffer from video thread\n");
cleanup(THREAD_FAILURE);
}
else if (fifoRet == Dmai_EFLUSH)
{
cleanup(THREAD_SUCCESS);
}
/* 将处理后的缓冲区返回到捕获驱动程序 */
if (Capture_put(hCapture, hDstBuf) < 0)
{
ERR("Failed to put capture buffer\n");
cleanup(THREAD_FAILURE);
}
}
cleanup:
/* 确保其他线程不在等待我们 */
Rendezvous_force(envp->hRendezvousCapStd);
Rendezvous_force(envp->hRendezvousInit);
Pause_off(envp->hPauseProcess);
Fifo_flush(envp->hOutFifo);//清洗捕获线程-->视频编码线程FIFO
/* 在清理之前与其他线程meet */
Rendezvous_meet(envp->hRendezvousCleanup);
/* 退出前清理线程 */
if (hCapture)
{
Capture_delete(hCapture);//销毁捕获器实例
}
if (hBufTab)
{
BufTab_delete(hBufTab); //删除与驱动程序使用的缓冲区表
}
if (hFifoBufTab)
{
BufTab_delete(hFifoBufTab);//删除与编码线程使用的缓冲区表
}
return status;
}
该线程提供图像捕获,并通过管道传递给videoThrFxn()线程。线程分为三大部分:线程初始化、线程循环和线程退出,退出自然是销毁资源然后返回就不用多说了。
初始化的作用是对传入的分辨率进行合法性判断、对其和申请缓冲区空间,并创建捕获设备驱动程序实例。在创建捕获设备实例后可以再创建缩放器,缩放器的相关接口在Dmai文件夹中的resizer.h文件中。然后进入主循环,在全局标志位没有被更改的情况下,反复运行4个操作:
1.从捕获设备实例中取一帧数据存到缓冲区
2.将数据缓冲区通过管道发送到video线程中进行编码
3.从视频线程获取编码完成的缓冲区
4.将处理后的缓冲区返回到捕获驱动程序
另外在第三步操作中判断fifo的状态以判断video线程是否执行了退出操作,若是,则capture线程退出。
video.c视频编码线程
/*
* video.c
*/
#include <xdc/std.h>
#include <ti/sdo/ce/Engine.h>
#include <ti/sdo/dmai/Fifo.h>
#include <ti/sdo/dmai/Pause.h>
#include <ti/sdo/dmai/BufTab.h>
#include <ti/sdo/dmai/VideoStd.h>
#include <ti/sdo/dmai/BufferGfx.h>
#include <ti/sdo/dmai/Rendezvous.h>
#include <ti/sdo/dmai/ce/Venc1.h>
#include <ti/sdo/dmai/Capture.h>
#include "video.h"
#include "demo.h"
/* 捕获线程的管道中的缓冲区数 */
/* 注意:它需要匹配capture.c管道尺寸 */
#define VIDEO_PIPE_SIZE 4
unsigned char idr_trigger = 0;//IDR帧触发变量
void ForceIDR(Venc1_Handle hVe1, VIDENC1_DynamicParams *dynParams, unsigned int idr)
{
Int mystatus;
VIDENC1_Status tEncStatus;
VIDENC1_Handle enghandle = Venc1_getVisaHandle(hVe1);
tEncStatus.size = sizeof(VIDENC1_Status);
tEncStatus.data.buf = NULL;
if(0 == idr)
{
dynParams->forceFrame = IVIDEO_NA_FRAME;
}
else
{
dynParams->forceFrame = IVIDEO_IDR_FRAME;
}
mystatus = VIDENC1_control(enghandle, XDM_SETPARAMS, dynParams, &tEncStatus);
if(mystatus != VIDENC1_EOK)
{
VIDENC1_delete(enghandle);
return;
}
}
/******************************************************************************
* videoThrFxn
******************************************************************************/
Void *videoThrFxn(Void *arg)
{
VideoEnv *envp = (VideoEnv *) arg;//传入给线程的参数结构体
Void *status = THREAD_SUCCESS;//线程状态返回值
Venc1_Handle hVe1 = NULL;//创建的视频编码器句柄
Engine_Handle hEngine = NULL;//打开的编码引擎句柄
BufTab_Handle hBufTab = NULL;//创建的缓冲区表
Buffer_Handle hCapBuf, hDstBuf;//捕获线程缓冲区和写线程缓冲区
VIDENC1_Params params = Venc1_Params_DEFAULT;//编码所需静态参数结构体
VIDENC1_DynamicParams dynParams = Venc1_DynamicParams_DEFAULT;//编码所需动态参数结构体
Int fifoRet; //对FIFO操作的执行返回值
Int bufIdx; //对缓冲区操作的循环计数变量
ColorSpace_Type colorSpace = ColorSpace_YUV420PSEMI;//颜色空间类型定义
BufferGfx_Dimensions dim; //缓冲器Gfx尺寸
/* 打开编码引擎 */
hEngine = Engine_open(envp->engineName, NULL, NULL);
if (hEngine == NULL)
{
ERR("Failed to open codec engine %s\n", envp->engineName);
cleanup(THREAD_FAILURE);
}
//设置编解码器参数。我们将高度取整以适应编解码器的对齐限制。
params.maxWidth = envp->imageWidth; //静态参数——图像尺寸
params.maxHeight = Dmai_roundUp(envp->imageHeight, CODECHEIGHTALIGN);
params.encodingPreset = XDM_HIGH_SPEED; //静态参数——编码预设
if (colorSpace == ColorSpace_YUV420PSEMI)
{
params.inputChromaFormat = XDM_YUV_420SP; //静态参数——输入色度格式
} else
{
params.inputChromaFormat = XDM_YUV_422ILE;
}
params.reconChromaFormat = XDM_YUV_420SP; //静态参数——校正色度格式
params.maxFrameRate = envp->videoFrameRate;//静态参数——最大帧率
/* 根据比特率设置编解码器参数 */
if (envp->videoBitRate < 0) {
/* 可变比特率 */
params.rateControlPreset = IVIDEO_NONE;//速率控制预设
// params.rateControlPreset = IVIDEO_STORAGE;
/*
* 如果可变比特率使用伪造的比特率值(> 0),因为它将被忽略。
*/
// params.maxBitRate = 3000000;
}
else {
/* 恒定比特率 */
params.rateControlPreset = IVIDEO_STORAGE;
params.maxBitRate = 3000000;//envp->videoBitRate;
}
params.maxBitRate = 500000; //静态参数——最大比特率
dynParams.targetBitRate = params.maxBitRate; //动态参数——最大比特率
dynParams.inputWidth = envp->imageWidth; //动态参数——图像宽度
dynParams.inputHeight = envp->imageHeight; //动态参数——图像高度
dynParams.refFrameRate = params.maxFrameRate;//动态参数——参考帧帧率
dynParams.targetFrameRate = params.maxFrameRate;//动态参数——目标帧率
dynParams.interFrameInterval = 0; //动态参数——帧间间隔
/* (根据)创建视频编码器 */
hVe1 = Venc1_create(hEngine, envp->videoEncoder, ¶ms, &dynParams);
if (hVe1 == NULL) {
ERR("Failed to create video encoder: %s\n", envp->videoEncoder);
cleanup(THREAD_FAILURE);
}
/* 将输出缓冲区大小存储在环境中 */
envp->outBufSize = Venc1_getOutBufSize(hVe1);
/* (到此处)表示已创建编解码器并可用输出缓冲区大小 */
Rendezvous_meet(envp->hRendezvousWriter);
/* 【例程注】在高清分辨率的情况下,视频缓冲区将由捕获线程分配。 */
/* 将缓冲区发送到捕获线程以准备进行主循环 */
for (bufIdx = 0; bufIdx < VIDEO_PIPE_SIZE; bufIdx++)
{
fifoRet = Fifo_get(envp->hCaptureOutFifo, &hCapBuf);
if (fifoRet < 0) {
ERR("Failed to get buffer from capture thread\n");
cleanup(THREAD_FAILURE);
}/* 捕获线程是否冲洗了fifo? */
else if (fifoRet == Dmai_EFLUSH) {
cleanup(THREAD_SUCCESS);
}
/* 将缓冲区返回给捕获线程 */
if (Fifo_put(envp->hCaptureInFifo, hCapBuf) < 0) {
ERR("Failed to send buffer to display thread\n");
cleanup(THREAD_FAILURE);
}
}
/* 发出初始化已完成的信号,并等待其他线程 */
Rendezvous_meet(envp->hRendezvousInit);
while (!gblGetQuit())
{
/* 暂停处理? */
Pause_test(envp->hPauseProcess);
/* 从捕获线程获取待编码的帧 */
fifoRet = Fifo_get(envp->hCaptureOutFifo, &hCapBuf);
if (fifoRet < 0)
{
ERR("Failed to get buffer from video thread\n");
cleanup(THREAD_FAILURE);
}
else if (fifoRet == Dmai_EFLUSH)
{
cleanup(THREAD_SUCCESS);
}
/* 从写程序线程获取已处理的帧 */
fifoRet = Fifo_get(envp->hWriterOutFifo, &hDstBuf);
if (fifoRet < 0)
{
ERR("Failed to get buffer from video thread\n");
cleanup(THREAD_FAILURE);
}
else if (fifoRet == Dmai_EFLUSH)
{
cleanup(THREAD_SUCCESS);
}
/* (重置尺寸)确保整个缓冲区都用于输入? */
BufferGfx_resetDimensions(hCapBuf);
/* 确保视频缓冲区具有编解码器可接受的尺寸? */
BufferGfx_getDimensions(hCapBuf, &dim);
dim.height = Dmai_roundUp(dim.height, CODECHEIGHTALIGN);
BufferGfx_setDimensions(hCapBuf, &dim);
//IDR帧触发
if(idr_trigger)
{
ForceIDR(hVe1, &dynParams, 1);
idr_trigger=0;
}
/* 对得到的帧进行视频编码 */
if (Venc1_process(hVe1, hCapBuf, hDstBuf) < 0)
{
ERR("Failed to encode video buffer\n");
cleanup(THREAD_FAILURE);
}
//编码线程中取出编码帧在此处
//unsigned long vidbuflen = Buffer_getNumBytesUsed(hDstBuf);
//unsigned char* sendptr = Buffer_getUserPtr(hDstBuf);
/* 将尺寸重置为原始尺寸? */
BufferGfx_resetDimensions(hCapBuf);
/* 将编码后的帧发送到写线程 */
if (Fifo_put(envp->hWriterInFifo, hDstBuf) < 0)
{
ERR("Failed to send buffer to display thread\n");
cleanup(THREAD_FAILURE);
}
/* 将缓冲区返回给捕获线程 */
if (Fifo_put(envp->hCaptureInFifo, hCapBuf) < 0)
{
ERR("Failed to send buffer to display thread\n");
cleanup(THREAD_FAILURE);
}
}
cleanup:
/* 确保其他线程不在等待我们 */
Rendezvous_force(envp->hRendezvousInit);
Rendezvous_force(envp->hRendezvousWriter);
Pause_off(envp->hPauseProcess);
Fifo_flush(envp->hWriterInFifo);//清洗视频编码线程-->写线程FIFO
Fifo_flush(envp->hCaptureInFifo);//清洗视频编码线程-->捕获线程FIFO
/* 确保其他线程不在等待初始化完成 */
Rendezvous_meet(envp->hRendezvousCleanup);
if (hBufTab)
{
BufTab_delete(hBufTab);//删除缓冲区表
}
if (hVe1)
{
Venc1_delete(hVe1); //销毁编码器实例
}
if (hEngine)
{
Engine_close(hEngine); //关闭编码引擎
}
return status;
}
参数设置部分就不说了,流程的话,大致就是打开编码引擎,根据引擎创建视频编码器实例。这里打开引擎时的 envp->engineName 参数应该是 “encode” ,该参数表明了使用的是编码引擎。创建编码器实例时的 envp->videoEncoder 参数应该是 “h264enc” ,表明创建的是H264编码器。
这里还有一个有意思的地方,原例程中注明了:在高清分辨率的情况下,视频缓冲区将由捕获线程分配 ,我在精简例程的时候确实发现了无论是video还是capture中都有同样的申请缓冲区空间的代码,但是没看出来video中申请的缓冲区是如何交给capture线程并替换掉capture中申请的缓冲区的,此处要是有小伙伴看出来了麻烦告知哈,但是在此我要进行的是1080P的视频编码,所以即便遵照默认,也删去了video线程中的缓冲区申请语句。
此处的for循环,对应了capture线程中线程循环前的操作,这个线程间交互的操作是这样的:capture线程中依次填充申请到的缓冲区并发送给video线程,video线程收到缓冲区后把缓冲区返回给capture线程,所以真相只有一个,该处的作用是防止编程时文件开头 VIDEO_PIPE_SIZE 宏在两文件中设置不同导致的风险。
而后在线程循环过程中调用编码器实例对从capture线程传递的图像进行编码,并将编码后的数据通过FIFO传递给写线程。
writer.c写线程
/*
* writer.c
*/
#include <stdio.h>
#include <xdc/std.h>
#include <ti/sdo/dmai/Fifo.h>
#include <ti/sdo/dmai/BufTab.h>
#include <ti/sdo/dmai/BufferGfx.h>
#include <ti/sdo/dmai/Rendezvous.h>
#include "writer.h"
#include "demo.h"
/* 在写线程管道中的缓冲数量 */
#define NUM_WRITER_BUFS 5//10
/******************************************************************************
* 写线程入口函数
******************************************************************************/
Void *writerThrFxn(Void *arg)
{
WriterEnv *envp = (WriterEnv *) arg;
Void *status = THREAD_SUCCESS;
Buffer_Attrs bAttrs = Buffer_Attrs_DEFAULT;
BufTab_Handle hBufTab = NULL;
Buffer_Handle hOutBuf;
Int fifoRet;
Int bufIdx;
/*
* 创建一个缓冲区表,用于与视频线程之间来回传递缓冲区。
*/
hBufTab = BufTab_create(NUM_WRITER_BUFS, envp->outBufSize, &bAttrs);
if (hBufTab == NULL) {
ERR("Failed to allocate contiguous buffers\n");
cleanup(THREAD_FAILURE);
}
/* 将所有缓冲区发送到视频线程以填充编码数据 */
for (bufIdx = 0; bufIdx < NUM_WRITER_BUFS; bufIdx++)
{
if (Fifo_put(envp->hOutFifo, BufTab_getBuf(hBufTab, bufIdx)) < 0) {
ERR("Failed to send buffer to display thread\n");
cleanup(THREAD_FAILURE);
}
}
/* 发出初始化已完成的信号,并等待其他线程 */
Rendezvous_meet(envp->hRendezvousInit);
while (TRUE) {
/* 从视频线程获取编码缓冲区 */
fifoRet = Fifo_get(envp->hInFifo, &hOutBuf);
if (fifoRet < 0) {
ERR("Failed to get buffer from video thread\n");
cleanup(THREAD_FAILURE);
}
/* 视频线程是否刷新了FIFO? */
else if (fifoRet == Dmai_EFLUSH) {
cleanup(THREAD_SUCCESS);
}
/* 获取编码数据 */
if (Buffer_getNumBytesUsed(hOutBuf))
{
unsigned char *ptr = (unsigned char *)Buffer_getUserPtr(hOutBuf);
unsigned long length = Buffer_getNumBytesUsed(hOutBuf);
//在这里处理一帧的数据
printf("ptr: %p Length:%lu",ptr,length);
}
else {
printf("Warning, writer received 0 byte encoded frame\n");
}
/* 将缓冲区返回给捕获线程 */
if (Fifo_put(envp->hOutFifo, hOutBuf) < 0) {
ERR("Failed to send buffer to display thread\n");
cleanup(THREAD_FAILURE);
}
}
cleanup:
/* 确保其他线程不在等待我们 */
Rendezvous_force(envp->hRendezvousInit);
Pause_off(envp->hPauseProcess);
Fifo_flush(envp->hOutFifo);
/* 在清理之前与其他线程meet */
Rendezvous_meet(envp->hRendezvousCleanup);
/* 在退出前清理线程 */
if (hBufTab) {
BufTab_delete(hBufTab);
}
return status;
}
写线程已经删减的差不多了,注释很清楚了,在注释的位置使用指针和长度就可以对数据任意操作,分析传递都可以,但是注意要尽快将缓冲区返回以免编码那边不够用。
ctrl.c控制线程
这个线程是内容最少的了,当然,是在删去了例程中串口和网络交互的代码之后,基本上想干什么都行。
/*
* ctrl.c
*/
#include <xdc/std.h>
#include <ti/sdo/dmai/Pause.h>
#include <ti/sdo/dmai/Rendezvous.h>
#include "ctrl.h"
#include "demo.h"
/******************************************************************************
* ctrlThrFxn
******************************************************************************/
Void *ctrlThrFxn(Void *arg)
{
CtrlEnv *envp = (CtrlEnv *) arg;
Void *status = THREAD_SUCCESS;
/* 发出初始化已完成的信号,并等待其他线程 */
Rendezvous_meet(envp->hRendezvousInit);
while (!gblGetQuit()) {
//这里接收指令
}
/* 确保其他线程不在等待我们 */
Rendezvous_force(envp->hRendezvousInit);
Pause_off(envp->hPauseProcess);
/* 在清理之前与其他线程会面 */
Rendezvous_meet(envp->hRendezvousCleanup);
return status;
}
你可能会问,之前文章中截图的那些其它文件比如codecs.c去哪儿了?那里面还有很多东西呢!嘿嘿,因为我已经把闲杂文件给删除了呀,是不是很清爽?不是?再见。
删减后的demo.h文件内容
/*
* demo.h
*/
#ifndef _DEMO_H
#define _DEMO_H
/* Linux标准头文件 */
#include <stdio.h>
#include <signal.h>
#include <pthread.h>
/* Error message */
#define ERR(fmt, args...) fprintf(stderr, "Error: " fmt, ## args)
/* 函数错误码 */
#define SUCCESS 0
#define FAILURE -1
/* 线程错误码 */
#define THREAD_SUCCESS (Void *) 0
#define THREAD_FAILURE (Void *) -1
/* 编解码器施加的输入缓冲区高度对齐限制 */
#define CODECHEIGHTALIGN 16
/* 全局数据结构 */
typedef struct GlobalData {
Int quit; /* 全局退出标志*/
pthread_mutex_t mutex; /* 互斥保护全局数据 */
} GlobalData;
#define GBL_DATA_INIT { 0 }
/* 全局变量 */
extern GlobalData gbl;
/* 保护全局数据的函数 */
static inline Int gblGetQuit(void)
{
Int quit;
pthread_mutex_lock(&gbl.mutex);
quit = gbl.quit;
pthread_mutex_unlock(&gbl.mutex);
return quit;
}
static inline Void gblSetQuit(void)
{
pthread_mutex_lock(&gbl.mutex);
gbl.quit = TRUE;
pthread_mutex_unlock(&gbl.mutex);
}
/* Cleans up cleanly after a failure */
#define cleanup(x) \
status = (x); \
gblSetQuit(); \
goto cleanup
#endif /* _DEMO_H */
删减后的ctrl.h文件内容
/*
* ctrl.h
*/
#ifndef _CTRL_H
#define _CTRL_H
#include <xdc/std.h>
#include <ti/sdo/dmai/Pause.h>
#include <ti/sdo/dmai/Rendezvous.h>
/* Environment passed when creating the thread */
typedef struct CtrlEnv {
Rendezvous_Handle hRendezvousInit;
Rendezvous_Handle hRendezvousCleanup;
Pause_Handle hPauseProcess;
Char *engineName;
} CtrlEnv;
/* Thread function prototype */
extern Void *ctrlThrFxn(Void *arg);
#endif /* _CTRL_H */
删减后的capture.h内容
/*
* capture.h
*/
#ifndef _CAPTURE_H
#define _CAPTURE_H
#include <xdc/std.h>
#include <ti/sdo/dmai/Fifo.h> //提供缓冲区交互
#include <ti/sdo/dmai/Pause.h> //提供暂停
#include <ti/sdo/dmai/Rendezvous.h>//提供线程同步
#include <ti/sdo/dmai/VideoStd.h> //提供视频标准参数
/* 创建线程时设置的环境参数 */
typedef struct CaptureEnv {
Rendezvous_Handle hRendezvousInit;
Rendezvous_Handle hRendezvousCapStd;
Rendezvous_Handle hRendezvousCleanup;
Rendezvous_Handle hRendezvousPrime;
Pause_Handle hPauseProcess;
Fifo_Handle hOutFifo; //捕获线程-->视频编码线程FIFO
Fifo_Handle hInFifo; //捕获线程<--视频编码线程FIFO
VideoStd_Type videoStd; //视频标准,用于计算缓冲区大小
Int32 imageWidth; //图像宽度,用于计算缓冲区大小
Int32 imageHeight;//图像高度,用于计算缓冲区大小
Capture_Input videoInput; //视频源选择,用于创建捕获器实例
} CaptureEnv;
/* 线程函数原型 */
extern Void *captureThrFxn(Void *arg);
#endif /* _CAPTURE_H */
删减后的video.h文件内容
/*
* video.h
*/
#ifndef _VIDEO_H
#define _VIDEO_H
#include <xdc/std.h>
#include <ti/sdo/dmai/Fifo.h> //提供缓冲区交互
#include <ti/sdo/dmai/Pause.h> //提供暂停
#include <ti/sdo/dmai/Rendezvous.h>//提供线程同步
/* 创建线程时设置的环境参数 */
typedef struct VideoEnv {
Rendezvous_Handle hRendezvousInit;
Rendezvous_Handle hRendezvousCleanup;
Rendezvous_Handle hRendezvousWriter;
Pause_Handle hPauseProcess;
Fifo_Handle hWriterInFifo; //视频编码线程-->写线程FIFO
Fifo_Handle hWriterOutFifo; //视频编码线程<--写线程FIFO
Fifo_Handle hCaptureInFifo; //视频编码线程-->捕获线程FIFO
Fifo_Handle hCaptureOutFifo;//视频编码线程<--捕获线程FIFO
Char *videoEncoder; //用于创建指定的视频编码器实例"h264enc"
Char *engineName; //引擎名称,用于启动指定类型的引擎"encode"
Int32 outBufSize;
Int videoBitRate; //视频比特率,用于静态参数重设
Int videoFrameRate; //视频(最大)帧率,用于静态参数重设
Int32 imageWidth; //图像宽度,用于申请缓冲区和创建编码器实例
Int32 imageHeight; //图像高度,用于申请缓冲区和创建编码器实例
} VideoEnv;
/* 线程函数原型 */
extern Void *videoThrFxn(Void *arg);
#endif /* _VIDEO_H */
删减后的writer.h文件内容
/*
* writer.h
*/
#ifndef _WRITER_H
#define _WRITER_H
#include <xdc/std.h>
#include <ti/sdo/dmai/Fifo.h> //提供缓冲区交互
#include <ti/sdo/dmai/Pause.h> //提供暂停
#include <ti/sdo/dmai/Rendezvous.h>//提供线程同步
/* 创建线程时设置的环境参数 */
typedef struct WriterEnv {
Rendezvous_Handle hRendezvousInit;
Rendezvous_Handle hRendezvousCleanup;
Pause_Handle hPauseProcess;
Fifo_Handle hOutFifo;
Fifo_Handle hInFifo;
Int32 outBufSize;
} WriterEnv;
/* 线程函数原型 */
extern Void *writerThrFxn(Void *arg);
#endif /* _WRITER_H */
编译
将原来demo中encodets文件夹里的东西,除了 encodets.cfg 和 Makefile 之外其他东西都删了,上述文件存在此文件夹中,./dm365 中所有c/h文件也都删了,这样进入encodets中执行make就可以编译了,上面版本是 编译过 没有问题的。
但是受限当前条件还没实测过,嘿嘿嘿嘿。
————本篇后记————
你说我为何要费这么大劲写这个呢?肯定是 怕我忘了或者存搞丢了, 为了帮助别人呀,因为我在学的时候网上没有这样的帖子嘛。
不过千万别找我,这是我个人的学习笔记性质,要是对谁谁造成了经济损失什么的,我可不负责哈。
你以为这就完了么?才怪,看完例程就完了,学的怕是个锤子哟。
- 分享
- 举报
-
浏览量:834次2023-06-30 10:11:29
-
浏览量:681次2023-06-12 14:34:57
-
浏览量:615次2023-06-12 14:35:02
-
浏览量:1178次2023-06-12 14:34:40
-
浏览量:668次2023-10-23 17:56:00
-
浏览量:1478次2023-11-01 10:56:09
-
浏览量:4948次2018-11-13 10:03:09
-
浏览量:1488次2023-06-12 14:35:30
-
浏览量:987次2023-11-01 11:26:42
-
浏览量:4566次2021-04-27 16:33:22
-
浏览量:2199次2020-05-22 19:32:20
-
浏览量:704次2023-10-30 15:15:38
-
浏览量:5461次2021-04-27 16:33:54
-
2023-06-12 14:35:32
-
浏览量:3090次2018-05-07 16:22:35
-
浏览量:5524次2020-08-20 14:18:11
-
浏览量:1395次2024-02-27 17:03:43
-
浏览量:1303次2024-05-16 12:25:25
-
浏览量:595次2023-10-30 15:19:41
-
广告/SPAM
-
恶意灌水
-
违规内容
-
文不对题
-
重复发帖
Tony
感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~
举报类型
- 内容涉黄/赌/毒
- 内容侵权/抄袭
- 政治相关
- 涉嫌广告
- 侮辱谩骂
- 其他
详细说明