美文网首页
LINUX(CentOS)下JAVA调用 C++ SO库实现录像

LINUX(CentOS)下JAVA调用 C++ SO库实现录像

作者: 匿名程序员 | 来源:发表于2018-12-10 19:55 被阅读53次

    1.首先获取FFmpeg,并编译
    1.1

    git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg
    

    进入ffmpeg目录:

    cd ffmpeg
    

    1.2编译三部曲:

    ./configure --extra-cflags="-O3 -fPIC" --enable-pic --disable-static --enable-shared
    make -j4
    make install
    

    注意上面的参数-fPIC,--enable-pic。之前没注意。c++调用so库没问题,java死活掉不起来,找不到ffmpeg函数。

    2.C++封装FFmpeg 录像函数
    直接上代码,代码没有怎么整理,有点乱,调试部分代码可以删除。RtspStreamMuxTask 文件代码转载自https://blog.csdn.net/toshiba689/article/details/79426680,且用且珍惜。

    2.1 mux.h

    
    #pragma once
    
    extern "C" {
    
    //开始录像
    //参数:videoSrcAddr 视频源地址,filePath 录像文件路径
    //返回录像句柄,异常返回-1
    int startRecordVideo(char* videoSrcAddr, char* filePath);
    
    //停止录像
    //参数:index录像句柄
    //返回传入的句柄号,异常返回-1
    int stopRecordVideo(int index);
    
    }
    
    

    2.2 mux.cpp

    #include "mux.h"
    #include "RtspStreamMuxTask.h"
    #include "TickCounter.h"
    #include <memory>
    #include <vector>
    #include <map>
    using namespace std;
    #include "dlog2.h"
    
    std::map<int, std::shared_ptr<RtspStreamMuxTask> > muxTasks;
    static volatile int muxTaskNum = 0;
    
    //开始录像
    int startRecordVideo(char* videoSrcAddr, char* filePath)
    {
    Mcgi_log( "%s %s", videoSrcAddr,  filePath);
    TickCounter tc(__FUNCTION__);
    #if 1
    
    std::shared_ptr<RtspStreamMuxTask> mux(new RtspStreamMuxTask);
    //RtspStreamMuxTask mux;
    mux.get()->SetInputUrl(videoSrcAddr);
    mux.get()->SetOutputPath(filePath);
    mux.get()->StartRecvStream();
    muxTasks.insert(make_pair(muxTaskNum, mux)); //<int, std::shared_ptr<RtspStreamMuxTask> >
    
    #else
    
    RtspStreamMuxTask* mux = new RtspStreamMuxTask;
    mux->SetInputUrl(videoSrcAddr);
    mux->SetOutputPath(filePath);
    mux->StartRecvStream();
    muxTasks.insert(make_pair(muxTaskNum++, mux));
    
    #endif
    
    printf("**** video record index: %d\n", muxTaskNum);
    Mcgi_log( "%d %s %s", muxTaskNum, videoSrcAddr, filePath);
    return muxTaskNum++;
    
    }
    
    //停止录像
    
    int stopRecordVideo(int index)
    {
    TickCounter tc(__FUNCTION__);
    printf("111111111  %d\n", muxTasks.count(index));
    
    #if 1
    if (muxTasks.count(index))
    {
    
    printf("2222222222\n");
    muxTasks[index].get()->StopRecvStream();
    muxTasks.erase(index);
    printf("**** video record count: %d\n", muxTasks.size());
    return index;
    }
    
    #else
    
    if (muxTasks.count(index) > 0)
    {
    muxTasks[index]->StopRecvStream();
    delete muxTasks[index];
    
    muxTasks.erase(index);
    printf("**** video record count: %d\n", muxTasks.size());
    return index;
    }
    
    #endif
    return -1;
    }
    

    2.3 RtspStreamMuxTask.h

    #ifndef RtspStreamMuxTask_H
    #define RtspStreamMuxTask_H
    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <errno.h>
    #include <unistd.h>
    #include <iostream>
    using namespace std;
    
    #ifdef __cplusplus 
    extern "C" 
    { 
    #endif 
    #include "libavcodec/avcodec.h"
    #include "libavformat/avformat.h"
    #include "libavfilter/avfilter.h"
    #ifdef __cplusplus 
    }; 
    
    #endif 
    
    class RtspStreamMuxTask
    {
    
    public:
    RtspStreamMuxTask();
    virtual ~RtspStreamMuxTask();
    void  SetInputUrl(string rtspUrl);
    void  SetOutputPath(string outputPath);
    void StartRecvStream();
    void StopRecvStream();
    
    private:
    void run();
    int  OpenInputStream();
    void CloseInputStream();
    void readAndMux();
    static void* ReadingThrd(void * pParam);
    int  openOutputStream();
    void closeOutputStream();
    void ReleaseCodecs();
    void GetVideoSize(long & width, long & height)  //获取视频分辨率
    {
    width  = coded_width;
    height = coded_height;
    }
    
    private:
    string m_inputUrl;
    string m_outputFile;
    
    AVFormatContext* m_inputAVFormatCxt;
    AVBitStreamFilterContext* m_bsfcAAC;
    AVBitStreamFilterContext* m_bsfcH264;
    
    int m_videoStreamIndex;
    int m_audioStreamIndex;
    
    AVFormatContext* m_outputAVFormatCxt;
    char m_tmpErrString[64];
    
    volatile bool m_stop_status;
    pthread_t m_hReadThread;
    
    bool  m_bInputInited;
    bool  m_bOutputInited;
    
    int    coded_width, coded_height;
    int    m_frame_rate;
    };
    
    #endif // RtspStreamMuxTask_H
    
    // https://blog.csdn.net/toshiba689/article/details/79426680
    

    2.4 RtspStreamMuxTask.cpp

    #include "RtspStreamMuxTask.h"
    #include <sstream>
    #include "TickCounter.h"
    #include "dlog2.h"
    
    
    //////////////////////////////////////////////////////////////
    
    RtspStreamMuxTask::RtspStreamMuxTask()
    {
    //dLog( char* dIfomSource, char* dIfomLevel, char* dFmt, ... );
    Mcgi_log( "%s", __FUNCTION__ );
    
        m_stop_status = false;
        m_inputAVFormatCxt = NULL;
        m_bsfcAAC = NULL;
    m_bsfcH264 = NULL;
        m_videoStreamIndex = -1;
    m_audioStreamIndex = -1;
        m_outputAVFormatCxt = NULL;
    m_hReadThread = NULL;
    m_bInputInited = false;
    m_bOutputInited = false;
        coded_width = coded_height = 0;
        m_frame_rate = 25;
    }
    
    RtspStreamMuxTask::~RtspStreamMuxTask()
    {
    StopRecvStream();
    }
    
    void  RtspStreamMuxTask::SetInputUrl(string rtspUrl)
    {
    Mcgi_log( "%s", __FUNCTION__ );
    m_inputUrl = rtspUrl;
    }
    
    void  RtspStreamMuxTask::SetOutputPath(string outputPath)
    {
    
    Mcgi_log( "%s", __FUNCTION__ );
    m_outputFile = outputPath;
    }
    
    void RtspStreamMuxTask::StartRecvStream()
    {
    
    Mcgi_log( "%s", __FUNCTION__ );
    TickCounter tc(__FUNCTION__);
    if(m_inputUrl.empty())
    return;
    
        m_videoStreamIndex = -1;
    m_audioStreamIndex = -1;
    m_bInputInited  = false;
    m_bOutputInited = false;
    coded_width = coded_height = 0;
    int err = pthread_create(&m_hReadThread, NULL, ReadingThrd, this);
    //pthread_detach(m_hReadThread);
    //sleep(1);
    }
    
    void RtspStreamMuxTask::StopRecvStream()
    {
    
    TickCounter tc(__FUNCTION__);
        m_stop_status = true;
        printf("333333333 m_stop_status: %d\n", m_stop_status);
    //sleep(1);
    if (m_hReadThread != NULL)
    {
            //WaitForSingleObject(m_hReadThread, INFINITE);
            void* rval1;
            pthread_join(m_hReadThread, &rval1);
    //CloseHandle(m_hReadThread);
    m_hReadThread = NULL;
    }
        CloseInputStream();
    }
    
    void* RtspStreamMuxTask::ReadingThrd(void * pParam)
    {
    Mcgi_log( "%s", __FUNCTION__ );
    TickCounter tc(__FUNCTION__);
    RtspStreamMuxTask * pTask = (RtspStreamMuxTask *) pParam;
    pTask->run();
    
    return 0;
    }
    
    void RtspStreamMuxTask::run()
    {
    Mcgi_log( "%s", __FUNCTION__ );
    TickCounter tc(__FUNCTION__);
        try
        {
            //m_stop_status = false;
            OpenInputStream();
    openOutputStream();
            //m_stop_status = false;
            readAndMux();
            CloseInputStream();
    closeOutputStream();
        }
        catch(std::exception& e)
        {
    printf("%s \n", e.what());
            CloseInputStream();
        }
    }
    
    int RtspStreamMuxTask::OpenInputStream()
    {
    Mcgi_log( "%s start", __FUNCTION__ );
    TickCounter tc(__FUNCTION__);
        if (m_inputAVFormatCxt)
        {
            string strError  = ("already has input avformat");
    printf("%s \n", strError.c_str());
    Mcgi_log( "%s %s", __FUNCTION__, strError.c_str());
    return -1;
        }
    
    Mcgi_log( "%s 1", __FUNCTION__ );
        m_bsfcAAC = av_bitstream_filter_init("aac_adtstoasc");
        if(!m_bsfcAAC)
        {
            string strError = ("can not create aac_adtstoasc filter");
    printf("%s \n", strError.c_str());
    Mcgi_log( "%s %s", __FUNCTION__, strError.c_str());
    return -1;
        }
    
    Mcgi_log( "%s 2", __FUNCTION__ );
    m_bsfcH264 = av_bitstream_filter_init("h264_mp4toannexb");
        if(!m_bsfcH264)
        {
            string strError = ("can not create h264_mp4toannexb filter");
    printf("%s \n", strError.c_str());
    Mcgi_log( "%s %s", __FUNCTION__, strError.c_str());
    return -1;
        }
    
    ///////////////////////////////////////////////////////////
    Mcgi_log( "%s 3", __FUNCTION__ );
        int res = 0;
    AVDictionary* options = NULL;
    //av_dict_set(&options, "bufsize", "4096000", 0);
    av_dict_set(&options, "rtsp_transport", "tcp", 0);              //采用tcp传输
        res = avformat_open_input(&m_inputAVFormatCxt, m_inputUrl.c_str(), 0, &options);
    Mcgi_log( "%s 4", __FUNCTION__ );
    
        if(res < 0)
        {
            string strError = ("can not open file:" + m_inputUrl + ",errcode:" + to_string(res) + ",err msg:" + av_make_error_string(m_tmpErrString, AV_ERROR_MAX_STRING_SIZE, res));
    printf("%s \n", strError.c_str());
    Mcgi_log( "%s %s", __FUNCTION__, strError.c_str());
    return -1;
        }
    
    Mcgi_log( "%s 5", __FUNCTION__ );
        if (avformat_find_stream_info(m_inputAVFormatCxt, 0) < 0)
        {
            string strError = ("can not find stream info");
    printf("%s \n", strError.c_str());
    Mcgi_log( "%s %s", __FUNCTION__, strError.c_str());
    return -1;
        }
    
    Mcgi_log( "%s 6", __FUNCTION__ );
        av_dump_format(m_inputAVFormatCxt, 0, m_inputUrl.c_str(), 0);
    Mcgi_log( "%s 7", __FUNCTION__ );
        for (int i = 0; i < m_inputAVFormatCxt->nb_streams; i++)
        {
            AVStream *in_stream = m_inputAVFormatCxt->streams[i];
    printf("codec id: %d, URL: %s \n", in_stream->codec->codec_id, m_inputUrl.c_str());
    if (in_stream->codec->codec_type == AVMEDIA_TYPE_VIDEO)
    {
    m_videoStreamIndex = i;
    coded_width = in_stream->codec->width;
    coded_height = in_stream->codec->height;
    if(in_stream->avg_frame_rate.den != 0 && in_stream->avg_frame_rate.num != 0)
    {
    m_frame_rate = in_stream->avg_frame_rate.num/in_stream->avg_frame_rate.den;//每秒多少帧
    }
    printf("video stream index: %d, width: %d, height: %d, FrameRate: %d\n", m_videoStreamIndex, in_stream->codec->width, in_stream->codec->height, m_frame_rate);
    }
    
    else if (in_stream->codec->codec_type == AVMEDIA_TYPE_AUDIO)
    {
    m_audioStreamIndex = i;
    }
        }
    
    m_bInputInited = true;
    Mcgi_log( "%s end", __FUNCTION__ );
    return 0;
    }
    
    void RtspStreamMuxTask::CloseInputStream()
    {
    TickCounter tc(__FUNCTION__);
        if (m_inputAVFormatCxt)
        {
            avformat_close_input(&m_inputAVFormatCxt);
        }
    
        if(m_bsfcAAC)
        {
            av_bitstream_filter_close(m_bsfcAAC);
            m_bsfcAAC = NULL;
        }
    if(m_bsfcH264)
    {
        av_bitstream_filter_close(m_bsfcH264);
            m_bsfcH264 = NULL;
    }
    
    m_bInputInited = false;
    }
    
    int  RtspStreamMuxTask::openOutputStream()
    {
    Mcgi_log( "%s start", __FUNCTION__ );
    TickCounter tc(__FUNCTION__);
        if (m_outputAVFormatCxt)
        {
            printf("already has rtmp avformat \n");
    return -1;
        }
    
        int res = 0;
        if(!m_outputFile.empty())
        {
    res = avformat_alloc_output_context2(&m_outputAVFormatCxt, NULL, NULL, m_outputFile.c_str());
            if (m_outputAVFormatCxt == NULL)
            {
                printf("can not alloc output context \n");
    return -1;
            }
    
            AVOutputFormat* fmt = m_outputAVFormatCxt->oformat;
            //fmt->audio_codec = AV_CODEC_ID_AAC;
            //fmt->video_codec = AV_CODEC_ID_H264;
            for (int i = 0; i < m_inputAVFormatCxt->nb_streams; i++)
            {
                AVStream *in_stream = m_inputAVFormatCxt->streams[i];
                AVStream *out_stream = avformat_new_stream(m_outputAVFormatCxt, in_stream->codec->codec);
                if (!out_stream)
                {
                    printf("can not new out stream");
    return -1;
    
                }
                res = avcodec_copy_context(out_stream->codec, in_stream->codec);
                if (res < 0)
                {
                    string strError = "can not copy context, url: " + m_inputUrl + ",errcode:" + to_string(res) + ",err msg:" + av_make_error_string(m_tmpErrString, AV_ERROR_MAX_STRING_SIZE, res);
    printf("%s \n", strError.c_str());
    return -1;
                }
    
    #define AV_CODEC_FLAG_GLOBAL_HEADER (1 << 22)
    #define CODEC_FLAG_GLOBAL_HEADER AV_CODEC_FLAG_GLOBAL_HEADER
    //#define AVFMT_RAWPICTURE 0x0020
    
                if (m_outputAVFormatCxt->oformat->flags & AVFMT_GLOBALHEADER)
                {
                    out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
                }
            }
    
            av_dump_format(m_outputAVFormatCxt, 0, m_outputFile.c_str(), 1);
            if (!(fmt->flags & AVFMT_NOFILE))
            {
                res = avio_open(&m_outputAVFormatCxt->pb, m_outputFile.c_str(), AVIO_FLAG_WRITE);
                if (res < 0)
                {
    
                    string strError = "can not open output io, file:" + m_outputFile + ",errcode:" + to_string(res) + ", err msg:" + av_make_error_string(m_tmpErrString, AV_ERROR_MAX_STRING_SIZE, res);
    printf("%s \n", strError.c_str());
    Mcgi_log("%s", strError.c_str());
    return -1;
    }
            }
    
    res = avformat_write_header(m_outputAVFormatCxt, NULL);
            if (res < 0)
            {
                string strError = "can not write outputstream header, URL:" + m_outputFile + ",errcode:" + to_string(res) + ", err msg:" + av_make_error_string(m_tmpErrString, AV_ERROR_MAX_STRING_SIZE, res);
    printf("%s \n", strError.c_str());
    m_bOutputInited = false;
    return -1;
            }
    m_bOutputInited = true;
        }
    
        Mcgi_log("%s end", __FUNCTION__);
    return 0;
    }
    
    void RtspStreamMuxTask::closeOutputStream()
    {
    TickCounter tc(__FUNCTION__);
        if (m_outputAVFormatCxt)
        {
            if(m_bOutputInited)
    {
              int res = av_write_trailer(m_outputAVFormatCxt);
    }
            if (!(m_outputAVFormatCxt->oformat->flags & AVFMT_NOFILE))
            {
                if(m_outputAVFormatCxt->pb)
                {
                    avio_close(m_outputAVFormatCxt->pb);
                }
            }
    
            avformat_free_context(m_outputAVFormatCxt);
            m_outputAVFormatCxt = NULL;
        }
    
    m_bOutputInited = false;
    }
    
    void RtspStreamMuxTask::readAndMux()
    {
    
    Mcgi_log( "%s start", __FUNCTION__ );
    TickCounter tc(__FUNCTION__);
    int nVideoFramesNum = 0;
    int64_t  first_pts_time = 0;
    //DWORD start_time = GetTickCount();
    
        AVPacket pkt;
    av_init_packet(&pkt);
        while(!m_stop_status)
        {
            int res;
            res = av_read_frame(m_inputAVFormatCxt, &pkt);
            if (res < 0)  //读取错误或流结束
            {
    if(AVERROR_EOF == res)
    {
    printf("End of file \n");
    }
    
    else
    {
        printf("av_read_frame() got error: %d \n", res);
    }
    break; 
            }
    
            AVStream *in_stream = m_inputAVFormatCxt->streams[pkt.stream_index];
            AVStream *out_stream = m_outputAVFormatCxt->streams[pkt.stream_index];
            //pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
            //pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
            //pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
            pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF);
    
    pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF);
    pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
    
            pkt.pos = -1;
            if(in_stream->codec->codec_type != AVMEDIA_TYPE_VIDEO && in_stream->codec->codec_type != AVMEDIA_TYPE_AUDIO)
    
    {
    continue;
    }
    
    if(in_stream->codec->codec_type == AVMEDIA_TYPE_VIDEO)  //视频
    {
    nVideoFramesNum++;
    // write the compressed frame to the output format
    int nError = av_interleaved_write_frame(m_outputAVFormatCxt, &pkt);
    if (nError != 0)
    {
    char tmpErrString[AV_ERROR_MAX_STRING_SIZE] = {0};
    av_make_error_string(tmpErrString, AV_ERROR_MAX_STRING_SIZE, nError);
    printf("Error: %d while writing video frame, %s\n", nError, tmpErrString);
    }
    
    //int nSecs = pkt.pts*in_stream->time_base.num/in_stream->time_base.den;
    //printf("Frame time: %02d:%02d \n", nSecs/60, nSecs%60);
    }
    
    else if(in_stream->codec->codec_type == AVMEDIA_TYPE_AUDIO) //音频
    {
    
    #if 0
    
    // write the compressed frame to the output format
    int nError = av_interleaved_write_frame(m_outputAVFormatCxt, &pkt);
    if (nError != 0)
    {
    
    char tmpErrString[AV_ERROR_MAX_STRING_SIZE] = {0};
    av_make_error_string(tmpErrString, AV_ERROR_MAX_STRING_SIZE, nError);
    printf("Error: %d while writing audio frame, %s\n", nError, tmpErrString);
    }
    
    #endif
    }
    
    if((in_stream->codec->codec_type == AVMEDIA_TYPE_VIDEO) )
    {
    if(first_pts_time == 0)
        first_pts_time = pkt.pts;
    int64_t pts_time = (pkt.pts - first_pts_time)*1000*in_stream->time_base.num/in_stream->time_base.den; //转成毫秒
    
    //int64_t now_time = GetTickCount() - start_time;
    //if(pts_time > now_time + 10 && pts_time < now_time + 3000)
    //{
    //  Sleep(pts_time-now_time);
    //}
    //else if(pts_time == 0 && nVideoFramesNum > 1)
    //{
    //  Sleep(20);
    //}
    }
    
    av_free_packet(&pkt);
    static int frameId = 0;
    frameId++;
    if (frameId % 10 == 1)
    printf("frameId: %d\n", frameId);
    //TickCounter tc("while");
        }//while
    
    Mcgi_log( "%s end", __FUNCTION__ );
    printf("Reading ended, read %d video frames \n", nVideoFramesNum);
    }
    
    // https://blog.csdn.net/toshiba689/article/details/79426680
    

    2.6 TickCounter.h

    #pragma once
    #include <iostream>
    using namespace std;
    class TickCounter
    {
    public:
    TickCounter(string debug);
    ~TickCounter();
    string debug;
    };
    
    

    2.7 TickCounter.cpp

    #include "TickCounter.h"
    #include <stdio.h>
    TickCounter::TickCounter(string debug)
    {
    printf("%s enter <----\n", debug.c_str());
    this->debug = debug;
    }
    
    TickCounter::~TickCounter()
    {
    printf("%s leave ---->\n", debug.c_str());
    }
    

    2.8 Makefile

    #头文件
    
    #INCLUDE = $(shell pkg-config --cflags opencv)
    INCLUDE =
    INCLUDE += -I.
    #扩展库
    LIBS =
    #LIBS = $(shell pkg-config --libs opencv)
    LIBS += -lpthread -lavformat -lavcodec -lavdevice -lavfilter -lavutil -lswresample
    #-lm -lz
    
    CFLAGS = -O3 -Wall -std=c++11
    #-fPIC -shared
    # 源文件
    SOURCES = RtspStreamMuxTask.cpp TickCounter.cpp  dlog2.cpp mux.cpp
    
    #main.cpp
    # 可执行文件 
    OBJECTS = $(SOURCES:.cpp=.o)
    TARGET = libmux.so
    
    $(TARGET):$(OBJECTS)
    g++ -shared $(CFLAGS) $(OBJECTS) $(LIBS) -o $(TARGET)
    
    #-fPIC
    $(OBJECTS):$(SOURCES)
    g++ -fPIC $(CFLAGS) -c $(SOURCES) $(LIBS)
    #
    
    clean:
    rm $(OBJECTS) $(TARGET)
    
    # 编译规则 $@代表目标文件 $< 代表第一个依赖文件 
    %.o:%.cpp
    
    g++ -fPIC $(CFLAGS) $(INCLUDE) -o $@ -c $<
    

    2.9 JAVA代码

    import com.sun.jna.Library;
    import com.sun.jna.Native;
    public class TestSo
    {
    public interface CMux extends Library {
    // 调用linux下面的so文件,注意,这里只要写mux就可以了,不要写libmux,也不要加后缀
    CMux INSTANCE = (CMux) Native.loadLibrary("mux", CMux.class);
    int startRecordVideo(String videoSrcAddr, String filePath);
    int stopRecordVideo(int index);
    }
    
    public int startRecordVideo(String videoSrcAddr, String filePath){
    return CMux.INSTANCE.startRecordVideo(videoSrcAddr, filePath);
    }
    
    public int stopRecordVideo(int index){
    return CMux.INSTANCE.stopRecordVideo(index);
    }
    
    public static void main(String[] args) {
    TestSo ts = new TestSo();
    String videoSrcAddr = "rtsp://172.28.175.86:8554/channel=0";
    String filePath = "/root/yangxm/work/rec/0.mp4";
    int c = ts.startRecordVideo(videoSrcAddr, filePath);
    try
    {
    Thread.sleep(10000);
    }
    
    catch(Exception e)
    {
    ;
    }
    
    System.out.println(videoSrcAddr + "," + filePath);
    }
    }
    

    2.9.1 编译
    javac TestSo.java

    2.9.2 调用
    java TestSo

    相关文章

      网友评论

          本文标题:LINUX(CentOS)下JAVA调用 C++ SO库实现录像

          本文链接:https://www.haomeiwen.com/subject/fnidhqtx.html