fileplaylist.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  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>
  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 file path is a physical file reachable by the server process. The file
  27. ending determines the content type which is send to the client.
  28. */
  29. package playlist
  30. import (
  31. "encoding/json"
  32. "io/ioutil"
  33. "math/rand"
  34. "os"
  35. "path/filepath"
  36. "sync"
  37. "time"
  38. "devt.de/krotik/common/stringutil"
  39. "devt.de/krotik/dudeldu"
  40. )
  41. /*
  42. FileExtContentTypes maps file extensions to content types
  43. */
  44. var FileExtContentTypes = map[string]string{
  45. ".mp3": "audio/mpeg",
  46. ".flac": "audio/flac",
  47. ".aac": "audio/x-aac",
  48. ".mp4a": "audio/mp4",
  49. ".mp4": "video/mp4",
  50. ".nsv": "video/nsv",
  51. ".ogg": "audio/ogg",
  52. ".spx": "audio/ogg",
  53. ".opus": "audio/ogg",
  54. ".oga": "audio/ogg",
  55. ".ogv": "video/ogg",
  56. ".weba": "audio/webm",
  57. ".webm": "video/webm",
  58. ".axa": "audio/annodex",
  59. ".axv": "video/annodex",
  60. }
  61. /*
  62. FrameSize is the frame size which is used by the playlists
  63. */
  64. var FrameSize = dudeldu.FrameSize
  65. /*
  66. FilePlaylistFactory data structure
  67. */
  68. type FilePlaylistFactory struct {
  69. data map[string][]map[string]string
  70. }
  71. /*
  72. NewFilePlaylistFactory creates a new FilePlaylistFactory from a given definition
  73. file.
  74. */
  75. func NewFilePlaylistFactory(path string) (*FilePlaylistFactory, error) {
  76. // Try to read the playlist file
  77. pl, err := ioutil.ReadFile(path)
  78. if err != nil {
  79. return nil, err
  80. }
  81. // Strip out comments
  82. pl = stringutil.StripCStyleComments(pl)
  83. // Unmarshal json
  84. ret := &FilePlaylistFactory{}
  85. err = json.Unmarshal(pl, &ret.data)
  86. if err != nil {
  87. return nil, err
  88. }
  89. return ret, nil
  90. }
  91. /*
  92. Playlist returns a playlist for a given path.
  93. */
  94. func (fp *FilePlaylistFactory) Playlist(path string, shuffle bool) dudeldu.Playlist {
  95. if data, ok := fp.data[path]; ok {
  96. // Check if the playlist should be shuffled
  97. if shuffle {
  98. r := rand.New(rand.NewSource(time.Now().UnixNano()))
  99. shuffledData := make([]map[string]string, len(data), len(data))
  100. for i, j := range r.Perm(len(data)) {
  101. shuffledData[i] = data[j]
  102. }
  103. data = shuffledData
  104. }
  105. return &FilePlaylist{path, 0, data, nil, false,
  106. &sync.Pool{New: func() interface{} { return make([]byte, FrameSize, FrameSize) }}}
  107. }
  108. return nil
  109. }
  110. /*
  111. FilePlaylist data structure
  112. */
  113. type FilePlaylist struct {
  114. path string // Path of this playlist
  115. current int // Pointer to the current playing item
  116. data []map[string]string // Playlist items
  117. file *os.File // Current open file
  118. finished bool // Flag if this playlist has finished
  119. framePool *sync.Pool // Pool for byte arrays
  120. }
  121. /*
  122. currentItem returns the current playlist item
  123. */
  124. func (fp *FilePlaylist) currentItem() map[string]string {
  125. if fp.current < len(fp.data) {
  126. return fp.data[fp.current]
  127. }
  128. return fp.data[len(fp.data)-1]
  129. }
  130. /*
  131. Name is the name of the playlist.
  132. */
  133. func (fp *FilePlaylist) Name() string {
  134. return fp.path
  135. }
  136. /*
  137. ContentType returns the content type of this playlist e.g. audio/mpeg.
  138. */
  139. func (fp *FilePlaylist) ContentType() string {
  140. ext := filepath.Ext(fp.currentItem()["path"])
  141. if ctype, ok := FileExtContentTypes[ext]; ok {
  142. return ctype
  143. }
  144. return "audio"
  145. }
  146. /*
  147. Artist returns the artist which is currently playing.
  148. */
  149. func (fp *FilePlaylist) Artist() string {
  150. return fp.currentItem()["artist"]
  151. }
  152. /*
  153. Title returns the title which is currently playing.
  154. */
  155. func (fp *FilePlaylist) Title() string {
  156. return fp.currentItem()["title"]
  157. }
  158. /*
  159. Frame returns the current audio frame which is playing.
  160. */
  161. func (fp *FilePlaylist) Frame() ([]byte, error) {
  162. var err error
  163. var frame []byte
  164. if fp.finished {
  165. return nil, dudeldu.ErrPlaylistEnd
  166. }
  167. if fp.file == nil {
  168. // Make sure first file is loaded
  169. err = fp.nextFile()
  170. }
  171. if err == nil {
  172. // Get new byte array from a pool
  173. frame = fp.framePool.Get().([]byte)
  174. n := 0
  175. nn := 0
  176. for n < len(frame) && err == nil {
  177. nn, err = fp.file.Read(frame[n:])
  178. n += nn
  179. // Check if we need to read the next file
  180. if n < len(frame) {
  181. err = fp.nextFile()
  182. }
  183. }
  184. // Make sure the frame has no old data if it was only partially filled
  185. if n == 0 {
  186. // Special case we reached the end of the playlist
  187. frame = nil
  188. if err != nil {
  189. err = dudeldu.ErrPlaylistEnd
  190. }
  191. } else if n < len(frame) {
  192. // Resize frame if we have less data
  193. frame = frame[:n]
  194. }
  195. }
  196. if err == dudeldu.ErrPlaylistEnd {
  197. fp.finished = true
  198. }
  199. return frame, err
  200. }
  201. /*
  202. nextFile jumps to the next file for the playlist.
  203. */
  204. func (fp *FilePlaylist) nextFile() error {
  205. // Except for the first call advance the current pointer
  206. if fp.file != nil {
  207. fp.current++
  208. fp.file.Close()
  209. fp.file = nil
  210. // Return special error if the end of the playlist has been reached
  211. if fp.current >= len(fp.data) {
  212. return dudeldu.ErrPlaylistEnd
  213. }
  214. }
  215. // Check if a file is already open
  216. if fp.file == nil {
  217. // Open a new file
  218. f, err := os.Open(fp.currentItem()["path"])
  219. if err != nil {
  220. // Jump to the next file if there is an error
  221. fp.current++
  222. return err
  223. }
  224. fp.file = f
  225. }
  226. return nil
  227. }
  228. /*
  229. ReleaseFrame releases a frame which has been written to the client.
  230. */
  231. func (fp *FilePlaylist) ReleaseFrame(frame []byte) {
  232. if len(frame) == FrameSize {
  233. fp.framePool.Put(frame)
  234. }
  235. }
  236. /*
  237. Finished returns if the playlist has finished playing.
  238. */
  239. func (fp *FilePlaylist) Finished() bool {
  240. return fp.finished
  241. }
  242. /*
  243. Close any open files by this playlist and reset the current pointer. After this
  244. call the playlist can be played again.
  245. */
  246. func (fp *FilePlaylist) Close() error {
  247. if fp.file != nil {
  248. fp.file.Close()
  249. fp.file = nil
  250. }
  251. fp.current = 0
  252. fp.finished = false
  253. return nil
  254. }