eklitzke / radio-player

A Haskell program to play Internet radio using the new GStreamer bindings

This URL has Read+Write access

radio-player / radio.hs
100644 140 lines (110 sloc) 4.598 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
-- Simple program to play an Internet radio stream using the GHC GStreamer
-- bindings. Most of the GStreamer boilerplate was copied from the demo app in
-- the gtk2hs source code.
--
-- For now, the steam must be of mpeg audio (i.e. mp3).
 
import Network
import System.IO
import qualified Data.ByteString.Lazy as BS
 
import System.Environment
 
import qualified Media.Streaming.GStreamer as Gst
import qualified System.Glib as G
import qualified System.Glib.MainLoop as G
import qualified System.Glib.Properties as G
 
import System.Posix.Files
import System.Posix.Types
 
import System.Exit
import Data.Maybe
 
import Control.Concurrent
 
(.|.) = unionFileModes
 
fifoName :: String
fifoName = "/tmp/radio-player.fifo"
 
-- The mode for a fifo with perms prw------- (on Linux, 10600)
fifoMode :: FileMode
fifoMode = namedPipeMode .|. ownerReadMode .|. ownerWriteMode
--fifoMode = ownerReadMode .|. ownerWriteMode
 
mkElement action =
    do element <- action
       case element of
         Just element' ->
             return element'
         Nothing ->
             do hPutStrLn stderr "could not create all GStreamer elements\n"
                exitFailure
 
makeHTTPRequest :: String -> String -> String
makeHTTPRequest domain path = (
       "GET " ++ path ++ " HTTP/1.0\r\n"
    ++ "Hostname: " ++ domain ++ "\r\n"
    ++ "User-Agent: radio-player <http://github.com/eklitzke/radio-player>\r\n\r\n" )
 
-- skip over HTTP headers
skipResponseHeaders :: Handle -> IO ()
skipResponseHeaders hdl = do
    ln <- hGetLine hdl
    case ln of
        "\r" -> return ()
        _ -> skipResponseHeaders hdl
 
writeHTTPRequest :: Handle -> String -> IO ()
writeHTTPRequest sock s = do
    hPutStr sock s
    hFlush sock
 
-- Read from a handle (lazily) and write it out to another handle (eagerly).
-- This is meant to be called as a separate thread with forkIO
writeToFifo :: Handle -> Handle -> IO ()
writeToFifo sock out = do
    s <- BS.hGetContents sock
    BS.hPutStr out s
 
main = do
    -- Parse the command line options. Two formats are accepted:
    -- 1) radio <preset-name>
    -- 2) radio <domain> <path> <port>
    args <- getArgs
    [domain, path, port] <- return $ case args of
        [s] -> case s of
            "digitalis" -> ["fx.somafm.com", "/", "8900"]
            "groovesalad" -> ["fx.somafm.com", "/", "8032"]
            "kalx" -> ["icecast.media.berkeley.edu", "/kalx-128.mp3", "8000"]
        _ -> args
 
    -- Create a named pipe if it doesn't exist. For some reason, this doesn't
    -- work right on my laptop (no matter what I do, the file created doesn't
    -- have the fifo bit). For now, you just need to create it yourself with
    -- mkfifo.
    exists <- fileExist fifoName
    if exists
        then return $ Left ()
        else return $ Right $ createNamedPipe fifoName fifoMode
 
    -- Connect to the mpeg stream, and get ready to read mpeg data
    sock <- connectTo domain $ PortNumber $ fromIntegral (read port :: Int)
    writeHTTPRequest sock $ makeHTTPRequest domain path
    skipResponseHeaders sock
 
    -- Open the named pipe in read/write mode
    fifo <- openFile fifoName ReadWriteMode
 
    -- Copy the contents of the socket to the named pipe in a separate "thread"
    myThreadId <- forkIO $ writeToFifo sock fifo
 
    -- GStreamer boilerplate
    Gst.init
    mainLoop <- G.mainLoopNew Nothing True
 
    pipeline <- Gst.pipelineNew "audio-player"
    source <- mkElement $ Gst.elementFactoryMake "filesrc" $ Just "file-source"
    decoder <- mkElement $ Gst.elementFactoryMake "mad" $ Just "mad-decoder"
    sink <- mkElement $ Gst.elementFactoryMake "pulsesink" $ Just "pulse-output"
 
    G.objectSetPropertyString "location" source fifoName
 
    bus <- Gst.pipelineGetBus (Gst.castToPipeline pipeline)
    Gst.busAddWatch bus G.priorityDefault $ \bus message ->
        do case Gst.messageType message of
            Gst.MessageEOS ->
                do putStrLn "end of stream"
                   G.mainLoopQuit mainLoop
            Gst.MessageError ->
                let G.GError _ _ msg = fst $ fromJust $ Gst.messageParseError message
                    messageStr = "Error: " ++ msg
                in do hPutStrLn stderr messageStr
                      G.mainLoopQuit mainLoop
            _ -> return ()
           return True
 
    mapM_ (Gst.binAdd $ Gst.castToBin pipeline) [source, decoder, sink]
 
    Gst.elementLink source decoder
    Gst.elementLink decoder sink
 
    G.timeoutAdd (return True) 100
 
    Gst.elementSetState pipeline Gst.StatePlaying
 
    G.mainLoopRun mainLoop
 
    Gst.elementSetState pipeline Gst.StateNull