Matricola 129548
Programmazione di sistema 2005/2006
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:
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).
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.
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.
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.
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 è costituito dal namespace Irradiant,
il quale contiene a sua volta una serie di namespace che raggruppano
le classi in funzione delle loro competenze:
Il namespace Irradiant contiene 5 classi:
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.
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 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.
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.
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().
Diagramma della sezione cursore di Irradiant.Server
In namespace Network comprende le classi impiegate
per implementare il trasporto di rete:
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 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.
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.
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.
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.
Il namespace Interfaces raccoglie la definizione delle
interfacce usate nell'intero programma:
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.
Questa interfaccia definisce la destinazione del flusso audio nel client, permettendo l'uso della destinazione che impiega WaveLib, o quella fittizia per i test.
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à.
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.
Questo namespace contiene le sorgenti dati che implementano
le interfacce IAudioSource e IVideoSource:
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 è 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.
In Codecs sono contenuti gli encoder e decoder
usati per trasmormare i dati da essere trasmessi:
Convertono il flusso audio in frammenti audio wave compressi con Deflate, l'algoritmo alla base di ZIP e GZIP.
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.
Questo namespace raccoglie le tipologie di
IFragment usate:
Contengono nel campo Data i byte compressi con
deflate e in Rate e SampleSize la frequenza
e la profondità di campionamento.
Un oggetto VideoSliceFragment contiene informazioni
sulla dimensione totale dell'area catturata, lo spostamento verticale
della slice e la slice stessa.
CursorInfoFragment contiene oltre alla posizione
del cursore, lo spostamente dell'hotspot rispetto ad essa e un
oggetto Bitmap che ne rappresenta l'aspetto.
Questo namespace contiene le sottoclassi di Packet che
implementano il protocollo di rete:
AudioPacket contiene semplicemente un
IAudioFragment, mentre VideoFragment può
contenere un ICursorFragment o un
IVideoFragment, a seconda del sottotipo.
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 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à.
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.
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 è 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.
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.
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.
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.
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.