Irradiant

Obiettivi

Si realizzi una applicazione che permetta la condivisione in rete del contenuto dello schermo di un calcolatore e la diffusione di un commento audio associato.

L'applicazione permette la trasmissione in streaming del contenuto dello schermo presente sul calcolatore e del segnale audio proveniente dalla scheda sonora ad un insieme di destinatari, utilizzando la tecnica IP multicast.

L'applicazione sviluppata potrà operare sia come destinatario che come sorgente.

In modalità sorgente, l'applicazione deve consentire di:

In modalità destinatario, l'applicazione deve consentire di:

Requisiti

Il sistema deve permettere la trasmissione di due flussi di informazione limitando il consumo di risorse condivise e massimizzando la probabilità di una corretta interpretazione delle informazioni da parte dei destinatari. A questo scopo occorre che la banda complessivamente impegnata dalla sorgente non superi un limite fissato dall'utente; tale limite dovrà essere ovviamente compatibile con i requisiti minimi necessari a trasportare il flusso audio della sessione. Tale flusso dovrà, infatti, avere la priorità su quello video al fine di mantenere l'intellegibilità del contenuto.

Poiché una parte significativa della comunicazione può essere veicolata attraverso il movimento del mouse, le informazioni relative alla sua posizione dovranno essere riportate in ciascun pacchetto audio, al fine di garantirne una tempestiva visualizzazione da parte dei destinatari.

Per ridurre il consumo di banda da parte della componente video, è possibile adottare qualche forma di compressione. Data la limitata variabilità del contenuto dello schermo durante una presentazione, è possibile ottenere un primo livello di compressione suddividendo l'immagine da trasmettere in blocchi elementari ed inviando solo i blocchi nei quali si è verificata una variazione rispetto al fotogramma precendente.

Poiché il trasporto multicast è intrinsecamente inaffidabile, occorre che l'applicazione in modalità sorgente provveda ad inviare, con una certa ridondanza, le informazioni relative al video: questo può essere ottenuto mediante il concetto di "key frame"; ovvero, trascorso un certo intervallo di tempo, l'applicazione provvede a ritrasmettere tutti i sottoblocchi, anche se non sono variati. L'intervallo di tempo tra due key frame deve essere configurabile dall'utente.

Il protocollo applicativo utilizzato per la comunicazione tra i calcolatori deve essere definito in maniera opportuna in fase di progetto e può ispirarsi a protocolli esistenti per il trasferimento di contenuti multimediali (RTP).

Scelte progettuali

Il programma è stato sviluppato in C# usando le librerie previste dal CLR dove possibile, colmando le lacune accedendo alle API Win32 per Windows e GDK# per la fase iniziale, la quale è stata condotta sotto piattaforma Debian GNU/Linux usando l'ambiente Mono, compatibile con .NET 2.0, effettuando la transizione verso Microsoft Windows e Microsoft Visual C# 2005 Express nel momento di implementare l'interfaccia grafica. Per questo motivo si sono impiegate fin dall'avvio tecniche derivate dalla metodologia Agile, ovvero l'uso di un sistema di controllo delle versioni e l'uso di unit testing per verificare la funzionalità delle componenti del programma, compito per il quale è stato impiegato il framework NUnit.

L'uso diffuso di unit testing ha influenzato alcune scelte progettuali, in particolare il meccanismo di sincronizzazione dei thread, pensato per non essere dipendente dall'orologio consentendo quindi simulazioni affidabili e rapide, adatte all'impiego nei test automatici.

Un'altra scelta progettuale è stata quella di organizzare i componenti del programma in modo da formare un framework, presentando i componenti stessi come elementi separati e riutilizzabili, perlopiù definiti mediante l'uso di interfacce. Per questo si è cercato di mantenere le policy al di fuori della libreria, lasciandovi solo i puri meccanismi, e di implementarle solo nell'applicazione vera e propria.

Ad esempio è possibile osservare come possa essere semplice usare una politica diversa di gestione della banda in IrradiantServer oppure una sogente video differente o, ancora, un diverso sistema di raccolta delle statistiche.

Protocollo di comunicazione

Come specificato nei requisiti, il sistema fa uso di pacchetti multicast su UDP, pertanto la comunicazione è unidirezionale dal server verso i client. La struttura del singolo pacchetto è definita nella classe Packet, con un header costituito da un campo Type di 16 bit che definisce la tipologia di pacchetto (es. audio o video), un campo Subtype di 16 bit che indica il sottotipo (come la distinzione tra immagine e cursore nei pacchetti video), un campo a 64 bit per il Timestamp e un campo Length da 32 bit che riporta la lunghezza del payload. Dopo l'header seguono direttamente i dati da trasmettere che variano tra i diversi tipi di pacchetti. I pacchetti fuori ordine vengono scartati.

Video

La trasmissione dei dati video avviene per strisce dette "slice". L'immagine dello schermo viene suddivisa in slice, le quali vengono trasmesse solo se differiscono dall'ultima slice corrispondente trasmessa o se il timeout a loro associato è scaduto. L'uso di un timeout separato associato a ogni slice evita i picchi di occupazione di banda che si avrebbero con l'invio periodico di keyframe. Oltre alla slice, codificata in formato bitmap, PNG o JPEG, vengono anche trasmesse informazioni sulla dimensione dell'intera area catturata e lo spostamento verticale della singola slice all'interno della schermata. I dati sul puntatore sono, invece, trasmessi con pacchetti differenti, contenenti la posizione del cursore, l'offset dell'hotspot dall'origine dell'immagine che lo rappresenta e l'immagine stessa.

Audio

L'audio viene trasmesso come frammento wave codificato a 22050Hz con 16 bit di precisione, compresso con l'algoritmo Deflate. Per semplicità di implementazione si è fatto uso del supporto alla serializzazione incluso nell'ambiente .NET, trasmettendo l'oggetto AudioDeflatedWaveFragment serializzato. Questo oggetto, oltre ai dati audio, contiene anche l'indicazione della frequenza di campionamento e del numero di bit di precisione.

Il framework

Il framework è costituito dal namespace Irradiant, il quale contiene a sua volta una serie di namespace che raggruppano le classi in funzione delle loro competenze:

Irradiant

Il namespace Irradiant contiene 5 classi:

Compare

Questa classe contiene il solo metodo statico Bitmap() il cui scopo è quello di confrontare due immagini per decidere se queste sono uguali, in quanto la classe System.Drawing.Bitmap non definisce l'operatore di uguaglianza, necessario per determinare le slice da trasmettere.

Synchronizer

La classe Synchronizer racchiude la logica di sincronizzazione tra i thread: essa accetta nel metodo Start() un delegate che verrà eseguito i un worker thread separato. Questo delegate potrà bloccarsi in attesa di sincronizzazione ogni qual volta chiami il metodo WaitTick() dell'oggetto Synchronizer, fino a che il thread controllante non lo sblocchi mediante il metodo Tick(). L'esecuzione del worker thread viene terminata dal metodo Stop().

Client

Client costituisce la porzione di programma che si pone in attesa di ricevere pacchetti dalla rete, ne estrae i frammenti di video o audio e fornisce alcuni metodi per impiegarli. Quando vengono ricevuti dati dalla rete il client provvede a lanciare uno degli eventi NewVideoFragmentDelegate, NewAudioFragmentDelegate o NewCursorFragmentDelegate, a seconda del tipo di frammento.

Server

Diagramma della sezione video di Irradiant.Server

L'oggetto Server associa delle sorgenti video e audio ai rispettivi encoder, per poi trasmettere i risultati alla rete. Le operazioni necessarie per prelevare i dati dalle sorgenti, codificarli e spedirli avvengono in un thread separato, controllato dai metodi forniti dalla classe Synchronizer, da cui Server eredita. Ad ogni Tick() si codificano audio, video e posizione del puntatore in base agli intervalli di aggiornamento specificati dalle proprietà AudioRefresh, VideoRefresh e CursorRefresh.

BandwidthManager

Questa classe raccoglie tutta la logica per la gestione del consumo di banda sulla rete, che viene misurata considerando i dati raccolti dalla classe Statistics e quindi controllata mediante i metodi appositi degli encoder, anche se attualmente sono disponibili solo per il video, in quanto l'attuale codec audio non presenta la possibilità di effettuare regolazioni. L'intera logica di regolazione è contenuta nel metodo Manage().

Irradiant.Network

Diagramma della sezione cursore di Irradiant.Server

In namespace Network comprende le classi impiegate per implementare il trasporto di rete:

Packet

La classe Packet è responsabile di rappresentare in memoria, serializzare e deserializzare i pacchetti di rete, come descritti nella sezione riguardante il protocollo di comunicazione.

Multicast

Multicast eredita da System.Net.Sockets.Socket e ne è una specializzazione che fa uso di indirizzi IP multicast, fornendo due modalità differenti, accessibili con i metodi Client() e Server(). Questi utlimi gestiscono l'impostazione delle opzioni necessarie per l'utilizzo in multicast e per l'iscrizione al gruppo multicast scelto.

Server

Diagramma della sezione audio di Irradiant.Server

Il Server di rete si occupa di serializzare i pacchetti da spedire e inviarli alla rete attraverso la un oggetto Multicast in modalità server, aggiornando le statistiche raccolte in un oggetto Statistics, accessibile mediante la proprietà dallo stesso nome.

Client

Usando un oggetto Multicast in modalità client, la classe Client resta in ascolto sulla rete ricevendo i pacchetti destinati al gruppo o all'indirizzo impostato, eventualmente verificando il campo Timestamp e lanciando una eccezione OutOfOrderException nel caso il pacchetto fosse fuori sequenza.

Statistics

La classe Statistics raccoglie le statistiche di rete segnalate dagli oggetti Server e Client, quali il numero di byte inviati e ricevuti, il numero di pacchetti inviati e ricevuti, la dimensione massima dei pacchetti e il numero di pacchetti scartati.

Irradiant.Interfaces

Il namespace Interfaces raccoglie la definizione delle interfacce usate nell'intero programma:

IAudioSource, IVideoSource

IAudioSource e IVideoSource forniscono le sorgenti dei dati audio e video. In particolare IVideoSource fornisce anche i dati sul cursore. Le due interfacce sono state utili sia per astrarre il programma dalla piattaforma di esecuzione, permettendo l'implementazione di una sorgente video Win32 per Microsoft Windows e una GDK per ambienti unix, sia per permettere le simulazioni nei test automatici mediante l'uso di sorgenti fittizie.

IAudioSink

Questa interfaccia definisce la destinazione del flusso audio nel client, permettendo l'uso della destinazione che impiega WaveLib, o quella fittizia per i test.

IAudioEncoder, IVideoEncoder, IAudioDecoder, IVideoDecoder

Prevedendo la possibilità di avere codifiche differenti dei dati, si è scelto di isolare il programma dai codec implementati mediante queste interfacce, le quali dovrebbero garantire sufficiente flessibilità.

IFragment, IVideoFragment, IAudioFragment, ICursorFragment

I dati prodotti dagli encoder e consumati dai decoder sono rappresentati dall'interfaccia IFragment, di cui IVideoFragment, IAudioFragment e ICursorFragment sono le specializzazioni in funzione della tipologia di frammento.

Irradiant.Sources

Questo namespace contiene le sorgenti dati che implementano le interfacce IAudioSource e IVideoSource:

AudioFake, AudioWaveLib

AudioFake è la sorgente audio usata dai test automatici, la quale restituisce dati audio preregistrati, mentre AudioWaveLib impiega il pacchetto WaveLib per accedere alle API win32 per prelevare il flusso audio dal sistem.

VideoFake, VideoGDK, VideoWin32

VideoFake è la sorgente video usata dai test automatici e restituisce schermate preregistrate e posizione fittizie del cursore, VideoGDK usa le librerie GDK# per prelevare l'immagine dello schermo e i dati sul cursore, mentre VideoWin32 usa le API win32.

Irradiant.Codecs

In Codecs sono contenuti gli encoder e decoder usati per trasmormare i dati da essere trasmessi:

AudioDeflatedEncoder, AudioDeflatedDecoder

Convertono il flusso audio in frammenti audio wave compressi con Deflate, l'algoritmo alla base di ZIP e GZIP.

VideoSliceEncoder, VideoSliceDecoder

Suddividono ogni schermata in una serie di slice, restituendo solo le porzioni di schermo che differiscono dall'ultima cattura. Se fosse intercorso un periodo superiore a quello consentito dall'ultima trasmissione di una slice, questa viene comunque ritrasmessa, per consentire ai nuovi client di sincronizzarsi. Ogni slice può essere codificata in formato bitmap, PNG o JPEG a diversi livelli di compressione, a seconda della qualità richiesta con i metodi DecreaseQuality() e IncreaseQuality(). I metodi IncreasePacketSize() e DecreasePacketSize() influenzano invece la dimensione verticale di ogni slice, la quale viene regolata dinamicamente per evitare di trasmettere pacchetti troppo piccoli e migliorare la compressione.

Fragments

Questo namespace raccoglie le tipologie di IFragment usate:

AudioDeflatedWaveFragment

Contengono nel campo Data i byte compressi con deflate e in Rate e SampleSize la frequenza e la profondità di campionamento.

VideoSliceFragment

Un oggetto VideoSliceFragment contiene informazioni sulla dimensione totale dell'area catturata, lo spostamento verticale della slice e la slice stessa.

CursorInfoFragment

CursorInfoFragment contiene oltre alla posizione del cursore, lo spostamente dell'hotspot rispetto ad essa e un oggetto Bitmap che ne rappresenta l'aspetto.

Irradiant.Packets

Questo namespace contiene le sottoclassi di Packet che implementano il protocollo di rete:

AudioPacket, VideoPacket

AudioPacket contiene semplicemente un IAudioFragment, mentre VideoFragment può contenere un ICursorFragment o un IVideoFragment, a seconda del sottotipo.

Irradiant.Forms

Il namespace Forms contiene tutte le classi che fanno uso del framework Windows.Forms per costruire l'interfaccia grafica per il client e per il controllo del server.

ClientStart, ServerStart

ClientStart e ServerStart sono due finestre di dialogo che presentano i parametri da configurare per lanciare la porzione client e server di Irradiant. I parametri sono accessibili mediante proprietà.

ServerRegionSelector

Questa è una finestra di dialogo utilizzata da ServerStart per specificare l'area di cattura per il server, nel caso non si scelga la modalità a schermo intero.

ServerNotifyIcon

Il controllo del server avviene tramite questa classe che contiene un'icona dell'area di notifica che fornisce indicatori dello stato del server e i comandi per interromperne e riprendere il funzionamento.

ClientViewer

ClientViewer è la finestra di visualizzazione del flusso video ricevuto. Possiede una modalità a schermo intero raggiungibile facendo doppio clic oppure con il taso F11. Con il clic destro appare un menù contestuale dove è possibile selezionare, oltre alla modalità a schermo intero, amche se visualizzare o meno alcune statistiche che verranno visualizzate al di sopra del flusso video.

IrradiantServer e IrradiantClient

Diagramma generale di IrradiantServer

Il server raccoglie le impostazioni dalla finestra di dialogo ServerStart con le quali crea e configura un'istanza della classe Server. Oltre ai parametri di configurazione, vengono creati anche le sorgenti audio e video con i relativi encoder. Per gestire l'occupazione di banda vengono istanziate sia Statistics che BandwidthManager. Dopo aver avviato il Server se ne chiama periodicamente il metodo Tick(), mentre ogni secondo si controlla la banda utilizzata prendendo gli opportuni provvedimenti mediante il metodo Manage() di BandwidthManager.

Il client, invece, dopo aver ottenuto l'indirizzo multicast a cui registrarsi con la finestra ClientStart si pone in ascolto e semplicemente reagisce ai pacchetti ricevuti dalla rete.

Criteri di gestione dell'occupazione di banda

Diagramma generale di IrradiantClient

Poiché l'occupazione di banda dipende dagli stessi flussi audio e video, non è possibile prevederla a monte e si rende necessario gestirla dinamicamente. L'intera implementazione dei seguenti criteri è contenuta nel metodo Manage() della classe BandwidthManager, in modo che sia semplice variarne la logica in funzione delle necessità.

La politica usata prevede che se negli ultimi due secondi la banda è stata superiore al 95% del valore desiderato, la qualità venga diminuita, in modo da evitare il raggiungimento del limite previsto. Se, invece, la banda non ha raggiunto il 70% del limite, la qualità viene incrementata. Quando il numero di pacchetti non è stato superiore ai 100 e non vi sono stati pacchetti maggiori di 20kB si procede all'aumento della dimensione dei pacchetti, il che corrisponde ad un incremento della dimensione delle slice, migliorando la compressione e riducendo l'overhead. Nel caso ci fossero stati pacchetti scartati a causa della dimensione eccessiva l'azione prevista è quella di ridurre la dimensione dei pacchetti, diminuendo anche la qualità della codifica, in modo da amplificare il risultato.

Al fine di evitare possibili degenerazioni nelle dimensioni delle slice, all'interno di VideoSliceEncoder sono stati posti dei vincoli affinché esse siano comprese tra 1 e 64 pixel verticali.

Possibili miglioramenti

Come è stato fatto per l'audio, sarebbe utile definire un'interfaccia IVideoSink per separare meglio il decoder video dalla destinazione del flusso decodificato. Questa separazione faciliterebbe l'estrazione della logica dalla classe ClientViewer a IrradiantClient, in modo simile a quanto fatto con la classe IrradiantServer.

Al fine di migliorare la flessibilità del framework nei confronti dell'aggiunta di nuovi tipi di codifica sia per il video che per l'audio, potrebbe essere possibile introdurre una classe Muxer e una Demuxer nel namespace Network, che si occupino di creare pacchetti con tipo e sottotipo opportuni e di smistarli in base a questi ultimi verso il rispettivo decoder. Attualmente la logica di smistamento è contenuta nella classe Client e usa valori fissi e predeterminati. Muxer e Demuxer dovrebbero quindi permettere agli encoder a ai decoder di registrarsi dinamicamente, dichiarando quali tipi e sottotipi di frammenti sono in grado di gestire. Per fare questo si renderebbe inoltre necessaria una interfaccia IDecoder che permetta al Demuxer di inviare al decoder i frammenti in modo generico, dopo aver determinato il corretto destinatario.

L'implementazione della gestione del cursore potrebbe essere rivista, in particolare nel server, al fine di essere più simile a quella dell'audio e del video, fornendo le interfacce ICursorSource e ICursorEncoder, così come ICursorDecoder e ICursorSink nel client.

Poiché si è preferito rilasciare il codice scritto sotto LGPL, sarebbe infine necessario rimpiazzare WaveLib, in quanto la licenza secondo la quale può essere usato è incompatibile con la licenza LGPL.

Codice sorgente

Il codice scritto è disponibile presso l'archivio mercurial http://techn.ocracy.org/irradiant ed è rilasciato con licenza LGPL. Oltre a questo, la sezione audio richiede il pacchetto WaveLib, compilabile con il progetto WaveLib.csproj incluso nell'archivio.