1. ONVIF 프로토콜 이해와 구현
1.1 ONVIF 프로토콜의 개요
ONVIF(Open Network Video Interface Forum)은 네트워크 비디오 장비 간의 상호 운용성을 위한 국제 표준 프로토콜입니다. 2008년 설립 이후, 이 프로토콜은安防监控系统, 원격 모니터링, 스마트 시티 인프라 등 다양한 분야에서 광범위하게 활용되고 있습니다.
ONVIF의 핵심 기능은 다음과 같습니다:
- 장비 자동 발견(Device Discovery)
- 실시간 비디오 스트림 접근
- PTZ(Pan/Tilt/Zoom) 제어
- 사용자 권한 관리
- 이벤트 알림 시스템
1.2 C#으로 ONVIF 클라이언트 개발하기
C#에서 ONVIF 클라이언트를 구현하기 위해서는 SOAP/XML 기반의 통신을 처리해야 합니다. 다음 예제는 네트워크상의 ONVIF 호환 장비를 검색하는 기본 구현입니다:
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace ONVIFCameraClient
{
public class DeviceDiscovery
{
private readonly HttpClient _httpClient;
public DeviceDiscovery()
{
_httpClient = new HttpClient();
_httpClient.Timeout = TimeSpan.FromSeconds(10);
}
public async Task<string> SearchDeviceAsync(string ipAddress)
{
var serviceUrl = $"http://{ipAddress}/onvif/device_service";
var soapRequest = BuildDiscoveryRequest();
var content = new StringContent(
soapRequest,
Encoding.UTF8,
"text/xml"
);
try
{
var response = await _httpClient.PostAsync(serviceUrl, content);
return await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException ex)
{
Console.WriteLine($"연결 오류: {ex.Message}");
return string.Empty;
}
}
private string BuildDiscoveryRequest()
{
return @"
<GetCapabilities xmlns=""http://www.onvif.org/ver10/device/wsdl"">
<Category>All</Category>
</GetCapabilities>
";
}
}
}
1.3 세션 관리 및 오류 처리
장비와의 통신에서 안정적인 세션 관리는 필수적입니다. 다음과 같은 예외 처리 패턴을 적용하세요:
public class ONVIFSession : IDisposable
{
private HttpClient _client;
private string _sessionId;
private bool _disposed;
public async Task<bool> ConnectAsync(string url, string user, string pass)
{
try
{
_client = new HttpClient { BaseAddress = new Uri(url) };
var authResponse = await AuthenticateAsync(user, pass);
_sessionId = ExtractSessionId(authResponse);
return !string.IsNullOrEmpty(_sessionId);
}
catch (Exception ex)
{
Console.WriteLine($"세션 연결 실패: {ex.Message}");
return false;
}
}
public async Task<string> SendCommandAsync(string soapBody)
{
if (string.IsNullOrEmpty(_sessionId))
throw new InvalidOperationException("세션이 연결되지 않았습니다");
try
{
// 명령 전송 로직
return await ExecuteRequestAsync(soapBody);
}
catch (TaskCanceledException)
{
// 타임아웃 처리
throw new TimeoutException("요청 시간이 초과되었습니다");
}
}
public void Dispose()
{
if (!_disposed)
{
_client?.Dispose();
_disposed = true;
}
}
}
2. RTSP 프로토콜의 구조와 구현
2.1 RTSP 프로토콜 기초
RTSP(Real Time Streaming Protocol)는 스트리밍 미디어 서버를 제어하기 위한 네트워크 프로토콜입니다. TCP 또는 UDP(포트 554)를 통해 작동하며, RTP(Real-time Transport Protocol)와 함께 사용되어 실시간 미디어 전송을 실현합니다.
2.2 RTSP 명령어 구현
RTSP는 다양한 제어 명령을 제공합니다:
using System;
using System.Net.Sockets;
using System.Text;
namespace RTSPClient
{
public class RtspProtocolHandler
{
private TcpClient _connection;
private NetworkStream _stream;
private int _sequenceNumber = 1;
private string _sessionIdentifier;
public void Connect(string host, int port)
{
_connection = new TcpClient(host, port);
_stream = _connection.GetStream();
}
public string SendDescribe(string url)
{
var request = $"DESCRIBE {url} RTSP/1.0\r\n" +
$"CSeq: {_sequenceNumber++}\r\n" +
$"Accept: application/sdp\r\n" +
$"\r\n";
return SendRequest(request);
}
public string SendSetup(string url, int trackId)
{
var request = $"SETUP {url}/trackID={trackId} RTSP/1.0\r\n" +
$"CSeq: {_sequenceNumber++}\r\n" +
$"Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n" +
$"\r\n";
var response = SendRequest(request);
ParseSession(response);
return response;
}
public string SendPlay(string url, double startTime = 0)
{
var request = $"PLAY {url} RTSP/1.0\r\n" +
$"CSeq: {_sequenceNumber++}\r\n" +
$"Range: npt={startTime}-\r\n" +
$"Session: {_sessionIdentifier}\r\n" +
$"\r\n";
return SendRequest(request);
}
public string SendPause(string url)
{
var request = $"PAUSE {url} RTSP/1.0\r\n" +
$"CSeq: {_sequenceNumber++}\r\n" +
$"Session: {_sessionIdentifier}\r\n" +
$"\r\n";
return SendRequest(request);
}
private string SendRequest(string request)
{
byte[] data = Encoding.ASCII.GetBytes(request);
_stream.Write(data, 0, data.Length);
byte[] buffer = new byte[4096];
int bytesRead = _stream.Read(buffer, 0, buffer.Length);
return Encoding.ASCII.GetString(buffer, 0, bytesRead);
}
private void ParseSession(string response)
{
var lines = response.Split('\n');
foreach (var line in lines)
{
if (line.StartsWith("Session:"))
{
_sessionIdentifier = line.Split(':')[1].Trim().Split(';')[0];
}
}
}
}
}
3. VLC 플레이어 연동
3.1 .NET에서 VLC 라이브러리 사용
VideoLAN Client(VLC)는 다양한 코덱을 지원하는 강력한 미디어 플레이어입니다. .NET 애플리케이션에서 VLC를 사용하려면 P/Invoke를 통해 네이티브 라이브러리를 호출해야 합니다:
using System;
using System.Runtime.InteropServices;
namespace VideoPlayer
{
public class VlcMediaPlayer : IDisposable
{
private IntPtr _instance;
private IntPtr _media;
private IntPtr _player;
private bool _disposed = false;
[DllImport("libvlc.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr libvlc_new(int argc, IntPtr[] argv);
[DllImport("libvlc.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr libvlc_media_new_path(IntPtr instance, string path);
[DllImport("libvlc.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr libvlc_media_player_new_from_media(IntPtr media);
[DllImport("libvlc.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void libvlc_media_player_play(IntPtr player);
[DllImport("libvlc.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void libvlc_media_player_pause(IntPtr player);
[DllImport("libvlc.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void libvlc_media_player_stop(IntPtr player);
[DllImport("libvlc.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void libvlc_media_player_release(IntPtr player);
[DllImport("libvlc.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void libvlc_media_release(IntPtr media);
[DllImport("libvlc.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void libvlc_release(IntPtr instance);
public bool Initialize(string vlcPath)
{
try
{
string[] arguments = {
"--no-video-title-show",
"--no-snapshot-preview"
};
IntPtr[] argv = new IntPtr[arguments.Length];
for (int i = 0; i < arguments.Length; i++)
{
argv[i] = Marshal.StringToHGlobalAnsi(arguments[i]);
}
_instance = libvlc_new(arguments.Length, argv);
foreach (var arg in argv)
{
Marshal.FreeHGlobal(arg);
}
return _instance != IntPtr.Zero;
}
catch (Exception ex)
{
Console.WriteLine($"VLC 초기화 실패: {ex.Message}");
return false;
}
}
public void LoadMedia(string filePath)
{
if (_instance == IntPtr.Zero)
throw new InvalidOperationException("VLC 인스턴스가 초기화되지 않았습니다");
_media = libvlc_media_new_path(_instance, filePath);
_player = libvlc_media_player_new_from_media(_media);
}
public void Play()
{
if (_player != IntPtr.Zero)
libvlc_media_player_play(_player);
}
public void Pause()
{
if (_player != IntPtr.Zero)
libvlc_media_player_pause(_player);
}
public void Stop()
{
if (_player != IntPtr.Zero)
libvlc_media_player_stop(_player);
}
public void Dispose()
{
if (!_disposed)
{
if (_player != IntPtr.Zero)
{
libvlc_media_player_release(_player);
_player = IntPtr.Zero;
}
if (_media != IntPtr.Zero)
{
libvlc_media_release(_media);
_media = IntPtr.Zero;
}
if (_instance != IntPtr.Zero)
{
libvlc_release(_instance);
_instance = IntPtr.Zero;
}
_disposed = true;
}
}
}
}
4. 비디오 스트림 재생 구현
4.1 RTSP 스트림을 VLC로 전송
네트워크 카메라로부터 수신한 RTSP 스트림을 VLC 플레이어에 연결하여 재생하는 구조입니다:
using System;
namespace CameraStream
{
public class StreamReceiver
{
private RTSPClient.RtspProtocolHandler _rtspClient;
private VideoPlayer.VlcMediaPlayer _player;
private bool _isStreaming;
public void Initialize(string cameraIp, string vlcPath)
{
_rtspClient = new RTSPClient.RtspProtocolHandler();
_rtspClient.Connect(cameraIp, 554);
_player = new VideoPlayer.VlcMediaPlayer();
if (!_player.Initialize(vlcPath))
{
throw new Exception("VLC 플레이어 초기화 실패");
}
}
public void StartStreaming(string streamUrl)
{
if (_isStreaming) return;
// RTSP 세션 설정
_rtspClient.SendDescribe(streamUrl);
_rtspClient.SendSetup(streamUrl, 1);
_rtspClient.SendPlay(streamUrl);
// VLC에 RTSP URL 로드
_player.LoadMedia(streamUrl);
_player.Play();
_isStreaming = true;
}
public void StopStreaming()
{
if (!_isStreaming) return;
_player.Stop();
_isStreaming = false;
}
}
}
4.2 PTZ 제어 구현
ONVIF 프로토콜을 활용한 PTZ 카메라 제어를 위한 구현 예제입니다:
using System;
using System.Threading.Tasks;
namespace PTZControl
{
public class PtzController
{
private readonly ONVIFCameraClient.ONVIFSession _session;
public PtzController(ONVIFCameraClient.ONVIFSession session)
{
_session = session;
}
public async Task MoveUpAsync()
{
await ExecutePtzCommand("ContinuousMove", "PanTilt", 0, 0.5);
}
public async Task MoveDownAsync()
{
await ExecutePtzCommand("ContinuousMove", "PanTilt", 0, -0.5);
}
public async Task MoveLeftAsync()
{
await ExecutePtzCommand("ContinuousMove", "PanTilt", -0.5, 0);
}
public async Task MoveRightAsync()
{
await ExecutePtzCommand("ContinuousMove", "PanTilt", 0.5, 0);
}
public async Task ZoomInAsync()
{
await ExecutePtzCommand("ContinuousMove", "Zoom", 0, 0, 0.5);
}
public async Task ZoomOutAsync()
{
await ExecutePtzCommand("ContinuousMove", "Zoom", 0, 0, -0.5);
}
public async Task StopMovementAsync()
{
var soapBody = @"
<Stop xmlns=""http://www.onvif.org/ver20/ptz/wsdl"">
<ProfileToken>MainProfile</ProfileToken>
<PanTilt>true</PanTilt>
<Zoom>true</Zoom>
</Stop>";
await _session.SendCommandAsync(soapBody);
}
private async Task ExecutePtzCommand(string command, string category,
double x = 0, double y = 0, double z = 0)
{
var soapBody = $@"
<{command} xmlns=""http://www.onvif.org/ver20/ptz/wsdl"">
<ProfileToken>MainProfile</ProfileToken>
<Velocity>
<PanTilt x=""{x}"" y=""{y}"" xmlns=""http://www.onvif.org/xsd""/>
<Zoom x=""{z}"" xmlns=""http://www.onvif.org/xsd""/>
</Velocity>
{command}>";
await _session.SendCommandAsync(soapBody);
}
}
}
5. 네트워크 버퍼링 및 최적화
5.1 버퍼 관리 전략
안정적인 스트리밍 재생을 위해 네트워크 버퍼를 적절히 관리해야 합니다. 버퍼 크기는 네트워크 지연과 대역폭에 따라 동적으로 조정하는 것이 좋습니다:
using System;
using System.Collections.Concurrent;
namespace StreamBuffer
{
public class NetworkBuffer
{
private readonly ConcurrentQueue _buffer;
private readonly int _maxBufferSize;
private readonly object _syncLock = new object();
public NetworkBuffer(int maxSize = 100)
{
_maxBufferSize = maxSize;
_buffer = new ConcurrentQueue();
}
public void AddData(byte[] data)
{
lock (_syncLock)
{
if (_buffer.Count >= _maxBufferSize)
{
// 가장 오래된 데이터 제거
if (_buffer.TryDequeue(out _))
{
Console.WriteLine("버퍼가 가득 찼습니다. 이전 데이터를 제거합니다.");
}
}
_buffer.Enqueue(data);
}
}
public bool TryGetData(out byte[] data)
{
return _buffer.TryDequeue(out data);
}
public int CurrentSize => _buffer.Count;
public bool IsEmpty => _buffer.IsEmpty;
}
}
5.2 스트림 동기화 처리
오디오와 비디오의 동기화를 위해 타임스탬프를 기반으로 프레임을 정렬하는 로직이 필요합니다:
using System;
using System.Collections.Generic;
namespace StreamSync
{
public class FrameSynchronizer
{
private SortedDictionary _videoFrames;
private SortedDictionary _audioFrames;
private long _videoPts;
private long _audioPts;
public void AddVideoFrame(byte[] frameData, long pts)
{
_videoFrames[pts] = frameData;
_videoPts = pts;
}
public void AddAudioFrame(byte[] frameData, long pts)
{
_audioFrames[pts] = frameData;
_audioPts = pts;
}
public (byte[] video, byte[] audio) GetSyncFrames()
{
long targetPts = Math.Min(_videoPts, _audioPts);
_videoFrames.TryGetValue(targetPts, out var videoFrame);
_audioFrames.TryGetValue(targetPts, out var audioFrame);
return (videoFrame, audioFrame);
}
}
}
본 article에서는 C# 환경에서 ONVIF 프로토콜과 RTSP를 활용한 네트워크 비디오 스트리밍 구현 방법을 다루었습니다. VLC 플레이어와의 연동을 통해 다양한 비디오 포맷을 지원하며, PTZ 제어를 통해 원격 카메라 조작이 가능합니다.