草庐IT

Qt-FFmpeg开发-保存视频流裸流(11)

mahuifa 2023-04-12 原文

音视频/FFmpeg #Qt

Qt-FFmpeg开发-保存视频流裸流?

目录

更多精彩内容
?个人内容分类汇总 ?
?音视频开发 ?

1、概述?

  • 最近研究了一下FFmpeg开发,功能实在是太强大了,网上ffmpeg3、4的文章还是很多的,但是学习嘛,最新的还是不能放过,就选了一个最新的ffmpeg n5.1.2版本,和3、4版本api变化还是挺大的;
  • 在这个Demo里主要使用Qt + FFmpeg开发一个简单的【视频播放器】,支持【保存视频流裸流】功能,这里主要使用的是【软解码】,需要使用硬解码的可以看之前的文章;
  • 同时为了尽可能的简单,这里没有进行音频解码和播放,只是单独的进行视频解码播放;
  • 再日常开发中,经常有将播放的网络视频流图像保存到本地视频文件中的需求,但是如果将图像重新编码保存则会非常消耗CPU资源,裸流数据一般是H264格式的数据,这里其实可以直接将网络视频流未解码的AVPacket直接保存到视频文件中,不需要编码,可大大降低资源占用;
  • 并且直接保存裸流的代码流程不重新编码/转码保存的流程简单许多。

开发环境说明

  • 系统:Windows10、Ubuntu20.04
  • Qt版本:V5.12.5
  • 编译器:MSVC2017-64、GCC/G++64
  • FFmpeg版本:n5.1.2
    • 注意:如果使用了较低版本的库,程序中部分功能可能会存在问题,不会兼容。
    • 官方下载
    • 我使用的库

2、实现效果?

  1. ffmpeg音视频库【软解码】实现的视频播放器;
  2. 支持打开本地视频文件(如mp4、mov、avi等)、网络视频流(rtsp、rtmp、http等);
  3. 支持视频匀速播放;
  4. 采用QPainter进行显示,支持自适应窗口缩放;
  5. 视频播放支持实时开始/关闭、暂停/继续播放;
  6. 视频解码、线程控制、显示各部分功能分离,低耦合度。
  7. 采用最新的5.1.2版本ffmpeg库进行开发,超详细注释信息,将所有踩过的坑、解决办法、注意事项都得很写清楚。
  8. 在使用ffmpeg打开网络视频流时,如果是【h264裸流可以直接保存为本地文件】,不需要进行编码操作。

3、FFmpeg保存裸流代码流程?

  • 白色部分: 主要为打开读取网络视频流、解码流程;
  • 绿色部分: 主要是打开输出文件,将裸流保存到文件的流程。

4、主要代码?

  • 啥也不说了,直接上代码,一切有注释

  • videodecode.h文件

    /******************************************************************************
     * @文件名     videodecode.h
     * @功能       视频解码类,在这个类中调用ffmpeg打开视频进行解码,并且打开输出文件,将h264裸流保存
     *
     * @开发者     mhf
     * @邮箱       1603291350@qq.com
     * @时间       2022/09/15
     * @备注
     *****************************************************************************/
    #ifndef VIDEODECODE_H
    #define VIDEODECODE_H
    
    #include <QString>
    #include <QSize>
    
    struct AVFormatContext;
    struct AVCodecContext;
    struct AVRational;
    struct AVPacket;
    struct AVFrame;
    struct SwsContext;
    struct AVBufferRef;
    struct AVStream;
    class QImage;
    
    class VideoDecode
    {
    public:
        VideoDecode();
        ~VideoDecode();
    
        bool open(const QString& url = QString());    // 打开媒体文件,或者流媒体rtmp、strp、http
        QImage read();                               // 读取视频图像
        void close();                                 // 关闭
        bool isEnd();                                 // 是否读取完成
        const qint64& pts();                          // 获取当前帧显示时间
    
    private:
        void initFFmpeg();                            // 初始化ffmpeg库(整个程序中只需加载一次)
        void showError(int err);                      // 显示ffmpeg执行错误时的错误信息
        qreal rationalToDouble(AVRational* rational); // 将AVRational转换为double
        void clear();                                 // 清空读取缓冲
        void free();                                  // 释放
        bool openSave();                              // 打开输出文件并初始化
    
    private:
        AVFormatContext* m_formatContext = nullptr;   // 解封装上下文
        AVCodecContext*  m_codecContext  = nullptr;   // 解码器上下文
        SwsContext*      m_swsContext    = nullptr;   // 图像转换上下文
        AVPacket* m_packet = nullptr;                 // 数据包
        AVFrame*  m_frame  = nullptr;                 // 解码后的视频帧
        int    m_videoIndex   = 0;                    // 视频流索引
        qint64 m_totalTime    = 0;                    // 视频总时长
        qint64 m_totalFrames  = 0;                    // 视频总帧数
        qint64 m_obtainFrames = 0;                    // 视频当前获取到的帧数
        qint64 m_pts          = 0;                    // 图像帧的显示时间
        qreal  m_frameRate    = 0;                    // 视频帧率
        QSize  m_size;                                // 视频分辨率大小
        char*  m_error = nullptr;                     // 保存异常信息
        bool   m_end = false;                         // 视频读取完成
        uchar* m_buffer = nullptr;                    // YUV图像需要转换位RGBA图像,这里保存转换后的图形数据
    
        /********  保存裸流使用 ******************/
        AVFormatContext* m_formatContextSave = nullptr;  // 封装上下文
        QString m_strCodecName;                          // 编解码器名称
        AVStream* m_videoStream = nullptr;               // 输出视频流
        bool m_writeHeader = false;                      // 是否写入文件头
    };
    
    #endif // VIDEODECODE_H
    
    
  • videodecode.cpp文件

    #include "videodecode.h"
    #include <QDebug>
    #include <QDir>
    #include <QImage>
    #include <QMutex>
    #include <qdatetime.h>
    
    
    extern "C" {        // 用C规则编译指定的代码
    #include "libavcodec/avcodec.h"
    #include "libavformat/avformat.h"
    #include "libavutil/avutil.h"
    #include "libswscale/swscale.h"
    #include "libavutil/imgutils.h"
    
    }
    
    #define ERROR_LEN 1024  // 异常信息数组长度
    #define PRINT_LOG 1
    
    VideoDecode::VideoDecode()
    {
    //    initFFmpeg();      // 5.1.2版本不需要调用了
    
        m_error = new char[ERROR_LEN];
    }
    
    VideoDecode::~VideoDecode()
    {
        close();
    }
    
    /**
     * @brief 初始化ffmpeg库(整个程序中只需加载一次)
     *        旧版本的ffmpeg需要注册各种文件格式、解复用器、对网络库进行全局初始化。
     *        在新版本的ffmpeg中纷纷弃用了,不需要注册了
     */
    void VideoDecode::initFFmpeg()
    {
        static bool isFirst = true;
        static QMutex mutex;
        QMutexLocker locker(&mutex);
        if(isFirst)
        {
            //        av_register_all();         // 已经从源码中删除
            /**
             * 初始化网络库,用于打开网络流媒体,此函数仅用于解决旧GnuTLS或OpenSSL库的线程安全问题。
             * 一旦删除对旧GnuTLS和OpenSSL库的支持,此函数将被弃用,并且此函数不再有任何用途。
             */
            avformat_network_init();
            isFirst = false;
        }
    }
    
    /**
     * @brief      打开媒体文件,或者流媒体,例如rtmp、strp、http
     * @param url  视频地址
     * @return     true:成功  false:失败
     */
    bool VideoDecode::open(const QString &url)
    {
        if(url.isNull()) return false;
    
        AVDictionary* dict = nullptr;
        av_dict_set(&dict, "rtsp_transport", "tcp", 0);      // 设置rtsp流使用tcp打开,如果打开失败错误信息为【Error number -135 occurred】可以切换(UDP、tcp、udp_multicast、http),比如vlc推流就需要使用udp打开
    //    av_dict_set(&dict, "max_delay", "3", 0);             // 设置最大复用或解复用延迟(以微秒为单位)。当通过【UDP】 接收数据时,解复用器尝试重新排序接收到的数据包(因为它们可能无序到达,或者数据包可能完全丢失)。这可以通过将最大解复用延迟设置为零(通过max_delayAVFormatContext 字段)来禁用。
    //    av_dict_set(&dict, "timeout", "1000000", 0);         // 以微秒为单位设置套接字 TCP I/O 超时,如果等待时间过短,也可能会还没连接就返回了。
    
        // 打开输入流并返回解封装上下文
        int ret = avformat_open_input(&m_formatContext,          // 返回解封装上下文
                                      url.toStdString().data(),  // 打开视频地址
                                      nullptr,                   // 如果非null,此参数强制使用特定的输入格式。自动选择解封装器(文件格式)
                                      &dict);                    // 参数设置
        // 释放参数字典
        if(dict)
        {
            av_dict_free(&dict);
        }
        // 打开视频失败
        if(ret < 0)
        {
            showError(ret);
            free();
            return false;
        }
    
        // 读取媒体文件的数据包以获取流信息。
        ret = avformat_find_stream_info(m_formatContext, nullptr);
        if(ret < 0)
        {
            showError(ret);
            free();
            return false;
        }
        m_totalTime = m_formatContext->duration / (AV_TIME_BASE / 1000); // 计算视频总时长(毫秒)
    #if PRINT_LOG
        qDebug() << QString("视频总时长:%1 ms,[%2]").arg(m_totalTime).arg(QTime::fromMSecsSinceStartOfDay(int(m_totalTime)).toString("HH:mm:ss zzz"));
    #endif
    
        // 通过AVMediaType枚举查询视频流ID(也可以通过遍历查找),最后一个参数无用
        m_videoIndex = av_find_best_stream(m_formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
        if(m_videoIndex < 0)
        {
            showError(m_videoIndex);
            free();
            return false;
        }
    
        AVStream* videoStream = m_formatContext->streams[m_videoIndex];  // 通过查询到的索引获取视频流
    
        // 获取视频图像分辨率(AVStream中的AVCodecContext在新版本中弃用,改为使用AVCodecParameters)
        m_size.setWidth(videoStream->codecpar->width);
        m_size.setHeight(videoStream->codecpar->height);
        m_frameRate = rationalToDouble(&videoStream->avg_frame_rate);  // 视频帧率
    
        // 通过解码器ID获取视频解码器(新版本返回值必须使用const)
        const AVCodec* codec = avcodec_find_decoder(videoStream->codecpar->codec_id);
        m_totalFrames = videoStream->nb_frames;
        m_strCodecName = codec->name;
    
    #if PRINT_LOG
        qDebug() << QString("分辨率:[w:%1,h:%2] 帧率:%3  总帧数:%4  解码器:%5")
                    .arg(m_size.width()).arg(m_size.height()).arg(m_frameRate).arg(m_totalFrames).arg(codec->name);
    #endif
    
        // 分配AVCodecContext并将其字段设置为默认值。
        m_codecContext = avcodec_alloc_context3(codec);
        if(!m_codecContext)
        {
    #if PRINT_LOG
            qWarning() << "创建视频解码器上下文失败!";
    #endif
            free();
            return false;
        }
    
        // 使用视频流的codecpar为解码器上下文赋值
        ret = avcodec_parameters_to_context(m_codecContext, videoStream->codecpar);
        if(ret < 0)
        {
            showError(ret);
            free();
            return false;
        }
    
        m_codecContext->flags2 |= AV_CODEC_FLAG2_FAST;    // 允许不符合规范的加速技巧。
        m_codecContext->thread_count = 8;                 // 使用8线程解码
    
        // 初始化解码器上下文,如果之前avcodec_alloc_context3传入了解码器,这里设置NULL就可以
        ret = avcodec_open2(m_codecContext, nullptr, nullptr);
        if(ret < 0)
        {
            showError(ret);
            free();
            return false;
        }
    
        // 分配AVPacket并将其字段设置为默认值。
        m_packet = av_packet_alloc();
        if(!m_packet)
        {
    #if PRINT_LOG
            qWarning() << "av_packet_alloc() Error!";
    #endif
            free();
            return false;
        }
        // 分配AVFrame并将其字段设置为默认值。
        m_frame = av_frame_alloc();
        if(!m_frame)
        {
    #if PRINT_LOG
            qWarning() << "av_frame_alloc() Error!";
    #endif
            free();
            return false;
        }
    
        // 分配图像空间
        int size = av_image_get_buffer_size(AV_PIX_FMT_RGBA, m_size.width(), m_size.height(), 4);
        /**
         * 【注意:】这里可以多分配一些,否则如果只是安装size分配,大部分视频图像数据拷贝没有问题,
         *         但是少部分视频图像在使用sws_scale()拷贝时会超出数组长度,在使用使用msvc debug模式时delete[] m_buffer会报错(HEAP CORRUPTION DETECTED: after Normal block(#32215) at 0x000001AC442830370.CRT delected that the application wrote to memory after end of heap buffer)
         *         特别是这个视频流http://vfx.mtime.cn/Video/2019/02/04/mp4/190204084208765161.mp4
         */
        m_buffer = new uchar[size + 1000];    // 这里多分配1000个字节就基本不会出现拷贝超出的情况了,反正不缺这点内存
    //    m_image = new QImage(m_buffer, m_size.width(), m_size.height(), QImage::Format_RGBA8888);  // 这种方式分配内存大部分情况下也可以,但是因为存在拷贝超出数组的情况,delete时也会报错
        m_end = false;
    
        return openSave();
    }
    
    /**
     * @brief
     * @return
     */
    QImage VideoDecode::read()
    {
        // 如果没有打开则返回
        if(!m_formatContext)
        {
            return QImage();
        }
    
        // 读取下一帧数据
        int readRet = av_read_frame(m_formatContext, m_packet);
        if(readRet < 0)
        {
            avcodec_send_packet(m_codecContext, m_packet); // 读取完成后向解码器中传如空AVPacket,否则无法读取出最后几帧
        }
        else
        {
            if(m_packet->stream_index == m_videoIndex)     // 如果是图像数据则进行解码
            {
                if(m_formatContextSave)
                {
                    // 由于保存的m_formatContextSave只创建了一个视频流,而读取到的图像的流索引不一定为0,可能会出现错误【Invalid packet stream index: 1】
                    // 所以这里需要将stream_index指定为和m_formatContextSave中视频流索引相同,因为就一个流,所以直接设置为0
                    m_packet->stream_index = 0;
                    av_write_frame(m_formatContextSave, m_packet);   // 将数据包写入输出媒体文件
                }
                // 计算当前帧时间(毫秒)
    #if 1       // 方法一:适用于所有场景,但是存在一定误差
                m_packet->pts = qRound64(m_packet->pts * (1000 * rationalToDouble(&m_formatContext->streams[m_videoIndex]->time_base)));
                m_packet->dts = qRound64(m_packet->dts * (1000 * rationalToDouble(&m_formatContext->streams[m_videoIndex]->time_base)));
    #else       // 方法二:适用于播放本地视频文件,计算每一帧时间较准,但是由于网络视频流无法获取总帧数,所以无法适用
                m_obtainFrames++;
                m_packet->pts = qRound64(m_obtainFrames * (qreal(m_totalTime) / m_totalFrames));
    #endif
                // 将读取到的原始数据包传入解码器
                int ret = avcodec_send_packet(m_codecContext, m_packet);
                if(ret < 0)
                {
                    showError(ret);
                }
            }
        }
        av_packet_unref(m_packet);  // 释放数据包,引用计数-1,为0时释放空间
    
        int ret = avcodec_receive_frame(m_codecContext, m_frame);
        if(ret < 0)
        {
            av_frame_unref(m_frame);
            if(readRet < 0)
            {
                m_end = true;     // 当无法读取到AVPacket并且解码器中也没有数据时表示读取完成
            }
            return QImage();
        }
    
        m_pts = m_frame->pts;
    
        // 为什么图像转换上下文要放在这里初始化呢,是因为m_frame->format,如果使用硬件解码,解码出来的图像格式和m_codecContext->pix_fmt的图像格式不一样,就会导致无法转换为QImage
        if(!m_swsContext)
        {
            // 获取缓存的图像转换上下文。首先校验参数是否一致,如果校验不通过就释放资源;然后判断上下文是否存在,如果存在直接复用,如不存在进行分配、初始化操作
            m_swsContext = sws_getCachedContext(m_swsContext,
                                                m_frame->width,                     // 输入图像的宽度
                                                m_frame->height,                    // 输入图像的高度
                                                (AVPixelFormat)m_frame->format,     // 输入图像的像素格式
                                                m_size.width(),                     // 输出图像的宽度
                                                m_size.height(),                    // 输出图像的高度
                                                AV_PIX_FMT_RGBA,                    // 输出图像的像素格式
                                                SWS_BILINEAR,                       // 选择缩放算法(只有当输入输出图像大小不同时有效),一般选择SWS_FAST_BILINEAR
                                                nullptr,                            // 输入图像的滤波器信息, 若不需要传NULL
                                                nullptr,                            // 输出图像的滤波器信息, 若不需要传NULL
                                                nullptr);                          // 特定缩放算法需要的参数(?),默认为NULL
            if(!m_swsContext)
            {
    #if PRINT_LOG
                qWarning() << "sws_getCachedContext() Error!";
    #endif
                free();
                return QImage();
            }
        }
    
        // AVFrame转QImage
        uchar* data[]  = {m_buffer};
        int    lines[4];
        av_image_fill_linesizes(lines, AV_PIX_FMT_RGBA, m_frame->width);  // 使用像素格式pix_fmt和宽度填充图像的平面线条大小。
        ret = sws_scale(m_swsContext,             // 缩放上下文
                        m_frame->data,            // 原图像数组
                        m_frame->linesize,        // 包含源图像每个平面步幅的数组
                        0,                        // 开始位置
                        m_frame->height,          // 行数
                        data,                     // 目标图像数组
                        lines);                   // 包含目标图像每个平面的步幅的数组
        QImage image(m_buffer, m_frame->width, m_frame->height, QImage::Format_RGBA8888);
        av_frame_unref(m_frame);
    
        return image;
    }
    
    /**
     * @brief 关闭视频播放并释放内存
     */
    void VideoDecode::close()
    {
        clear();
        free();
    
        m_totalTime     = 0;
        m_videoIndex    = 0;
        m_totalFrames   = 0;
        m_obtainFrames  = 0;
        m_pts           = 0;
        m_frameRate     = 0;
        m_size          = QSize(0, 0);
    }
    
    /**
     * @brief  视频是否读取完成
     * @return
     */
    bool VideoDecode::isEnd()
    {
        return m_end;
    }
    
    /**
     * @brief    返回当前帧图像播放时间
     * @return
     */
    const qint64 &VideoDecode::pts()
    {
        return m_pts;
    }
    
    /**
     * @brief        显示ffmpeg函数调用异常信息
     * @param err
     */
    void VideoDecode::showError(int err)
    {
    #if PRINT_LOG
        memset(m_error, 0, ERROR_LEN);        // 将数组置零
        av_strerror(err, m_error, ERROR_LEN);
        qWarning() << "DecodeVideo Error:" << m_error;
    #else
        Q_UNUSED(err)
    #endif
    }
    
    /**
     * @brief          将AVRational转换为double,用于计算帧率
     * @param rational
     * @return
     */
    qreal VideoDecode::rationalToDouble(AVRational* rational)
    {
        qreal frameRate = (rational->den == 0) ? 0 : (qreal(rational->num) / rational->den);
        return frameRate;
    }
    
    /**
     * @brief 清空读取缓冲
     */
    void VideoDecode::clear()
    {
        if(m_formatContextSave && m_writeHeader)
        {
            av_write_trailer(m_formatContextSave);   // 写入文件尾
            m_writeHeader = false;
    
            avformat_free_context(m_formatContextSave);
            m_formatContext = nullptr;
            m_videoStream = nullptr;
        }
        // 因为avformat_flush不会刷新AVIOContext (s->pb)。如果有必要,在调用此函数之前调用avio_flush(s->pb)。
        if(m_formatContext && m_formatContext->pb)
        {
            avio_flush(m_formatContext->pb);
        }
        if(m_formatContext)
        {
            avformat_flush(m_formatContext);   // 清理读取缓冲
        }
    }
    
    void VideoDecode::free()
    {
        // 释放上下文swsContext。
        if(m_swsContext)
        {
            sws_freeContext(m_swsContext);
            m_swsContext = nullptr;             // sws_freeContext不会把上下文置NULL
        }
        // 释放编解码器上下文和与之相关的所有内容,并将NULL写入提供的指针
        if(m_codecContext)
        {
            avcodec_free_context(&m_codecContext);
        }
        // 关闭并失败m_formatContext,并将指针置为null
        if(m_formatContext)
        {
            avformat_close_input(&m_formatContext);
        }
        if(m_packet)
        {
            av_packet_free(&m_packet);
        }
        if(m_frame)
        {
            av_frame_free(&m_frame);
        }
        if(m_buffer)
        {
            delete [] m_buffer;
            m_buffer = nullptr;
        }
    }
    
    /**
     * @brief  打开输出文件
     * @return
     */
    bool VideoDecode::openSave()
    {
        QDir dir;
        if(!dir.exists("./Videos"))
        {
            dir.mkdir("./Videos");
        }
        QString strName = QString("./Videos/%1.h264").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd HH-mm-ss"));
        int ret = avformat_alloc_output_context2(&m_formatContextSave, nullptr, m_strCodecName.toStdString().data(), strName.toStdString().data());  // 这里使用和解码一样的编码器,防止保存的图像颜色出问题
    
        if(ret < 0)
        {
            free();
            showError(ret);
            return false;
        }
        // 创建并初始化AVIOContext以访问url所指示的资源。
        ret = avio_open(&m_formatContextSave->pb, strName.toStdString().data(), AVIO_FLAG_WRITE);
        if(ret < 0)
        {
            free();
            showError(ret);
            return false;
        }
    
        // 向媒体文件添加新流
        m_videoStream = avformat_new_stream(m_formatContextSave, nullptr);
        if(!m_videoStream)
        {
            free();
            showError(AVERROR(ENOMEM));
            return false;
        }
    
        //拷贝一些参数,给codecpar赋值(这里使用编码器上下文进行赋值)
        ret = avcodec_parameters_from_context(m_videoStream->codecpar, m_codecContext);
        if(ret < 0)
        {
            free();
            showError(ret);
            return false;
        }
    
        // 写入文件头
        ret = avformat_write_header(m_formatContextSave, nullptr);
        if(ret < 0)
        {
            free();
            showError(ret);
            return false;
        }
        m_writeHeader = true;
    
        qDebug() << "开始录制视频!";
        return true;
    }
    
    

5、完整源代码?

          ??  ☁️
         ???
☁️     ????
       ????
  ☁️    ⁣???
           |/
           ?   ☁️
  ☁️         ☁️

??????_????

有关Qt-FFmpeg开发-保存视频流裸流(11)的更多相关文章

  1. ruby - 使用 C 扩展开发 ruby​​gem 时,如何使用 Rspec 在本地进行测试? - 2

    我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当

  2. Ruby Sinatra 配置用于生产和开发 - 2

    我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm

  3. ruby-on-rails - Ruby 检查日期时间是否为 iso8601 并保存 - 2

    我需要检查DateTime是否采用有效的ISO8601格式。喜欢:#iso8601?我检查了ruby​​是否有特定方法,但没有找到。目前我正在使用date.iso8601==date来检查这个。有什么好的方法吗?编辑解释我的环境,并改变问题的范围。因此,我的项目将使用jsapiFullCalendar,这就是我需要iso8601字符串格式的原因。我想知道更好或正确的方法是什么,以正确的格式将日期保存在数据库中,或者让ActiveRecord完成它们的工作并在我需要时间信息时对其进行操作。 最佳答案 我不太明白你的问题。我假设您想检查

  4. ruby - 是否可以覆盖 gemfile 进行本地开发? - 2

    我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI

  5. ruby - 在 Windows 机器上使用 Ruby 进行开发是否会适得其反? - 2

    这似乎非常适得其反,因为太多的gem会在window上破裂。我一直在处理很多mysql和ruby​​-mysqlgem问题(gem本身发生段错误,一个名为UnixSocket的类显然在Windows机器上不能正常工作,等等)。我只是在浪费时间吗?我应该转向不同的脚本语言吗? 最佳答案 我在Windows上使用Ruby的经验很少,但是当我开始使用Ruby时,我是在Windows上,我的总体印象是它不是Windows原生系统。因此,在主要使用Windows多年之后,开始使用Ruby促使我切换回原来的系统Unix,这次是Linux。Rub

  6. ruby-on-rails - 在 Rails 开发环境中为 .ogv 文件设置 Mime 类型 - 2

    我正在玩HTML5视频并且在ERB中有以下片段:mp4视频从在我的开发环境中运行的服务器很好地流式传输到chrome。然而firefox显示带有海报图像的视频播放器,但带有一个大X。问题似乎是mongrel不确定ogv扩展的mime类型,并且只返回text/plain,如curl所示:$curl-Ihttp://0.0.0.0:3000/pr6.ogvHTTP/1.1200OKConnection:closeDate:Mon,19Apr201012:33:50GMTLast-Modified:Sun,18Apr201012:46:07GMTContent-Type:text/plain

  7. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  8. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  9. ruby - 安装libv8(3.11.8.13)出错,Bundler无法继续 - 2

    运行bundleinstall后出现此错误:Gem::Package::FormatError:nometadatafoundin/Users/jeanosorio/.rvm/gems/ruby-1.9.3-p286/cache/libv8-3.11.8.13-x86_64-darwin-12.gemAnerroroccurredwhileinstallinglibv8(3.11.8.13),andBundlercannotcontinue.Makesurethat`geminstalllibv8-v'3.11.8.13'`succeedsbeforebundling.我试试gemin

  10. Qt Designer的简单使用 - 2

    在前面两节的例子中,主界面窗口的尺寸和标签控件显示的矩形区域等,都是用C++代码编写的。窗口和控件的尺寸都是预估的,控件如果多起来,那就不好估计每个控件合适的位置和大小了。用C++代码编写图形界面的问题就是不直观,因此Qt项目开发了专门的可视化图形界面编辑器——QtDesigner(Qt设计师)。通过QtDesigner就可以很方便地创建图形界面文件*.ui,然后将ui文件应用到源代码里面,做到“所见即所得”,大大方便了图形界面的设计。本节就演示一下QtDesigner的简单使用,学习拖拽控件和设置控件属性,并将ui文件应用到Qt程序代码里。使用QtDesigner设计界面在开始菜单中找到「Q

随机推荐