fileplaylist.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. /*
  2. * DudelDu
  3. *
  4. * Copyright 2016 Matthias Ladkau. All rights reserved.
  5. *
  6. * This Source Code Form is subject to the terms of the MIT
  7. * License, If a copy of the MIT License was not distributed with this
  8. * file, You can obtain one at https://opensource.org/licenses/MIT.
  9. */
  10. /*
  11. Package playlist contains the default playlist implementation.
  12. FilePlaylistFactory
  13. FilePlaylistFactory is a PlaylistFactory which reads its definition from
  14. a file. The definition file is expected to be a JSON encoded datastructure of the form:
  15. {
  16. <web path> : [
  17. {
  18. "artist" : <artist>
  19. "title" : <title>
  20. "path" : <file path / url>
  21. }
  22. ]
  23. }
  24. The web path is the absolute path which may be requested by the streaming
  25. client (e.g. /foo/bar would be http://myserver:1234/foo/bar).
  26. The path is either a physical file or a web url reachable by the server process.
  27. The file ending determines the content type which is send to the client.
  28. */
  29. package playlist
  30. import (
  31. "crypto/tls"
  32. "encoding/json"
  33. "io"
  34. "io/ioutil"
  35. "math/rand"
  36. "net/http"
  37. "net/url"
  38. "os"
  39. "path/filepath"
  40. "sync"
  41. "time"
  42. "devt.de/krotik/common/stringutil"
  43. "devt.de/krotik/dudeldu"
  44. )
  45. /*
  46. FileExtContentTypes maps file extensions to content types
  47. */
  48. var FileExtContentTypes = map[string]string{
  49. ".mp3": "audio/mpeg",
  50. ".flac": "audio/flac",
  51. ".aac": "audio/x-aac",
  52. ".mp4a": "audio/mp4",
  53. ".mp4": "video/mp4",
  54. ".nsv": "video/nsv",
  55. ".ogg": "audio/ogg",
  56. ".spx": "audio/ogg",
  57. ".opus": "audio/ogg",
  58. ".oga": "audio/ogg",
  59. ".ogv": "video/ogg",
  60. ".weba": "audio/webm",
  61. ".webm": "video/webm",
  62. ".axa": "audio/annodex",
  63. ".axv": "video/annodex",
  64. }
  65. /*
  66. FrameSize is the frame size which is used by the playlists
  67. */
  68. var FrameSize = dudeldu.FrameSize
  69. /*
  70. FilePlaylistFactory data structure
  71. */
  72. type FilePlaylistFactory struct {
  73. data map[string][]map[string]string
  74. }
  75. /*
  76. NewFilePlaylistFactory creates a new FilePlaylistFactory from a given definition
  77. file.
  78. */
  79. func NewFilePlaylistFactory(path string) (*FilePlaylistFactory, error) {
  80. // Try to read the playlist file
  81. pl, err := ioutil.ReadFile(path)
  82. if err != nil {
  83. return nil, err
  84. }
  85. // Unmarshal json
  86. ret := &FilePlaylistFactory{}
  87. err = json.Unmarshal(pl, &ret.data)
  88. if err != nil {
  89. // Try again and strip out comments
  90. pl = stringutil.StripCStyleComments(pl)
  91. err = json.Unmarshal(pl, &ret.data)
  92. }
  93. if err != nil {
  94. return nil, err
  95. }
  96. return ret, nil
  97. }
  98. /*
  99. Playlist returns a playlist for a given path.
  100. */
  101. func (fp *FilePlaylistFactory) Playlist(path string, shuffle bool) dudeldu.Playlist {
  102. if data, ok := fp.data[path]; ok {
  103. // Check if the playlist should be shuffled
  104. if shuffle {
  105. r := rand.New(rand.NewSource(time.Now().UnixNano()))
  106. shuffledData := make([]map[string]string, len(data), len(data))
  107. for i, j := range r.Perm(len(data)) {
  108. shuffledData[i] = data[j]
  109. }
  110. data = shuffledData
  111. }
  112. return &FilePlaylist{path, 0, data, nil, false,
  113. &sync.Pool{New: func() interface{} { return make([]byte, FrameSize, FrameSize) }}}
  114. }
  115. return nil
  116. }
  117. /*
  118. FilePlaylist data structure
  119. */
  120. type FilePlaylist struct {
  121. path string // Path of this playlist
  122. current int // Pointer to the current playing item
  123. data []map[string]string // Playlist items
  124. stream io.ReadCloser // Current open stream
  125. finished bool // Flag if this playlist has finished
  126. framePool *sync.Pool // Pool for byte arrays
  127. }
  128. /*
  129. currentItem returns the current playlist item
  130. */
  131. func (fp *FilePlaylist) currentItem() map[string]string {
  132. if fp.current < len(fp.data) {
  133. return fp.data[fp.current]
  134. }
  135. return fp.data[len(fp.data)-1]
  136. }
  137. /*
  138. Name is the name of the playlist.
  139. */
  140. func (fp *FilePlaylist) Name() string {
  141. return fp.path
  142. }
  143. /*
  144. ContentType returns the content type of this playlist e.g. audio/mpeg.
  145. */
  146. func (fp *FilePlaylist) ContentType() string {
  147. ext := filepath.Ext(fp.currentItem()["path"])
  148. if ctype, ok := FileExtContentTypes[ext]; ok {
  149. return ctype
  150. }
  151. return "audio"
  152. }
  153. /*
  154. Artist returns the artist which is currently playing.
  155. */
  156. func (fp *FilePlaylist) Artist() string {
  157. return fp.currentItem()["artist"]
  158. }
  159. /*
  160. Title returns the title which is currently playing.
  161. */
  162. func (fp *FilePlaylist) Title() string {
  163. return fp.currentItem()["title"]
  164. }
  165. /*
  166. Frame returns the current audio frame which is playing.
  167. */
  168. func (fp *FilePlaylist) Frame() ([]byte, error) {
  169. var err error
  170. var frame []byte
  171. if fp.finished {
  172. return nil, dudeldu.ErrPlaylistEnd
  173. }
  174. if fp.stream == nil {
  175. // Make sure first file is loaded
  176. err = fp.nextFile()
  177. }
  178. if err == nil {
  179. // Get new byte array from a pool
  180. frame = fp.framePool.Get().([]byte)
  181. n := 0
  182. nn := 0
  183. for n < len(frame) && err == nil {
  184. nn, err = fp.stream.Read(frame[n:])
  185. n += nn
  186. // Check if we need to read the next file
  187. if n < len(frame) || err == io.EOF {
  188. err = fp.nextFile()
  189. }
  190. }
  191. // Make sure the frame has no old data if it was only partially filled
  192. if n == 0 {
  193. // Special case we reached the end of the playlist
  194. frame = nil
  195. if err != nil {
  196. err = dudeldu.ErrPlaylistEnd
  197. }
  198. } else if n < len(frame) {
  199. // Resize frame if we have less data
  200. frame = frame[:n]
  201. }
  202. }
  203. if err == dudeldu.ErrPlaylistEnd {
  204. fp.finished = true
  205. }
  206. return frame, err
  207. }
  208. /*
  209. nextFile jumps to the next file for the playlist.
  210. */
  211. func (fp *FilePlaylist) nextFile() error {
  212. var err error
  213. var stream io.ReadCloser
  214. // Except for the first call advance the current pointer
  215. if fp.stream != nil {
  216. fp.current++
  217. fp.stream.Close()
  218. fp.stream = nil
  219. // Return special error if the end of the playlist has been reached
  220. if fp.current >= len(fp.data) {
  221. return dudeldu.ErrPlaylistEnd
  222. }
  223. }
  224. // Check if a file is already open
  225. if fp.stream == nil {
  226. item := fp.currentItem()["path"]
  227. if _, err = url.ParseRequestURI(item); err == nil {
  228. var resp *http.Response
  229. // We got an url - access it without SSL verification
  230. client := &http.Client{Transport: &http.Transport{
  231. TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  232. }}
  233. resp, err = client.Get(item)
  234. stream = resp.Body
  235. } else {
  236. // Open a new file
  237. stream, err = os.Open(item)
  238. }
  239. if err != nil {
  240. // Jump to the next file if there is an error
  241. fp.current++
  242. return err
  243. }
  244. fp.stream = stream
  245. }
  246. return err
  247. }
  248. /*
  249. ReleaseFrame releases a frame which has been written to the client.
  250. */
  251. func (fp *FilePlaylist) ReleaseFrame(frame []byte) {
  252. if len(frame) == FrameSize {
  253. fp.framePool.Put(frame)
  254. }
  255. }
  256. /*
  257. Finished returns if the playlist has finished playing.
  258. */
  259. func (fp *FilePlaylist) Finished() bool {
  260. return fp.finished
  261. }
  262. /*
  263. Close any open files by this playlist and reset the current pointer. After this
  264. call the playlist can be played again.
  265. */
  266. func (fp *FilePlaylist) Close() error {
  267. if fp.stream != nil {
  268. fp.stream.Close()
  269. fp.stream = nil
  270. }
  271. fp.current = 0
  272. fp.finished = false
  273. return nil
  274. }