Tutorial: OSX statusbar radio app
1 year ago
Appen Norsk Radio har lenge ligget på topplisten i AppStore for Mac. Den lar deg høre på radiokanaler rett fra statusmenyen i OSX og her har jeg tenkt å vise deg hvordan du selv kan lage en liknende app.
Denne tutorialen passer for nybegynnere og nysgjerrige. Jeg forusetter at du har XCode installert og kun minimal kjennskap til utvikling for Apples plattformer er nødvendig.
Ny app
Start XCode og velg å lage en ny Cocoa Application.

Jeg har valgt å kalle min app Min Radio og bruker klasseprefiks RB. Om du velger andre navn kan du oversette referanser til filnavn og klasser som nødvendig. Vi benytter ikke Automatic Reference Counting her, så pass på at dette ikke er valgt.

Når prosjektet er åpnet i XCode kan du allerede nå velge Run og få opp appen på skjermen med sitt tomme vindu. Ikke spesielt interessant. Avslutt appen igjen ved å velge Stop i XCode.
Kjør i bakgrunnen
Vi behøver ikke et vindu for vår app, så åpne MainMenu.xib og slett alt under Objects bortsett fra “App Delegate”.

Videre må vi erklære at vi vil startes som en bakgrunnsapp uten vindu som ikke vises i Dock på vanlig måte. Åpne gruppen Supporting Files og filen Min Radio-Info.plist. Høyreklikk under den siste raden og velg Add row. I dropdown-menyen velger du Application is background only og setter value til YES.

Hvis du velger Run nå vil du ikke se noen indikasjon på at appen kjører bortsett fra at du har muligheten til å velge Stop i XCode. Stopp appen igjen.
Menu, please
Den første koden vi skal skrive er for å sørge for at vi vises på skjermen. Vi gjør dette ved å lage en meny som vil havne oppe til høyre blant de andre ikonene på statusbaren, til venstre for Spotlight, klokken osv.
Åpne RBAppDelegate.h.
https://gist.github.com/2375407
Vi legger til instansevariabelen NSStatusItem *systemStatusItem. Denne bruker vi som referanse til vårt element i statusbarmenyen. Menyen vi skal skrive vil bli lagt til under dette elementet.
Åpne RBAppDelegate.m og du vil se en metode kalt
-applicationDidFinishLaunching:
Denne kalles automatisk etter oppstart. Endre denne metoden til å inneholde følgende:
https://gist.github.com/2375410
Vi legger oss enkelt og greit til i statusbaren med tittelen Radio. Her har du også muligheten til å bruke en bildefil som ikon hvis du heller ønsker det, eller et eget view. Undersøk metodene -setImage: og -setView: på NSStatusItem for mer om dette.
Om du nå kjører appen skal du kunne se ordet Radio i statusbaren. Ingenting vil skje om du klikker der, men nå vet vi i det minste at appen vår er riktig satt opp! Furthur…
I call quits!
Det første vi skal gjøre med menyen er å legge til et valg for å avslutte appen. Da behøver vi først en måte å avslutte på. Legg til følgende metode i RBAppDelegate.m:
https://gist.github.com/2375412
Litt om NSApp
NSApp er en global konstant i appen din som peker på det samme som [NSApplication sharedApplication], altså din instans av klassen NSApplication. Alle apper har en. Klassen vi jobber med, RBAppDelegate, er en klasse som implementerer protokollen (tenk Interface) NSApplicationDelegate og er satt som delegaten til din instans av NSApplication, altså NSApp. For mer om hvilke hendelser delegaten din kan håndtere og hvilke metoder som finnes på NSApplication, se dokumentasjon for NSApplication og NSApplicationDelegate. En kjapp måte å gjøre dette på er å holde inn alt og klikke på navnet til konstanten du vil ha mer informasjon om. Her er vi interessert i å avslutte appen så vi kaller enkelt og greit metoden -terminate:.
Quits it is
Nå kan vi legge til en meny under statusbar-elementet vårt som kaller denne metoden. Menyen er en instans av NSMenu og i denne legger vi til et element som heter Avslutt som kaller metoden vi nettopp la til. Endre oppstart-metoden til følgende:
https://gist.github.com/2375415
Vi kunne lagd denne menyen i Interface Builder med pek og klikk, men senere skal vi dynamisk bygge opp en liste over radiokanaler så vi bygger menyen likegjerne programmatisk fra starten.
Kjør appen nå og klikk på Radio i system-menyen. Du skal nå se Avslutt. Velg dette, se at Stop-symbolet i XCode blir deaktivert og Radio forsvinner fra system-menyen. Gratulerer, du er en OSX app-utvikler!
Sound check
Vi har allerede kommet til hovedproblemet vi skal løse. Å streame en radiokanal fra nettet og spille den av. Dette er en relativt komplisert oppgave å løse. Løsningen involverer å opprette en HTTP-tilkobling mot en server på en URL hvor lyden finnes, lese n antall byes fra denne i en loop og skrive disse data til et ringbuffer i minnet, lage en konsument-tråd som begynner å lese fra denne ringbufferen så snart vi har nok data og prosesserer den rå data som der ligger i forhold til hvilken type codec som er benyttet, f.eks MP3, konvertere MP3 til PCM lyddata og på en eller annen måte avspille denne på brukerens høytalere eller headphones. Vi KAN begynne å implementere alt dette nå, men vi velger heller å lene oss på rammeverket QTKit.
Legg til et rammeverk
For å bruke rammeverket QTKit må vi først inkludere det i prosjektet vårt, slik at appen vår linker mot det.
Velg Min Radio øverst i menyen til venstre. Velg Min Radio igjen under Targets i neste kolonne som dukker opp og gå til tabben Build phases. Åpne gruppen Link Binary With Libraries og klikk pluss-tegnet.

Finn QTKit.framework i listen, marker den og velg Add.

Rammeverket dukker nå opp i fil-listen til venstre og du kan bruke det i dine kildekodefiler om du først importerer dets headerfiler.
Testing, testing, one two three
RBAppDelegate.h:
https://gist.github.com/2375426
RBAppDelegate.m:
https://gist.github.com/2375427
Prøv nå å legge inn denne linjen på slutten av -applicationDidFinishLaunching:
[self playUrlString:@"http://lyd.nrk.no/nrk_radio_p2_mp3_h"];
kjør appen og du vil etter et par sekunder, avhengig av hvor raskt nett du har, høre NRK P2. Magic! Selv om vår jobb har blitt betydelig enklere utføres faktisk fortsatt alle stegene jeg nevnte ovenfor. Det er tatt hånd om i QTKit. Ulempen med dette er at vi ikke kan gjøre noe spesielt interessant med lyden siden vi ikke har kontroll over den eller tilgang til den. Om du for eksempel vil støtte opptak ved å skrive til disk mens du lytter på en radiokanal, tegne lydbølger, etc, må du komme tettere innpå lyden. Om det er nok interesse kan jeg også lage en lengre tutorial på dette hvor vi bruker Core Audio.
Slett følgende linje igjen:
[self playUrlString:@"http://lyd.nrk.no/nrk_radio_p2_mp3_h"];
Whats the frequency, Kenneth?
Vi har nå en meny og vi vet hvordan vi spiller av en radiokanal fra en URL. Som du sikkert allerede har skjønt er det neste steget å lage menypunkter for noen radiokanaler og knytte valget av disse med en metode som bruker QTKit for å spille av den tilhørende URL’en. Det skalerer derimot dårlig å hardkode URL’er i applikasjonen. Vi behøver en måte å lagre og lese inn på navn på radiokanaler og deres URLer. For denne type lett strukturert, relativt statisk informasjon er det naturlig å benytte en “property list”, eller plist. En plist er en tekstfil som representerer datastrukturer. I appen leser vi denne filen og bygger opp menyen ut i fra denne.
stations.plist
Velg New File… fra filmenyen. Velg gruppen Resource i menyen til venstre og Property List i feltet til høyre. Kall denne filen stations og velg Create. Filen er nå åpen i XCode, men den er tom. Kopier teksten under og høyreklikk i den tomme filen. Velg Paste.
https://gist.github.com/2375430
Filen inneholder nå et tekst-serialisert array. Dette har 3 elementer som hver er en dictionary, altså en datastruktur med nøkler og tilhørende verdier. Disse inneholder navn og URL for NRK P1, P2 og P3 som strenger. Du kan selvsagt legge inn andre kanaler eller legge til flere. Om du vil videreutvikle appen kan du også gi brukeren mulighet til å redigere denne listen slik Norsk Radio ble oppdatert til å gjøre.
Radiomeny
Det fine med en plist er at vi enkelt kan lese den fra disk til datastrukturer i minnet i programmet vårt. Vi skal nå lese inn denne filen og bygge opp menyen vår. RBAppDelegate.m skal nå se slik ut:
Vi har flyttet meny-byggingen til en egen metode for å holde litt orden.
Om du nå starter appen og klikker på Radio skal du se radiokanalene i grått, fordi de ikke er koblet opp mot en action, og Avslutt. Perfekt! Vi må nå på et vis koble valget av disse med selve avspillingen. Det vi behøver er en metode som vet hvilken kanal som ble valgt og som starter avspillingen.
Knock, knock
Vi kan naturligvis ikke ha en egen metode for hver radiokanal, så metoden vi skriver må kunne finne ut hvilken radiokanal som skal spilles av. Heldigvis vil metodekall ved menyvalg bli sendt menyelementet som ble klikket som argument. På dette menyvalget kan vi sette en såkalt tag som vi kan bruke til å referere til array-indeksen for radiokanalen som ble valgt. Vi behøver tilgang til både stations arrayet samt menyelementene i den nye metoden så vi lager først 2 instansevariabler vi kan referere til i RBAppDelegate.h
https://gist.github.com/2375442
RBAppDelegate.m:
https://gist.github.com/2375436
Vi går over til å bruke instansevariablene i -createMenu hvor vi nå bygger menyelementene mer eksplisitt enn tidligere. Vi legger menyelementene inn i menuItems arrayet slik at vi kan endre status på dem, og vi tagger dem med array-index slik at vi enkelt kan få tak i kanalen de representerer fra stations arrayet. Vi kaller også retain på stations så vi er sikre på at den ikke forsvinner for oss etter metoden returnerer.
I oppstarten av appen må vi allokere minneplass til og initsiere menuItems arrayet, og ved avslutning rydder vi opp etter oss ved å kalle release på denne og på stations, som vi eksplisitt kalte retain på i -createMenu.
Vi har også innført en -play: metode som blir kalt ved et menyvalg og som skriver ut til loggen hva vi har tenkt å gjøre videre, som en verifisering på at vi har gjort ting riktig. Kjør appen og gjør noen menyvalg. Hvis du har gjort alt riktig skal du se noe ala dette i loggen:
2012-04-05 15:17:35.680 Min Radio[4787:403] Spiller NRK P1 fra http://lyd.nrk.no/nrk_radio_p1_ostlandssendingen_mp3_h
2012-04-05 15:17:37.234 Min Radio[4787:403] Spiller NRK P2 fra http://lyd.nrk.no/nrk_radio_p2_mp3_h
2012-04-05 15:17:38.719 Min Radio[4787:403] Spiller NRK P3 fra http://lyd.nrk.no/nrk_radio_p3_mp3_h
Spit and shine
Flott! Da har vi informasjon om hva brukeren vil lytte til. Før vi kobler opp lyden skal vi kjapt fikse et par småting med menyen; valget brukeren har gjort blir ikke reflektert i selve menyen. Når et menyelement blir valgt må vi sørge for å sette en hake ved dette elementet og samtidig sørge for at ingen andre elementer har en hake. I tillegg legger vi på en skillelinje før “Avslutt” med
[appMenu addItem:[NSMenuItem separatorItem]]
og en kjekk feature som viser kanalnavnet i statusbaren så brukeren enkelt kan se hvilken radiokanal som spilles. Her følger oppdatert play: og createMenu:
https://gist.github.com/2375444
Great success!
Det eneste som nå gjenstår er å spille av radiokanalen brukeren har valgt. Dette kan du allerede nå, men her er den siste nødvendige kodelinjene i appen vår; i slutten av -play: og en oppdatert -applicationWillTerminate::
https://gist.github.com/2375448
Oppsummering

Gratulerer! Du er nå faktisk en OSX app-utvikler!
Av hensyn til plass og fokus har jeg hoppet over endel ting her. En av de viktigste tingene som ikke er med er feilhåndtering. I metoden som spiller av en URL sender jeg bare nil istedet for en peker til et NSError-objekt. Her bør du sende en gyldig referanse og sjekke dette error-objektet for feil før du kaller autoPlay. I tillegg bør appen lytte til notifications fra QTKit så du kan informere brukeren om feil som oppstår, endre navnet i menyen hvis avspillingen plutselig stopper opp osv.
I tillegg kunne koden vært bedre organisert. Du kunne for eksempel flyttet ut alt som har med QTKit og avspilling å gjøre til en ny klasse, for eksempel RBPlayer. Og appen behøver nok en mulighet til å stoppe avspilling uten å avslutte appen. Dette løser du nå enkelt selv ved å legge til et menypunkt for dette med en egen action.
Hele prosjektet finner du på Github