LoginSignup
3
2

More than 5 years have passed since last update.

How to get Mjpeg data(JPG) from Sony's network camera in C.

Last updated at Posted at 2019-02-24

ネットワークカメラからMjpegファイルを取得する Getting Mjpeg files from IP Camera.

このプログラムは、ネットワークカメラの動画をMjpegで取得しウィンドウに表示させるものです。

ネットワークカメラは、その内部にサーバーを持っており、一般のHTTPサーバーと同じようにGET/POSTメソッドで画像ファイルを得たりカメラのセッティング、例えばシャッタースピードの変更を行ったりします。

カメラのメーカにより、要求先アドレスが違います。

ここではSONY製ネットワークカメラを例にCのソケットプログラミングでカメラサーバーからJpgファイルを取得します。

別のメーカーのカメラで使う場合、HTTPコマンド一覧をメーカーサイトからPDFで取得します。Axisは確認しましたがPanasonicのコマンドを見つけることができませんでした。

コマンド一覧がない場合、WiresharkなどでHTTPリクエスト文を確認するしかありません。

作成準備とコードのポイント

プログラム作成環境

  • Windows10
  • Visual Studio 2017
  • c/c++ console application

プロジェクトを作成したら、Cppファイルを作成します。
IPカメラはルーターにつなげて、あらかじめブラウザーなどで映像が映るか確認しておきます。

次に、コーディングに移ります。

最初にインクルードファイルを記述します。

ソケット通信の基本のヘッダーです。

main.cpp

#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>

#pragma comment(lib, "Ws2_32.lib")

int main() {
  return 0;
}

今回のプログラムではwindows.hを使用します。その場合、次のような形になります。

main.cpp
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif

#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#include <stdio.h>

#pragma comment(lib, "Ws2_32.lib")

int main() {
  return 0;
}

ソケット通信のプログラムでWindows.hが最初のインクルードにありますが、これではwinsock2.hとインクルードファイルの衝突が起きてエラーが出ますので、次のラインを先頭に置きます。ソケット通信でwindows.hを使わない場合は、この限りではありません。

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif

次に、このプログラムは最初、コマンドライン上で動くソケットプログラムのつもりで書いたのですが、ウィンドウ枠内で動画として表示させるためにプロシージャーを記述してあります。

int main(){return 0;}の中にgetNetworkCameraMjpegDataスレッドファンクションの内容を記述してもjpgファイルは取得できます。その場合、画像を表示することができません。

純粋なCのWINDOWSプログラムでjpgファイルを表示するためINT WINAPI WinMainを記述します。以下が基本形です。

INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpCmdLine, INT nCmdShow){

return 0;
}

このWinMainのなかにwindowを表示させるコードを記述します。
これで画像を表示させるウィンドウができます。

プロシージャーをその次に記述し、ウィンドウからのメッセージ処理のコードを書いています。WinMainとLRESULT CALLBACK WindowProcがセットになります。

プロシージャーではスイッチで分岐してそれぞれのメッセージを処理しています。今回のプログラムでは以下の3つを処理します。

  • WS_CREATEでウィンドウ内にメニューバーとメニューを作成します。

  • WS_COMMANDではメニューからのメッセージを取得し、それぞれその処理を書きます。Start,Stop,Quitの3つしかメニューはありませんが。

  • WS_DESTROYは、ウインドウの×ボタンを押したときの処理です。

次にワーカースレッドを作成します。

Mjpegは連続してjpgファイルを送信してきますのでその処理はループ処理になるはずです。したがって、スレッドを作成し処理させます。

プログラム中、関数DWORD WINAPI getNetworkCameraMjpegData(LPVOID lpParam){}とありますが、これがワーカースレッドとなる関数です。

このプログラムではここでHWND引数を渡す必要があります。引数欄にlpParamしか置いていけないので、下の例のようにstruct構造体内に引用したい型を記述して、関数内で構造体を初期化します。

プロシージャのスレッド作成のところの第四引数にこの構造体のポインターを置きます。CreateThread(NULL, 0, getNetworkCameraMjpegData, &pArgs, 0, &dwThredId);


struct ARGS{
    HWND hw;
};

DWORD WINAPI getNetworkCameraMjpegData(LPVOID lpParam){

    // Creating arugument struct for a worker thread.
    ARGS *pArgs = (ARGS*)lpParam;

    // 8, Setting size of window, retrieved Mjpeg streaming size.
    SetWindowPos(pArgs->hw, HWND_TOP, 0, 0, iWidh, iHeight, SWP_SHOWWINDOW);

}

// Thread creating and start function. 
CreateThread(NULL, 0, getNetworkCameraMjpegData, &pArgs, 0, &dwThredId);

最後に、ワーカースレッド内にソケット通信のコードを書きます。

ソケットを確立し、ホスト(カメラ)に接続したら、コマンドを送信します。

まず、Mjpegにおける画像サイズを得ます。得られた情報でウィンドウのサイズを変更します。

Sony製IPカメラのパラメーター情報をリクエストするコマンドは次のようになります。

Request
GET /command/inquiry.cgi?inq=camera HTTP/1.1\r\n
HOST:  <Camera Host address>\r\n\r\n

この時、手持ちのIPカメラSNC CH-210の場合、以下のレスポンスが返ります。

Response
HTTP/1.1 200 OK

Access-Control-Allow-Origin: *
Content-Type: text/plain
Content-Length: 1563
Pragma: no-cache
Cache-control: no-cache
Date: Thu, 01 Jan 2009 00:01:07 GMT
Server: gen5th/1.80.00

VidCapSize=1920,1080&ImageCodec=jpeg&JpImageSize=1280,0&M4AreaSelect=off&JpAreaSelect=off&AreaSelect=off&AreaSet=321,181,1600,900&JpFrameRate=12&JpQuality=7&JpTargetRatio=20&JpBandwidth=0.0&ImageMaxSize=1920,1080&ImageCodecMaxNum=2&ImageCodec1=jpeg&ImageSize1=1280,800&FrameRate1=12&BitRate1=2048&AutoRateCtrl1=off&IFrameInterval1=1&IFrameRatio1=30&Quality1=7&TargetRatio1=20&Bandwidth1=0.0&ImageCodec2=off&ImageSize2=640,480&FrameRate2=30&BitRate2=1024&AutoRateCtrl2=off&IFrameInterval2=1&IFrameRatio2=30&Quality2=7&TargetRatio2=20&Bandwidth2=0.0&VideoStd=ntsc&WBMode=atw&RGain=1e&BGain=1e&HighResoRGain=1e8&HighResoBGain=1e7&AutoShutter=on&BLComp=off&VideoNoiseReduction=off&AutoSlowShutter=on&AutoSlowShutterMinSpeed=5&Shutter=5&AutoShutterMaxSpeed=14&AutoShutterMinSpeed=5&AgcMaxGain=5&Agc=on&ExpComp=6&Gamma=0&Brightness=5&Saturation=3&Sharpness=3&Contrast=3&PtzfMode=normal&RelPanTilt=1&RelZoom=1&EflipFunc=1&Eflip=off&SolidPTZ=off&SolidPTZ1=off&SolidPTZ2=off&SolidPTZMode=quality&SolidPTZTiltDirection=up&Multicast=off&McAddress=239.192.0.200&McAddress1=239.192.0.200&McAddress2=239.192.0.200&RTSPMcAddress=239.192.0.200&McVideoPort=60000&McVideoPort1=60000&McVideoPort2=62000&RTSPMcVideoPort1=61000&RTSPMcVideoPort2=63000&McTtl=3&McVideoAutomode1=off&McVideoAutomode2=off&UcVideoPort=50000&UcVideoPort1=50000&UcVideoPort2=52000&RTSPUcVideoPort1=51000&RTSPUcVideoPort2=53000&RtpExpire=60000&RTSPPort=554&RTSPoverHTTPPort=8080&RTSPTimeout=0&RTPMJPEGExtnHeader=off&RTPMJPEGOffset=on&RTPInfo=on&JpMode=0000005d&M4Mode=0000005d&DayNightMode=manual&DayNight=off

機種により、レスポンスの細かな部分に違いがあるかもしれないが、ソニーであればほぼ同じ体裁で返ってきます。

ウィンドウの枠のサイズの情報はImageSize1=<width,height>で得られることがわかります。これをInt型に変換して、SetWindowPosファンクションで枠のサイズを変更します。

SetWindowPos(pArgs->hw, HWND_TOP, 0, 0, iWidh, iHeight, SWP_SHOWWINDOW);

次にMjepgモードで任意のフレームレートの画像を要求します。

ソニーの場合、次のようにリクエストします。

Request
GET /mjpeg?speed=10 HTTP/1.1\r\n
HOST: 192.168.100.45\r\n\r\n

speedとはフレームレートのことです。mjpegで毎秒10フレームの画像を送れという意味になります。HOSTはカメラのアドレスになります。

レスポンスは連続して送られてきます。ソニーのネットワークカメラでは多くの機種で以下のレスポンスが返ります。ソニーのエンジニア用PDFから転載します。

HTTP/1.0 200 OK
Content-Type: multipart/x-mixed-replace;boundary=--myboundary

--myboundary
Content-Type: image/jpeg
CamTim: 2004-05-18 Tue 10:13:05

<JPEG image data>
--myboundary
Content-Type: image/jpeg
CamTim: 2004-05-18 Tue 10:13:05

<JPEG image data>
--myboundary
Content-Type: image/jpeg
CamTim: 2004-05-18 Tue 10:13:06

<JPEG image data>
--myboundary

--myboundaryは境界線を表す表記で、なぜかAxis製カメラでもこの記述が使われています。これを区切りとして、画像データーが連続で送られてきます。

この場合、recvファンクションをループさせて使いデーターを受け取ります。上記の例ではjpgファイルのサイズが記されていませんが、本来DataLen のような記述があるはずです。これがそのファイルサイズを意味します。これもint型に変換してファイルとして保存する際に使います。

jpgイメージのデータ部はレスポンスヘッダーの後の\r\n\r\nの後に来ます。ポインターをその位置までもっていき、残りのバッファー部がjpgデーターとなります。あとはjpgファイルとして保存するだけです。

データー保存にはファイルポインターを使いますが、もちろんjpgデーターに文字列の終端文字\0が入らないようにバイナリーモードで作成します。fopen_s()関数の第3引数"wb"がその部分です。

jpgファイルができたら画像を表示させます。この方法は悩みましたが、結局CImageクラスを使うことにしました。インクルードファイルにatlimage.hがありますが、そのためです。このクラスの利用にはMFC関連のライブラリーをVS2017にあらかじめ取り込んでおく必要があります。

jpgファイルの構造がいまいち理解できてないので、MFCでよく使うCImageで楽な道を選択しました。これなら5行で表示できます。

このスレッドはメニューにあるStart Mjpegを選択すると実行されます。コードにあるように実行したときに、イベントハンドルを作成していますが、形式的にコード内に入れているだけです。

コード内の細かい部分の説明は省きますが、改良点、間違いがあれば連絡ください。

インターネット上で探してもネットワークカメラの画像取得部分のサンプルがないため、一般的なMjpeg取得方法とは違うかもしれません。また、マイクロソフトのライブラリ、Wininet.hとMFCを使えばより簡単に作れると思います。ただ、カメラメーカーの多くはWEBブラウザーで画像を表示させるのが普通のようです。

追記:printfとoutputdebugstringAが混在していますが、意味はありません。作成途中使用した残り物です。

Socket programming in C. Enjoy it!

How to retrieve Mjpeg files from a network camera and show images.
How it works. Let's get started.

main.cpp

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif

/*/
If an #include line is needed for the Windows.h header file, this should be preceded 
with the #define WIN32_LEAN_AND_MEAN macro. For historical reasons, the Windows.h header 
defaults to including the Winsock.h header file for Windows Sockets 1.1.
The declarations in the Winsock.h header file will conflict with the declarations
in the Winsock2.h header file required by Windows Sockets 2.0. The WIN32_LEAN_AND_MEAN
macro prevents the Winsock.h from being included by the Windows.h header.
/*/

#include <windows.h>
#include <stdio.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#include <stdlib.h>
#include <atlimage.h> // for CImage class. Using atlimage.h, ATL/MFC library must be added.

#pragma comment(lib, "Ws2_32.lib")

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
DWORD WINAPI getNetworkCameraMjpegData(LPVOID lpParam);
int retrieveMjpeg(HWND hWindow);

INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpCmdLine, INT nCmdShow){

    const wchar_t CLASS_NAME[] = L"MJPEG Window class";

    WNDCLASS wc = { };

    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.lpszClassName = CLASS_NAME;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.style = CS_HREDRAW | CS_VREDRAW | CS_GLOBALCLASS;
    wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
    RegisterClass(&wc);

    HWND hwn = CreateWindowEx(
        0,                              // Optional window styles.
        CLASS_NAME,                     // Window class
        L"Network camera view",         // Window text
        WS_OVERLAPPEDWINDOW | WS_VISIBLE,            // Window style

        // Size and position
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,

        NULL,       // Parent window    
        NULL,       // Menu
        hInstance,  // Instance handle
        NULL        // Additional application data
    );

    if (hwn == NULL)
    {
        return 1;
    }

    ShowWindow(hwn, nCmdShow);

    MSG msg;
    while (TRUE) {
        GetMessage(&msg, NULL, 0, 0);
        DispatchMessage(&msg);
    }

    return 0;

}

struct  ARGS // for arguments of thread function.
// This is passed by void pointer so it can be any data type
// that can be passed using a single void pointer (LPVOID).
{
    HWND hw;
};

// Create a handle to the thread event.
HANDLE hThreadEvent;

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    // Create thread argument struct
    ARGS pArgs;
    pArgs.hw = hwnd;

    //Create a handle to the thread. In this case, creating one thread.
    HANDLE hThreadArray[1];

    int ret = 0;
    switch (uMsg)
    {
    case WM_CREATE:
    {
        // Creating menubar and menu.
        HMENU hMenubar;
        HMENU hMenu;

        hMenubar = CreateMenu();
        hMenu = CreateMenu();

        AppendMenuW(hMenu, MF_STRING, 10001, L"&Start Mjpeg");
        AppendMenuW(hMenu, MF_STRING, 10002, L"&Stop");
        AppendMenuW(hMenu, MF_SEPARATOR, 0, NULL);
        AppendMenuW(hMenu, MF_STRING, 10003, L"&Quit");

        AppendMenuW(hMenubar, MF_POPUP, (UINT_PTR)hMenu, L"&Select Action");
        SetMenu(hwnd, hMenubar);

        break;
    }
    case WM_COMMAND:
    {
        switch (LOWORD(wParam)) {

        case 10001:

            DWORD dwThredId;

            hThreadEvent = CreateEvent(
                NULL,               // default security attributes
                TRUE,               // manual-reset event
                FALSE,              // initial state is nonsignaled
                TEXT("Thread Event")  // object name
            );

            hThreadArray[0] = CreateThread(NULL, 0, getNetworkCameraMjpegData, &pArgs, 0, &dwThredId);
            if (hThreadArray[0] == NULL)
            {
                ExitProcess(3);
            }

            SetEvent(hThreadEvent);

            break;
        case 10002:
            CloseHandle(hThreadEvent);
            hThreadEvent = NULL;
            break;
        case 10003:
            if (hThreadEvent != NULL) {
                CloseHandle(hThreadEvent);
            }
            PostQuitMessage(0);
            exit(0);

        default:
            break;

        }

        break;
    }
    case WM_DESTROY:
    {   

        CloseHandle(hThreadEvent);
        PostQuitMessage(0);
        exit(0);

    }

    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

DWORD WINAPI getNetworkCameraMjpegData(LPVOID lpParam){

    // Creating arugument struct for a worker thread.
    ARGS *pArgs = (ARGS*)lpParam;
    DWORD dwWaitResult;

    // Setting ip address and port.
    int iRes = 0;
    char ipAddress[] = "192.168.100.45"; // ip address of a network camera.
    int iPort = 80; // port value of a network camera.

    // 1, The WSAStartup function is called to initiate use of WS2_32.dll.
    WSAData wsData;
    iRes = WSAStartup(MAKEWORD(2, 2), &wsData);
    if (iRes != 0) {
        printf("WSAStartup failed.Error Value : %d", iRes);
        Sleep(2000);
        return 1;
    }

    // 2, Declare an sockaddr_in object that contains a sockaddr structure and initialize these values. 
    // Resolve the server address and port
    sockaddr_in cameraServ;
    cameraServ.sin_addr.S_un.S_addr = inet_addr(ipAddress);
    cameraServ.sin_family = AF_INET;
    cameraServ.sin_port = htons(iPort);

    // 3, Create a SOCKET object called iSocket.
    SOCKET iSocket = INVALID_SOCKET;
    iSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (iSocket == INVALID_SOCKET) {

        printf("Error at socket(): %ld\n", WSAGetLastError());
        WSACleanup(); // WSACleanup is used to terminate the use of the WS2_32 DLL.
        Sleep(2000);
        return 1;
    }

    // 4, Call the connect function, to connect server.
    iRes = connect(iSocket, (sockaddr*)&cameraServ, sizeof(cameraServ));
    if (iRes == SOCKET_ERROR) {

        printf("Unable connect to server. error value : %d\n", WSAGetLastError());
        WSACleanup();
        Sleep(2000);
        return 1;
    }

    ////// The information specified in the sockaddr structure includes: /////////////////////
    // 1, the IP address of the server that the client will try to connect to.
    // 2, the port number on the server that the client will connect to.
    //    This port was specified as port 80 when the client called the getaddrinfo function.


    printf("connected server %s\n", ipAddress);
    Sleep(2000);

    // 5, Sending and Receiving Data on the Client

    char getMjpeg[] = "GET /mjpeg?speed=10 HTTP/1.1\r\n";
    char host[] = "HOST: 192.168.100.45\r\n\r\n";
    char getCameraInfo[] = "GET /command/inquiry.cgi?inq=camera HTTP/1.1\r\n";
    int iMethod = sizeof(getMjpeg) - 1;
    int iHost = sizeof(host) - 1;
    char recvBuf[500000];

    ZeroMemory(recvBuf, sizeof(recvBuf));

    // 7, Inquiry to camera infomation. Getting image size.
    iRes = send(iSocket, getCameraInfo, sizeof(getCameraInfo) - 1, 0);
    if (iRes == SOCKET_ERROR) {
        printf("socket error\n");
        Sleep(2000);
        return 1;
    }
    iRes = send(iSocket, host, iHost, 0);
    if (iRes == SOCKET_ERROR) {
        printf("socket error\n");
        Sleep(2000);
        return 1;
    }
    iRes = recv(iSocket, recvBuf, sizeof(recvBuf), 0);
    if (iRes == SOCKET_ERROR) {
        printf("recv error value : %d\n", WSAGetLastError());
        Sleep(2000);
        return 1;
    }

    char* pInq = NULL;
    char* pSizeWidth = NULL;
    char* pSizeHeight = NULL;
    char* tokenSize = NULL;

    pInq = strstr(recvBuf, "ImageSize1="); // "ImageSize1=", its depend on each camera server.
    pInq += strlen("ImageSize1=");
    pSizeWidth = strtok_s(pInq, ",", &tokenSize);
    pSizeHeight = strtok_s(NULL, "&", &tokenSize);
    int iWidh = atoi(pSizeWidth);
    int iHeight = atoi(pSizeHeight);

    // 8, Setting size of window, retrieved Mjpeg streaming size.
    SetWindowPos(pArgs->hw, HWND_TOP, 0, 0, iWidh, iHeight, SWP_SHOWWINDOW);

    // 9, Sending Get-method message to a network camera.
    iRes = send(iSocket, getMjpeg, iMethod, 0);
    if (iRes == SOCKET_ERROR) {
        printf("socket error\n");
        Sleep(2000);
        return 1;
    }
    printf("%d bytes data send.\n", iRes);

    // Also, Sending host address and the end mark as "\r\n\r\n".
    iRes = send(iSocket, host, iHost, 0);
    if (iRes == SOCKET_ERROR) {
        printf("socket error\n");
        Sleep(2000);
        return 1;
    }

    printf("%d bytes data send.\n", iRes);

    // 10, Create char pointer to get http header and the buffer of jpg-image data.
    char *jpgData = NULL;
    char* dataLen = NULL;
    char* token = NULL;
    char* nextToken = NULL;

    do {

        // 11, Setting for the event of thread's end.
        dwWaitResult = WaitForSingleObject(
            hThreadEvent, // event handle
            INFINITE      // indefinite wait
        );

        switch (dwWaitResult)
        {

        case WAIT_OBJECT_0:
            // Event object was signaled. Do loop.
            break;
        default:
            // Out of signal. End loop.
            OutputDebugStringA("End event.\n");

            closesocket(iSocket);
            WSACleanup();

            return 0;
        }

        // Looping recv function.
        // 12,  Recieving buffer including HTTP header and jpeg raw image data from server.
        ZeroMemory(recvBuf, sizeof(recvBuf));
        iRes = recv(iSocket, recvBuf, sizeof(recvBuf), 0);
        if (iRes == SOCKET_ERROR) {
            printf("recv error value : %d\n", WSAGetLastError());
            Sleep(2000);
            return 1;
        }

        //Check the HTTP header, if you need.
        //printf("%s\n", recvBuf);

        // 13, Getting image length from recieved HTTP header string.

        dataLen = strstr(recvBuf, "DataLen: ");
        if (dataLen != NULL) {

            // dataLen = strstr(recvBuf, "Content-Length: ");
            // dataLen += strlen("Content-Length: ");
            // 14, Converting char type data-length to integer.
            dataLen += strlen("DataLen: ");
            dataLen = strtok_s(dataLen, "\r\n", &nextToken);
            int iDataLength = atoi(dataLen);

            // HTTP header ends "\r\n\r\n"
            // 15, So, searching the end of Header and incrementing 4 bytes. 
            jpgData = strstr(nextToken, "\r\n\r\n");
            if (jpgData != NULL) {
                jpgData += strlen("\r\n\r\n");

                // 16, Copying the data to the jpeg file by BYTE.
                // Open a file in binary (untranslated) mode.
                // Because, translations involving carriage-return and linefeed characters are suppressed.
                FILE* fp;
                fopen_s(&fp, "./data.jpg", "wb");
                fwrite(jpgData, sizeof(BYTE), iDataLength, fp);
                fclose(fp);

                retrieveMjpeg(pArgs->hw);

            }
        }


    } while (iRes > 0);

    // Closing socket and quit the use of WS2_32.dll.
    closesocket(iSocket);
    WSACleanup();

    Sleep(2000);

    return 0;
}

int retrieveMjpeg(HWND hWindow) {

    HDC hdc = GetDC(hWindow);

    CImage mjpeg;

    mjpeg.Load(_T("./data.jpg"));
    mjpeg.BitBlt(hdc, 0, 0, SRCCOPY);
    DeleteDC(hdc);

    return 0;
}


3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2