V4L2 采用多线程采集视频 , 保存的视频播放卡顿的问题

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了V4L2 采用多线程采集视频 , 保存的视频播放卡顿的问题相关的知识,希望对你有一定的参考价值。

在ARM板子上,采用V4L2控制摄像头采集视频,设置的是采集MJPEG格式的视频,采用多线程边采集,边保存视频。采集到的视频卡顿,检查了好久(包括多线程和V4L2),困扰了好久,都没有解决,
望大神帮看看,问题出在哪里,不胜感激!!

file_fd = fopen("test-mjpeg.mjpeg", "ab+");//图片文件名

1、下面这个是子线程(控制循环采集视频的)
void* snapshot(void *arg)

struct timeval now;
struct timespec outtime;

int ret = 0;

pthread_mutex_lock(&mutex);

while(is_run)
/* 640x480 ~=0.1s */

ret = cap_snapshot();
if(ret < 0)
printf("get a frame data err\n");


gettimeofday(&now, NULL);
outtime.tv_sec = now.tv_sec;
outtime.tv_nsec = now.tv_usec * 1000 + 10 * 1000;

/*当前的时间过10ms后超时 */

pthread_cond_timedwait(&cond, &mutex, &outtime);


pthread_mutex_unlock(&mutex);
pthread_exit(0);


2、下面的这个是在main函数里面的
while(1)


if(stream_buf)


fwrite(stream_buf,1, stream_buf_byteused, file_fd); //保存视频,stream_buf是保存了采集到的视频图像

pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);



3、这是dqbuf和qbuf部分
int cap_snapshot(void)


int ret = 0;

/*****************************/
struct v4l2_buffer buf;

struct pollfd tFds[1];
int iRet;

/* poll */
tFds[0].fd = devfd;
tFds[0].events = POLLIN;

iRet = poll(tFds, 1, -1);
if (iRet <= 0)

perror("poll error!\n");
return -1;


/* 9. dqbuf */
memset(&buf, 0, sizeof(struct v4l2_buffer));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;

ret = ioctl(devfd, VIDIOC_DQBUF, &buf);
if(ret < 0)
perror("Unable to dequeue buffer");
return ret;


stream_buf = (unsigned char *) malloc((size_t)(width * height << 1));
memcpy(stream_buf, mem[buf.index], buf.bytesused);
stream_buf_byteused = buf.bytesused;

/* 10. qbuf */

ret = ioctl(devfd, VIDIOC_QBUF, &buf);
if(ret < 0)
perror("Unable to requeue buffer");
free(stream_buf);
stream_buf = NULL;
return ret;


return 0;

参考技术A 看不懂啊这个

quartz结合多线程处理后台业务

  最近项目中有播放视频的需求,技术选型采用UMS播放器,免费版只能播放FLV格式的视频文件,因此需要对用户上传的视频进行格式转换,转换工具为FormatFactory,功能还是比较强大的。但是面临的一个问题,视频转换是非常耗时的,上传完直接转换是没法接受的,于是决定采用quartz,以任务调度的方式,在后台进行转换,具体步骤如下:

  1.定义一个任务队列,将待转换的视频文件信息放到队列中。采用单例模式,并且考虑到线程安全问题,采用线程安全的Vector作为队列容器:

  

技术分享图片
/**
 * 格式转换任务队列
 * 队列中放的是ResourceInfo类型对象
 * @author Administrator
 *
 */
public class TransformTaskQueue {

    private static TransformTaskQueue instance = null;

    //实际存放转换对象信息的队列,采用线程安全的Vercor容器
    public static Vector<ResourceInfo> taskQueue = new Vector<ResourceInfo>();

    public static TransformTaskQueue getInstance() {
        if (instance == null) {
            instance = new TransformTaskQueue();
        }
        return instance;
    }

    /**
     * 向队列中添加对象
     * @param info
     */
    public static void add(ResourceInfo info) {
        taskQueue.add(info);
    }

    /**
     * 从队列中删除对象
     * @param info
     */
    public static void remove(ResourceInfo info) {
        if(taskQueue.size()>0 && taskQueue.contains(info)){
            taskQueue.remove(info);
        }
    }

}
技术分享图片

  2.用户上传视频文件之后,后台进行判断,如果不是flv格式,则将文件转换所需信息封装到ResuorceInfo对象,将该对象放入待转换队列:

技术分享图片
// 如果源视频文件存在,则进行相应的转换,转换为FLV文件
        if (new File(TransConfig.VIDEO_SOURCE_ROOT + path + fileName).exists()) {

            ResourceInfo info = new ResourceInfo();
            info.setResourceId(resourceId);
            info.setPath(path);
            info.setFileName(fileName);
            info.setStatus(0);
            // 添加到转换队列
            TransformTaskQueue.add(info);

        } else {
            System.out.println("源文件不存在!");
        }
技术分享图片

  3.执行单个具体文件转换的操作类代码如下:

技术分享图片
/**
 * 执行具体转换操作的类,
 * 采用多线程技术,继承了runnable接口
 * @author Administrator
 *
 */
public class TransformExecutor implements Runnable,Serializable{

    private static final long serialVersionUID = 1L;
    
    private ResourceInfo info = null ;
    
    public TransformExecutor(ResourceInfo info){
        this.info = info;
    }

    @Override
    public void run() {
        
        String resourceId = info.getResourceId();
        String path = info.getPath();
        String fileName = info.getFileName();

        String videoFilename = TransConfig.VIDEO_SOURCE_ROOT + path
                + fileName;
        String flvFilename = path
                + FileUtil.getFilePrefix(fileName) + ".flv";

        // 转换成功,修改数据库中的is_transed字段为1
        if (Video2FLVTransfer.transform(videoFilename, flvFilename) == 1) {
            CRUDUtil.update(resourceId, 1);
        }
        // 转换失败,修改数据库中的is_transed字段为2
        else {
            CRUDUtil.update(resourceId, 2);
        }
        
        // 将resourceInfo从转换队列中去除
        TransformTaskQueue.remove(info);
        
    }

}
技术分享图片

  4.下面是开启多线程转换的操作类,采用线程池技术,因为转换视频文件格式工作量比较大,因此规定每次最多开启3个线程:

技术分享图片
/**
 * 转换执行器服务类
 * @author Administrator
 *
 */
public class TransExecutorService {
    
    private final ExecutorService pool;

    private static TransExecutorService instance;
    //线程池大小,即每次最多允许开启几个线程执行转换操作
    private static final int THREAD_SIZE = 3;
    
    public static TransExecutorService getInstance() {
        if (instance == null) {
            instance = new TransExecutorService();
        }
        return instance;
    }

    private TransExecutorService() {
//        pool = Executors.newCachedThreadPool();
        pool = Executors.newFixedThreadPool(THREAD_SIZE);
    }
    

    /**
     * 开启新线程,执行转换操作
     * @param info
     */
    public void execute(ResourceInfo info) {
        try {
            pool.submit(new TransformExecutor(info));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public synchronized void shutdown() {
        pool.shutdown();
    }
}
技术分享图片

  5.调度任务实现类,即每次执行调度,执行的操作

技术分享图片
/**
 * 调度任务具体执行类
 * @author Administrator
 *
 */
public class TransformJob implements Job {

    @Override
    public void execute(JobExecutionContext ctx) throws JobExecutionException {
        
        //获取当前待转换视频文件队列
        Vector<ResourceInfo> infos = TransformTaskQueue.getInstance().taskQueue;
        System.out.println("size:"+infos.size());
        
        //如果任务队列中存在待转换对象,则进行转换
        if (infos.size() > 0) {
            for (int i=0;i<infos.size();i++) {
                //status为0,表示不是正在转换中的
                if (infos.get(i).getStatus() == 0) {
                    infos.get(i).setStatus(1);
                    //新开线程,执行转换操作
                        TransExecutorService.getInstance().execute(infos.get(i));
                }
            }
        }
    }

}
技术分享图片

  6.任务调度管理类,规定了调度执行的一些规则,其中定时表达式请自行网上搜索,这里采用的是每10秒执行一次。

技术分享图片
/**
 * 格式转换任务调度管理类
 * 
 * @author Administrator
 * 
 */
public class SchedulManager {

    private static SchedulManager instance = new SchedulManager();
    private Scheduler scheduler;
    private volatile boolean start = false;

    private SchedulManager() {
        try {
            SchedulerFactory factory = new StdSchedulerFactory();
            scheduler = factory.getScheduler();
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    public static SchedulManager getInstance() {
        return instance;
    }

    /**
     * 开始执行,将加载调度配置并启动每个调度。
     * 
     * @注意: 一般在程序启动时调用该方法。
     */
    public void execute() {
        try {
            // 加载调度配置,并启动每个调度。
            scheduleJobs();
            scheduler.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 加载调度配置并启动每个调度
     * 
     * @注意: TODO
     */
    @SuppressWarnings("static-access")
    private void scheduleJobs() {

        Vector<ResourceInfo> infos = TransformTaskQueue.getInstance().taskQueue;
        System.out.println("size:" + infos.size());
        if (infos.size() > 0) {
            start();
        }
        start = true;
    }

    /**
     * 根据ResourceInfo启动一个调度
     * 
     * @注意: 内部方法,外部不能调用
     * @param ResourceInfo
     *            资源信息
     */
    private void start() {

        try {
            // String id = info.getResourceId();
            // 构造方法中 第一个是任务名称 ,第二个是任务组名,第三个是任务执行的类
            JobDetail jobDetail = new JobDetail("video_trans_id",
                    Scheduler.DEFAULT_GROUP, TransformJob.class);
            String cronExpr = "0/10 * * * * ?";
            String triggerName = "video_trans_trigger";
            Trigger trigger = new CronTrigger(triggerName,
                    Scheduler.DEFAULT_GROUP, cronExpr);

            scheduler.scheduleJob(jobDetail, trigger);

        } catch (Exception e) {
            System.out.println("出错");
            e.printStackTrace();
        }
    }

    public void init() {

        SchedulManager sm = SchedulManager.getInstance();
        // sm.start(info);
        // sm.scheduleJobs();
        sm.start();
        try {
            sm.scheduler.start();
        } catch (SchedulerException e) {
            // TODO 自动生成的 catch 块
            e.printStackTrace();
        }
    }
技术分享图片

  7.通过上述6个步骤,已经可以通过quartz以任务调度的形式来进行格式转换了,接下来的问题,是写一个listener类,以实现在服务器启动的时候,任务调度自动启动。

首先需要在web.xml中加入如下配置:

    <listener> 
        <listener-class>com.yunda.web.EventTransformStartupListener</listener-class> 
    </listener>

之后就是实现配置文件中的实现监听功能的类,非常简单,就是调用SchedulManager中的init()方法即可,代码如下:

技术分享图片
public class EventTransformStartupListener implements ServletContextListener {

    @Override
    public void contextDestroyed(ServletContextEvent arg0) {
    }

    @Override
    public void contextInitialized(ServletContextEvent arg0) {
        
        System.out.println("init...");
        
        SchedulManager sm = SchedulManager.getInstance();
        //sm.start(info);
        sm.init();
    }
}
技术分享图片

  至此,后台进行格式转换的功能全部完成,通过做这个功能,发现quartz采用的任务调度机制,跟linux的crontab差不多,也是采用定时扫描的方法来完成,连定时表达式的规则都长的差不多。先写这么多吧,就是学习了quartz和多线程的简单用法,留个笔记,以便日后深究^_^

以上是关于V4L2 采用多线程采集视频 , 保存的视频播放卡顿的问题的主要内容,如果未能解决你的问题,请参考以下文章

V4L2编程 视频采集

如何 开发 v4l2 tuener驱动

怎么通过rtp协议播放ffmpeg采集到的视频?

求助:Android播放视频文件的图层问题

V4L2学习5--VIVI虚拟摄像头驱动

quartz结合多线程处理后台业务