Самописная мозаика (система мониторинга IPTV)
В данной статье я приведу рабочий пример реализации системы мозаичного мониторинга ТВ-каналов - это когда много ТВ каналов одновременно в маленьких окошках выводятся на один большой монитор.
В данной статье я приведу рабочий пример реализации системы мозаичного мониторинга ТВ-каналов - это когда много ТВ каналов одновременно в маленьких окошках выводятся на один большой монитор.
Профессиональные системы подобного типа стоят бешеных денег (но они правда того стоят - как правило они не просто делают такую мозаичную картинку - они дают еще и много нужной и важной аналитической информации о потоках, о качестве, потерях, ошибках и т.п. Но иногда достаточно просто смотреть сразу несколько каналов :) и решить подобную задачу подручными средствами и бесплатно - вполне реально.
Я решил использовать для этого старый добрый Delphi 7 и VLC, точнее библиотеку для libvlc.dll (которая идет в составе дистрибутива VLC player'а) и интерфейс для этой dll-ки - PasLibVlc. На момент написания статьи проект располагался по адресу http://sourceforge.net/projects/paslibvlc/ . Этот интерфейс дает доступ к функциям из библиотеки libvlc.dll который и позволил мне соорудить свой, на самом деле, по сути своей, мозаичный плеер, с зачаточными функциями сбора статистики. Моя программа работает в двух режимах - первый режим - если в плейлисте количество каналов совпадает с количеством установленных экранов, она статично отображает на экране заданные каналы и при этом позволяет посмотреть накопленную статистику по кадрам для одного из каналов. В случае каких либо ошибок - обрамляет красным канал с ошибками. И второй режим - в заданном количестве окошек она пробегает по кругу по всему заданному плейлисту. Плейлист задается как обычный .m3u
Ну пришло время кода :), и немножко с комментариями. В конце статьи будет ссылка на архив где будут все исходники и бинарник программы (разумеется необходима еще установленная библиотека libvlc.dll)
основной модуль unit1.pas Download file Unit1.pas
unit Unit1; interface uses Windows, Messages, SysUtils, StrUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, PasLibVlcPlayerUnit, PasLibVlcUnit; type TForm1 = class(TForm) Timer1: TTimer; Panel1: TPanel; Label1: TLabel; Timer2: TTimer; Edit1: TEdit; Label2: TLabel; Button1: TButton; Label3: TLabel; Label4: TLabel; Label5: TLabel; Label6: TLabel; Label7: TLabel; Label8: TLabel; Label9: TLabel; Label10: TLabel; Label11: TLabel; Label12: TLabel; Label13: TLabel; Label14: TLabel; Label15: TLabel; Label16: TLabel; Label17: TLabel; Label18: TLabel; procedure FormCreate(Sender: TObject); procedure Timer1Timer(Sender: TObject); procedure Timer2Timer(Sender: TObject); procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; State_on: boolean= false; VlcPlayer: array[1..40] of TPasLibVlcPlayer; VP_x,VP_y,VP_n, Pause_switch, Num_Active: integer; TV_URLs, TV_Names: TStrings; Timer_first_finish: boolean; URL_Count, Last_URL, Last_MNTR, First_URL: integer; mntr: array[1..40] of record active: boolean; m_x,m_y: integer; url: string; end; urls: array[1..250] of record mntr_idx: integer; url: string; name: string; end; vlc_stats, prev_vlc_stats: array[1..40] of libvlc_media_stats_t; implementation uses unit2; {$R *.dfm} procedure ParseM3UPlayList(M3U: string; var Names: TStrings; var URLs: TStrings); var TMPstr: TStrings; i: integer; s: string; begin TMPstr := TStringList.Create; TMPstr.NameValueSeparator := ','; if ( M3U[1]=#$ef) and (M3U[2]=#$bb) and (M3U[3]=#$bf) then TMPstr.Text := UTF8ToAnsi(M3U) else TMPstr.Text := M3U; Names.Clear; URLs.Clear; for i := 0 to TMPstr.Count - 1 do begin s := TMPstr.Strings[i]; if s = '' then continue; if AnsiContainsText(s,'#extm3u') then continue; if not AnsiContainsText(s,'#extinf') then begin URLs.Append(s); end else begin Names.Append(Trim(TMPstr.ValueFromIndex[i])); end; end; end; procedure TForm1.FormCreate(Sender: TObject); var i, j: integer; TmpStr: TStrings; begin VP_n := 0; Form2 := TForm2.Create(Self); if Form2.ShowModal = mrOk then begin VP_x := StrToInt(Form2.Edit1.Text); VP_y := StrToInt(Form2.Edit2.Text); Num_Active := VP_y * VP_x; if Num_Active > 40 then begin ShowMessage('Слишком много (максимум 40) мониторов: '+IntToStr(Num_Active)); Halt; end; ShowMessage('Кол-во активных мониторов принудительно установлено: '+IntToStr(Num_Active)); Pause_Switch := StrToInt(Form2.Edit5.Text); for i := 1 to VP_y do begin for j := 1 to VP_x do begin inc(VP_n); VlcPlayer[VP_n] := TPasLibVlcPlayer.CreateParented(Form1.Handle); VlcPlayer[VP_n].Parent := Form1; VlcPlayer[VP_n].Top := 5+245*(i-1); VlcPlayer[VP_n].Left := 5+325*(j-1); VlcPlayer[VP_n].Visible := true; mntr[VP_n].m_x := j; mntr[VP_n].m_y := i; end; end; ClientWidth := VP_x * 325 - 5; ClientHeight := VP_y * 245 + 50; TV_URLs := TStringList.Create; TV_Names := TStringList.Create; TmpStr := TStringList.Create; TmpStr.Clear; if FileExists(Form2.Edit4.Text) then begin TmpStr.LoadFromFile(Form2.Edit4.Text); TV_URLs.Clear; TV_Names.Clear; ParseM3UPlayList(TmpStr.Text, TV_Names, TV_URLs); end else begin ShowMessage('Плейлист не найден :('); Halt; end; end; URL_Count := TV_URLs.Count; Last_URL := 0; First_URL := 0; Last_MNTR := 0; for i := 1 to URL_Count do begin urls[i].mntr_idx := 0; urls[i].url := TV_URLs.Strings[i-1]; urls[i].name := TV_Names.Strings[i-1]; end; Timer1.Enabled := true; Timer1.Interval := 1000*Pause_switch; Timer_first_finish := false; Timer1Timer(Sender); for i := 1 to VP_n do begin mntr[i].url := ''; mntr[i].active := false; end end; procedure TForm1.Timer1Timer(Sender: TObject); var i, num_a: integer; begin if Last_MNTR > 0 then begin Form1.Canvas.Pen.Color := clBtnFace; Form1.Canvas.Pen.Width := 5; Form1.Canvas.Rectangle(VlcPlayer[Last_MNTR].Left - 3, VlcPlayer[Last_MNTR].Top - 3,VlcPlayer[Last_MNTR].Left + 323, VlcPlayer[Last_MNTR].Top + 243); end; // предполагаем что урлов больше чем мониторов if URL_Count > VP_n then begin num_a := 0; // проходим по всем урлам - проверяем запущены ли все у нас мониторы или еще нет! for i := 1 to VP_n do if mntr[i].active then inc(num_a); // активных мониторов меньше ограничения - ищем первый свободный и запускаем там канал Last_URL+1 if num_a < Num_Active then begin inc(Last_MNTR); if Last_MNTR > VP_n then Last_MNTR := 1; inc(Last_URL); if Last_URL > URL_Count then Last_URL := 1; mntr[Last_MNTR].active := true; mntr[Last_MNTR].url := urls[Last_URL].url; urls[Last_URL].mntr_idx := Last_MNTR; if Last_MNTR > 1 then VlcPlayer[Last_MNTR].SetAudioVolume(0) else VlcPlayer[Last_MNTR].SetAudioVolume(50); Label1.Caption := 'запускаю: '+ IntToStr(Last_URL)+'. '+ urls[Last_URL].name + ' ('+ urls[Last_URL].url + ') в '+IntToStr(Last_MNTR)+' окне'; Form1.Canvas.Pen.Color := clLime; Form1.Canvas.Pen.Width := 5; Form1.Canvas.Rectangle(VlcPlayer[Last_MNTR].Left - 3, VlcPlayer[Last_MNTR].Top - 3,VlcPlayer[Last_MNTR].Left + 323, VlcPlayer[Last_MNTR].Top + 243); VlcPlayer[Last_MNTR].Play(mntr[Last_MNTR].url); EXIT; end //все кол-во разрешенных мониторов уже запущено else begin // Если при этом разрешенных мониторов (Num_Active) столько же сколько всего доступных мониторов (VP_n) if Num_Active = VP_n then begin inc(Last_MNTR); if Last_MNTR > VP_n then Last_MNTR := 1; inc(Last_URL); if Last_URL > URL_Count then Last_URL := 1; mntr[Last_MNTR].active := true; mntr[Last_MNTR].url := urls[Last_URL].url; urls[Last_URL].mntr_idx := Last_MNTR; if Last_MNTR > 1 then VlcPlayer[Last_MNTR].SetAudioVolume(0) else VlcPlayer[Last_MNTR].SetAudioVolume(50); Label1.Caption := 'запускаю: '+IntToStr(Last_URL)+'. '+ urls[Last_URL].name + ' ('+ urls[Last_URL].url + ') в '+IntToStr(Last_MNTR)+' окне'; Form1.Canvas.Pen.Color := clLime; Form1.Canvas.Pen.Width := 5; Form1.Canvas.Rectangle(VlcPlayer[Last_MNTR].Left - 3, VlcPlayer[Last_MNTR].Top - 3,VlcPlayer[Last_MNTR].Left + 323, VlcPlayer[Last_MNTR].Top + 243); VlcPlayer[Last_MNTR].Play(mntr[Last_MNTR].url); EXIT; end else begin for i := 1 to URL_Count do if urls[i].mntr_idx = Last_MNTR then urls[i].mntr_idx := 0; inc(Last_MNTR); if Last_MNTR > VP_n then Last_MNTR := 1; inc(Last_URL); if Last_URL > URL_Count then Last_URL := 1; if Last_MNTR - Num_Active < 0 then begin mntr[Last_MNTR+VP_n-Num_Active].active := false; VlcPlayer[Last_MNTR+VP_n-Num_Active].Play('zastavka.ps'); end else begin mntr[Last_MNTR-Num_Active].active := false; VlcPlayer[Last_MNTR-Num_Active].Play('zastavka.ps'); end; mntr[Last_MNTR].active := true; mntr[Last_MNTR].url := urls[Last_URL].url; urls[Last_URL].mntr_idx := Last_MNTR; Label1.Caption := 'запускаю: '+IntToStr(Last_URL)+'. '+ urls[Last_URL].name + ' ('+ urls[Last_URL].url + ') в '+IntToStr(Last_MNTR)+' окне'; Form1.Canvas.Pen.Color := clLime; Form1.Canvas.Pen.Width := 5; Form1.Canvas.Rectangle(VlcPlayer[Last_MNTR].Left - 3, VlcPlayer[Last_MNTR].Top - 3,VlcPlayer[Last_MNTR].Left + 323, VlcPlayer[Last_MNTR].Top + 243); VlcPlayer[Last_MNTR].Play(mntr[Last_MNTR].url); end; end; end else if URL_Count = VP_n then begin // отдельный случай - чисто для мониторинга // предполагаем что урлов стока же скока и мониторов - поэтому тупо все быстро запускаем - и ищем ошибки в потоках VlcPlayer[1].SetAudioVolume(50); VlcPlayer[1].Play(urls[1].url); // звук будет тока в первом окошке Sleep(2000); for i := 2 to URL_Count do begin VlcPlayer[i].SetAudioVolume(0); VlcPlayer[i].Play(urls[i].url); Sleep(300); end; for i := 1 to URL_Count do prev_vlc_stats[i] := VlcPlayer[i].GetStats; Timer1.Enabled := false; Edit1.Visible := true; Button1.Visible := true; for i := 2 to 18 do (FindComponent('Label'+IntToStr(i)) as TLabel).Visible := true; Timer2.Enabled := true; end end; procedure TForm1.Timer2Timer(Sender: TObject); var // stat:libvlc_media_stats_t; i: integer; begin for i := 1 to URL_Count do begin vlc_stats[i] := VlcPlayer[i].GetStats; if (vlc_stats[i].i_demux_corrupted > prev_vlc_stats[i].i_demux_corrupted) or (vlc_stats[i].i_demux_discontinuity > prev_vlc_stats[i].i_demux_discontinuity) or (vlc_stats[i].i_lost_pictures > prev_vlc_stats[i].i_lost_pictures) or (vlc_stats[i].i_lost_abuffers > prev_vlc_stats[i].i_lost_abuffers) then Form1.Canvas.Pen.Color := clRed; Form1.Canvas.Pen.Width := 3; Form1.Canvas.Rectangle(VlcPlayer[i].Left - 2, VlcPlayer[i].Top - 2,VlcPlayer[i].Left + 322, VlcPlayer[i].Top + 242); prev_vlc_stats[i] := vlc_stats[i]; end; i := StrToInt(Label18.Caption); Label3.Caption := IntToStr(vlc_stats[i].i_read_bytes)+' Read bytes'; Label4.Caption := IntToStr(vlc_stats[i].i_demux_read_bytes)+' Demux read bytes'; Label5.Caption := IntToStr(vlc_stats[i].i_demux_corrupted)+' Demux corrupted'; Label6.Caption := IntToStr(vlc_stats[i].i_demux_discontinuity)+' Demux discontinuity'; Label7.Caption := IntToStr(vlc_stats[i].i_decoded_video)+' Decoded video'; Label8.Caption := IntToStr(vlc_stats[i].i_decoded_audio)+' Decoded audio'; Label9.Caption := IntToStr(vlc_stats[i].i_displayed_pictures)+' Displayed pictures'; Label10.Caption := IntToStr(vlc_stats[i].i_lost_pictures)+' Lost pictures'; Label11.Caption := IntToStr(vlc_stats[i].i_played_abuffers)+' Played ABuffers'; Label12.Caption := IntToStr(vlc_stats[i].i_lost_abuffers)+' Lost ABuffers'; Label13.Caption := IntToStr(vlc_stats[i].i_sent_packets)+' Sent packets'; Label14.Caption := IntToStr(vlc_stats[i].i_sent_bytes)+' Sent bytes'; Label15.Caption := FloatToStr(vlc_stats[i].f_input_bitrate)+' Input bitrate'; Label16.Caption := FloatToStr(vlc_stats[i].f_demux_bitrate)+' Demux bitrate'; Label17.Caption := FloatToStr(vlc_stats[i].f_send_bitrate)+' Send bitrate'; end; procedure TForm1.Button1Click(Sender: TObject); var i: integer; begin i := StrToInt(Edit1.Text); if (i > 0) and (i <= URL_Count) then begin VlcPlayer[StrToInt(Label18.Caption)].SetAudioVolume(0); Label18.Caption := IntToStr(i); VlcPlayer[i].SetAudioVolume(50); end; end; end.
модуль unit2.pas с показом настроек - совсем простенький
Download file Unit2.pasunit Unit2; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Buttons; type TForm2 = class(TForm) Edit1: TEdit; Edit2: TEdit; Label1: TLabel; Label2: TLabel; Edit3: TEdit; Label3: TLabel; Edit4: TEdit; BitBtn1: TBitBtn; Label4: TLabel; Button1: TButton; Edit5: TEdit; Label5: TLabel; OpenDialog1: TOpenDialog; procedure BitBtn1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form2: TForm2; implementation {$R *.dfm} procedure TForm2.BitBtn1Click(Sender: TObject); begin if OpenDialog1.Execute then Edit4.Text := OpenDialog1.FileName; end; end.
описание формы для unit1.dfm
Download file Unit1.dfmobject Form1: TForm1 Left = 366 Top = 257 Width = 679 Height = 445 Caption = 'Moza IC by IbZ(c)' Color = clBtnFace Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -11 Font.Name = 'MS Sans Serif' Font.Style = [] OldCreateOrder = False Position = poScreenCenter OnCreate = FormCreate PixelsPerInch = 96 TextHeight = 13 object Panel1: TPanel Left = 0 Top = 368 Width = 671 Height = 50 Align = alBottom TabOrder = 0 object Label1: TLabel Left = 8 Top = 8 Width = 7 Height = 24 Font.Charset = DEFAULT_CHARSET Font.Color = clNavy Font.Height = -19 Font.Name = 'MS Sans Serif' Font.Style = [fsBold] ParentFont = False end object Label2: TLabel Left = 32 Top = 4 Width = 140 Height = 13 Caption = #8470' '#1082#1072#1085#1072#1083#1072' '#1076#1083#1103' '#1084#1086#1085#1080#1090#1086#1088#1080#1085#1075#1072 Visible = False end object Label3: TLabel Left = 616 Top = 0 Width = 54 Height = 13 Caption = 'Read bytes' Visible = False end object Label4: TLabel Left = 616 Top = 24 Width = 85 Height = 13 Caption = 'Demux read bytes' Visible = False end object Label5: TLabel Left = 472 Top = 0 Width = 81 Height = 13 Caption = 'Demux corrupted' Visible = False end object Label6: TLabel Left = 472 Top = 12 Width = 94 Height = 13 Caption = 'Demux discontinuity' Visible = False end object Label7: TLabel Left = 336 Top = 0 Width = 73 Height = 13 Caption = 'Decoded video' Visible = False end object Label8: TLabel Left = 336 Top = 24 Width = 73 Height = 13 Caption = 'Decoded audio' Visible = False end object Label9: TLabel Left = 336 Top = 12 Width = 86 Height = 13 Caption = 'Displayed pictures' Visible = False end object Label10: TLabel Left = 472 Top = 24 Width = 60 Height = 13 Caption = 'Lost pictures' Visible = False end object Label11: TLabel Left = 336 Top = 36 Width = 75 Height = 13 Caption = 'Played ABuffers' Visible = False end object Label12: TLabel Left = 472 Top = 36 Width = 63 Height = 13 Caption = 'Lost ABuffers' Visible = False end object Label13: TLabel Left = 240 Top = 8 Width = 63 Height = 13 Caption = 'Sent packets' Visible = False end object Label14: TLabel Left = 240 Top = 20 Width = 53 Height = 13 Caption = 'Send bytes' Visible = False end object Label15: TLabel Left = 616 Top = 12 Width = 56 Height = 13 Caption = 'Input bitrate' Visible = False end object Label16: TLabel Left = 616 Top = 36 Width = 65 Height = 13 Caption = 'Demux bitrate' Visible = False end object Label17: TLabel Left = 240 Top = 32 Width = 57 Height = 13 Caption = 'Send bitrate' Visible = False end object Label18: TLabel Left = 64 Top = 24 Width = 6 Height = 13 Caption = '1' Visible = False end object Edit1: TEdit Left = 32 Top = 20 Width = 25 Height = 21 TabOrder = 0 Text = '1' Visible = False end object Button1: TButton Left = 88 Top = 20 Width = 81 Height = 25 Caption = #1052#1086#1085#1080#1090#1086#1088#1080#1090#1100'!' TabOrder = 1 Visible = False OnClick = Button1Click end end object Timer1: TTimer Enabled = False OnTimer = Timer1Timer Left = 88 Top = 112 end object Timer2: TTimer Enabled = False OnTimer = Timer2Timer Left = 128 Top = 112 end end
описание формы для unit2.dfm
Download file Unit2.dfmobject Form2: TForm2 Left = 519 Top = 337 Width = 264 Height = 257 Caption = #1053#1072#1089#1090#1088#1086#1077#1095#1082#1080'...' Color = clBtnFace Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -11 Font.Name = 'MS Sans Serif' Font.Style = [] OldCreateOrder = False PixelsPerInch = 96 TextHeight = 13 object Label1: TLabel Left = 8 Top = 12 Width = 117 Height = 13 Caption = #1050#1086#1083#1080#1095#1077#1089#1090#1074#1086' '#1084#1086#1085#1080#1090#1086#1088#1086#1074 end object Label2: TLabel Left = 200 Top = 12 Width = 9 Height = 13 Caption = 'X' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -11 Font.Name = 'MS Sans Serif' Font.Style = [fsBold] ParentFont = False end object Label3: TLabel Left = 8 Top = 44 Width = 200 Height = 13 Caption = #1054#1076#1085#1086#1074#1088#1077#1084#1077#1085#1085#1086' '#1088#1072#1073#1086#1090#1072#1102#1097#1080#1093' '#1084#1086#1085#1080#1090#1086#1088#1086#1074 end object Label4: TLabel Left = 8 Top = 112 Width = 191 Height = 13 Caption = #1054#1090#1082#1088#1099#1090#1100' '#1092#1072#1081#1083' '#1089' '#1087#1083#1077#1081#1083#1080#1089#1090#1086#1084' '#1082#1072#1085#1072#1083#1086#1074 end object Label5: TLabel Left = 8 Top = 76 Width = 194 Height = 13 Caption = #1055#1072#1091#1079#1072' '#1084#1077#1078#1076#1091' '#1079#1072#1087#1091#1089#1082#1086#1084' '#1082#1072#1085#1072#1083#1086#1074' ('#1089#1077#1082'.)' end object Edit1: TEdit Left = 160 Top = 8 Width = 33 Height = 21 TabOrder = 0 Text = '3' end object Edit2: TEdit Left = 216 Top = 8 Width = 33 Height = 21 TabOrder = 1 Text = '3' end object Edit3: TEdit Left = 216 Top = 40 Width = 33 Height = 21 TabOrder = 2 Text = '5' end object Edit4: TEdit Left = 8 Top = 128 Width = 209 Height = 21 TabOrder = 3 Text = 'playlist.m3u' end object BitBtn1: TBitBtn Left = 224 Top = 128 Width = 25 Height = 25 TabOrder = 4 OnClick = BitBtn1Click Glyph.Data = { 76010000424D7601000000000000760000002800000020000000100000000100 04000000000000010000120B0000120B00001000000000000000000000000000 800000800000008080008000000080008000808000007F7F7F00BFBFBF000000 FF0000FF000000FFFF00FF000000FF00FF00FFFF0000FFFFFF00555555555555 55555555FFFFFFFFFF55555000000000055555577777777775F55500B8B8B8B8 B05555775F555555575F550F0B8B8B8B8B05557F75F555555575550BF0B8B8B8 B8B0557F575FFFFFFFF7550FBF0000000000557F557777777777500BFBFBFBFB 0555577F555555557F550B0FBFBFBFBF05557F7F555555FF75550F0BFBFBF000 55557F75F555577755550BF0BFBF0B0555557F575FFF757F55550FB700007F05 55557F557777557F55550BFBFBFBFB0555557F555555557F55550FBFBFBFBF05 55557FFFFFFFFF7555550000000000555555777777777755555550FBFB055555 5555575FFF755555555557000075555555555577775555555555} NumGlyphs = 2 end object Button1: TButton Left = 8 Top = 168 Width = 241 Height = 57 Caption = #1055' '#1040' '#1045' '#1061' '#1040' '#1051' '#1048' !!!' Default = True Font.Charset = ANSI_CHARSET Font.Color = clWindowText Font.Height = -21 Font.Name = 'Arial' Font.Style = [fsBold] ModalResult = 1 ParentFont = False TabOrder = 5 end object Edit5: TEdit Left = 216 Top = 72 Width = 33 Height = 21 TabOrder = 6 Text = '10' end object OpenDialog1: TOpenDialog Filter = 'M3U|*.m3u' Left = 152 Top = 136 end end
Ссылка на архив с исходниками, скомпилированной программой и примером плейлиста Download file MozaIC.zip