writer.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768
  1. package m3u8
  2. /*
  3. Part of M3U8 parser & generator library.
  4. This file defines functions related to playlist generation.
  5. Copyright 2013-2017 The Project Developers.
  6. See the AUTHORS and LICENSE files at the top-level directory of this distribution
  7. and at https://github.com/grafov/m3u8/
  8. ॐ तारे तुत्तारे तुरे स्व
  9. */
  10. import (
  11. "bytes"
  12. "errors"
  13. "fmt"
  14. "math"
  15. "strconv"
  16. "strings"
  17. "time"
  18. )
  19. var (
  20. ErrPlaylistFull = errors.New("playlist is full")
  21. )
  22. // Set version of the playlist accordingly with section 7
  23. func version(ver *uint8, newver uint8) {
  24. if *ver < newver {
  25. *ver = newver
  26. }
  27. }
  28. func strver(ver uint8) string {
  29. return strconv.FormatUint(uint64(ver), 10)
  30. }
  31. // Create new empty master playlist.
  32. // Master playlist consists of variants.
  33. func NewMasterPlaylist() *MasterPlaylist {
  34. p := new(MasterPlaylist)
  35. p.ver = minver
  36. return p
  37. }
  38. // Append variant to master playlist.
  39. // This operation does reset playlist cache.
  40. func (p *MasterPlaylist) Append(uri string, chunklist *MediaPlaylist, params VariantParams) {
  41. v := new(Variant)
  42. v.URI = uri
  43. v.Chunklist = chunklist
  44. v.VariantParams = params
  45. p.Variants = append(p.Variants, v)
  46. if len(v.Alternatives) > 0 {
  47. // From section 7:
  48. // The EXT-X-MEDIA tag and the AUDIO, VIDEO and SUBTITLES attributes of
  49. // the EXT-X-STREAM-INF tag are backward compatible to protocol version
  50. // 1, but playback on older clients may not be desirable. A server MAY
  51. // consider indicating a EXT-X-VERSION of 4 or higher in the Master
  52. // Playlist but is not required to do so.
  53. version(&p.ver, 4) // so it is optional and in theory may be set to ver.1
  54. // but more tests required
  55. }
  56. p.buf.Reset()
  57. }
  58. func (p *MasterPlaylist) ResetCache() {
  59. p.buf.Reset()
  60. }
  61. // Generate output in M3U8 format.
  62. func (p *MasterPlaylist) Encode() *bytes.Buffer {
  63. if p.buf.Len() > 0 {
  64. return &p.buf
  65. }
  66. p.buf.WriteString("#EXTM3U\n#EXT-X-VERSION:")
  67. p.buf.WriteString(strver(p.ver))
  68. p.buf.WriteRune('\n')
  69. var altsWritten map[string]bool = make(map[string]bool)
  70. for _, pl := range p.Variants {
  71. if pl.Alternatives != nil {
  72. for _, alt := range pl.Alternatives {
  73. // Make sure that we only write out an alternative once
  74. altKey := fmt.Sprintf("%s-%s-%s-%s", alt.Type, alt.GroupId, alt.Name, alt.Language)
  75. if altsWritten[altKey] {
  76. continue
  77. }
  78. altsWritten[altKey] = true
  79. p.buf.WriteString("#EXT-X-MEDIA:")
  80. if alt.Type != "" {
  81. p.buf.WriteString("TYPE=") // Type should not be quoted
  82. p.buf.WriteString(alt.Type)
  83. }
  84. if alt.GroupId != "" {
  85. p.buf.WriteString(",GROUP-ID=\"")
  86. p.buf.WriteString(alt.GroupId)
  87. p.buf.WriteRune('"')
  88. }
  89. if alt.Name != "" {
  90. p.buf.WriteString(",NAME=\"")
  91. p.buf.WriteString(alt.Name)
  92. p.buf.WriteRune('"')
  93. }
  94. p.buf.WriteString(",DEFAULT=")
  95. if alt.Default {
  96. p.buf.WriteString("YES")
  97. } else {
  98. p.buf.WriteString("NO")
  99. }
  100. if alt.Autoselect != "" {
  101. p.buf.WriteString(",AUTOSELECT=")
  102. p.buf.WriteString(alt.Autoselect)
  103. }
  104. if alt.Language != "" {
  105. p.buf.WriteString(",LANGUAGE=\"")
  106. p.buf.WriteString(alt.Language)
  107. p.buf.WriteRune('"')
  108. }
  109. if alt.Forced != "" {
  110. p.buf.WriteString(",FORCED=\"")
  111. p.buf.WriteString(alt.Forced)
  112. p.buf.WriteRune('"')
  113. }
  114. if alt.Characteristics != "" {
  115. p.buf.WriteString(",CHARACTERISTICS=\"")
  116. p.buf.WriteString(alt.Characteristics)
  117. p.buf.WriteRune('"')
  118. }
  119. if alt.Subtitles != "" {
  120. p.buf.WriteString(",SUBTITLES=\"")
  121. p.buf.WriteString(alt.Subtitles)
  122. p.buf.WriteRune('"')
  123. }
  124. if alt.URI != "" {
  125. p.buf.WriteString(",URI=\"")
  126. p.buf.WriteString(alt.URI)
  127. p.buf.WriteRune('"')
  128. }
  129. p.buf.WriteRune('\n')
  130. }
  131. }
  132. if pl.Iframe {
  133. p.buf.WriteString("#EXT-X-I-FRAME-STREAM-INF:PROGRAM-ID=")
  134. p.buf.WriteString(strconv.FormatUint(uint64(pl.ProgramId), 10))
  135. p.buf.WriteString(",BANDWIDTH=")
  136. p.buf.WriteString(strconv.FormatUint(uint64(pl.Bandwidth), 10))
  137. if pl.Codecs != "" {
  138. p.buf.WriteString(",CODECS=\"")
  139. p.buf.WriteString(pl.Codecs)
  140. p.buf.WriteRune('"')
  141. }
  142. if pl.Resolution != "" {
  143. p.buf.WriteString(",RESOLUTION=") // Resolution should not be quoted
  144. p.buf.WriteString(pl.Resolution)
  145. }
  146. if pl.Video != "" {
  147. p.buf.WriteString(",VIDEO=\"")
  148. p.buf.WriteString(pl.Video)
  149. p.buf.WriteRune('"')
  150. }
  151. if pl.URI != "" {
  152. p.buf.WriteString(",URI=\"")
  153. p.buf.WriteString(pl.URI)
  154. p.buf.WriteRune('"')
  155. }
  156. p.buf.WriteRune('\n')
  157. } else {
  158. p.buf.WriteString("#EXT-X-STREAM-INF:PROGRAM-ID=")
  159. p.buf.WriteString(strconv.FormatUint(uint64(pl.ProgramId), 10))
  160. p.buf.WriteString(",BANDWIDTH=")
  161. p.buf.WriteString(strconv.FormatUint(uint64(pl.Bandwidth), 10))
  162. if pl.Codecs != "" {
  163. p.buf.WriteString(",CODECS=\"")
  164. p.buf.WriteString(pl.Codecs)
  165. p.buf.WriteRune('"')
  166. }
  167. if pl.Resolution != "" {
  168. p.buf.WriteString(",RESOLUTION=") // Resolution should not be quoted
  169. p.buf.WriteString(pl.Resolution)
  170. }
  171. if pl.Audio != "" {
  172. p.buf.WriteString(",AUDIO=\"")
  173. p.buf.WriteString(pl.Audio)
  174. p.buf.WriteRune('"')
  175. }
  176. if pl.Video != "" {
  177. p.buf.WriteString(",VIDEO=\"")
  178. p.buf.WriteString(pl.Video)
  179. p.buf.WriteRune('"')
  180. }
  181. if pl.Captions != "" {
  182. p.buf.WriteString(",CLOSED-CAPTIONS=")
  183. if pl.Captions == "NONE" {
  184. p.buf.WriteString(pl.Captions) // CC should not be quoted when eq NONE
  185. } else {
  186. p.buf.WriteRune('"')
  187. p.buf.WriteString(pl.Captions)
  188. p.buf.WriteRune('"')
  189. }
  190. }
  191. if pl.Subtitles != "" {
  192. p.buf.WriteString(",SUBTITLES=\"")
  193. p.buf.WriteString(pl.Subtitles)
  194. p.buf.WriteRune('"')
  195. }
  196. if pl.Name != "" {
  197. p.buf.WriteString(",NAME=\"")
  198. p.buf.WriteString(pl.Name)
  199. p.buf.WriteRune('"')
  200. }
  201. p.buf.WriteRune('\n')
  202. p.buf.WriteString(pl.URI)
  203. if p.Args != "" {
  204. if strings.Contains(pl.URI, "?") {
  205. p.buf.WriteRune('&')
  206. } else {
  207. p.buf.WriteRune('?')
  208. }
  209. p.buf.WriteString(p.Args)
  210. }
  211. p.buf.WriteRune('\n')
  212. }
  213. }
  214. return &p.buf
  215. }
  216. // Version returns the current playlist version number
  217. func (p *MasterPlaylist) Version() uint8 {
  218. return p.ver
  219. }
  220. // SetVersion sets the playlist version number, note the version maybe changed
  221. // automatically by other Set methods.
  222. func (p *MasterPlaylist) SetVersion(ver uint8) {
  223. p.ver = ver
  224. }
  225. // For compatibility with Stringer interface
  226. // For example fmt.Printf("%s", sampleMediaList) will encode
  227. // playist and print its string representation.
  228. func (p *MasterPlaylist) String() string {
  229. return p.Encode().String()
  230. }
  231. // Creates new media playlist structure.
  232. // Winsize defines how much items will displayed on playlist generation.
  233. // Capacity is total size of a playlist.
  234. func NewMediaPlaylist(winsize uint, capacity uint) (*MediaPlaylist, error) {
  235. p := new(MediaPlaylist)
  236. p.ver = minver
  237. p.capacity = capacity
  238. if err := p.SetWinSize(winsize); err != nil {
  239. return nil, err
  240. }
  241. p.Segments = make([]*MediaSegment, capacity)
  242. return p, nil
  243. }
  244. // last returns the previously written segment's index
  245. func (p *MediaPlaylist) last() uint {
  246. if p.tail == 0 {
  247. return p.capacity - 1
  248. }
  249. return p.tail - 1
  250. }
  251. // Remove current segment from the head of chunk slice form a media playlist. Useful for sliding playlists.
  252. // This operation does reset playlist cache.
  253. func (p *MediaPlaylist) Remove() (err error) {
  254. if p.count == 0 {
  255. return errors.New("playlist is empty")
  256. }
  257. p.head = (p.head + 1) % p.capacity
  258. p.count--
  259. if !p.Closed {
  260. p.SeqNo++
  261. }
  262. p.buf.Reset()
  263. return nil
  264. }
  265. // Append general chunk to the tail of chunk slice for a media playlist.
  266. // This operation does reset playlist cache.
  267. func (p *MediaPlaylist) Append(uri string, duration float64, title string) error {
  268. seg := new(MediaSegment)
  269. seg.URI = uri
  270. seg.Duration = duration
  271. seg.Title = title
  272. return p.AppendSegment(seg)
  273. }
  274. // AppendSegment appends a MediaSegment to the tail of chunk slice for a media playlist.
  275. // This operation does reset playlist cache.
  276. func (p *MediaPlaylist) AppendSegment(seg *MediaSegment) error {
  277. if p.head == p.tail && p.count > 0 {
  278. return ErrPlaylistFull
  279. }
  280. p.Segments[p.tail] = seg
  281. p.tail = (p.tail + 1) % p.capacity
  282. p.count++
  283. if p.TargetDuration < seg.Duration {
  284. p.TargetDuration = math.Ceil(seg.Duration)
  285. }
  286. p.buf.Reset()
  287. return nil
  288. }
  289. // Combines two operations: firstly it removes one chunk from the head of chunk slice and move pointer to
  290. // next chunk. Secondly it appends one chunk to the tail of chunk slice. Useful for sliding playlists.
  291. // This operation does reset cache.
  292. func (p *MediaPlaylist) Slide(uri string, duration float64, title string) {
  293. if !p.Closed && p.count >= p.winsize {
  294. p.Remove()
  295. } else if !p.Closed {
  296. p.SeqNo++
  297. }
  298. p.Append(uri, duration, title)
  299. }
  300. // Reset playlist cache. Next called Encode() will regenerate playlist from the chunk slice.
  301. func (p *MediaPlaylist) ResetCache() {
  302. p.buf.Reset()
  303. }
  304. // Generate output in M3U8 format. Marshal `winsize` elements from bottom of the `segments` queue.
  305. func (p *MediaPlaylist) Encode() *bytes.Buffer {
  306. if p.buf.Len() > 0 {
  307. return &p.buf
  308. }
  309. p.buf.WriteString("#EXTM3U\n#EXT-X-VERSION:")
  310. p.buf.WriteString(strver(p.ver))
  311. p.buf.WriteRune('\n')
  312. // default key (workaround for Widevine)
  313. if p.Key != nil {
  314. p.buf.WriteString("#EXT-X-KEY:")
  315. p.buf.WriteString("METHOD=")
  316. p.buf.WriteString(p.Key.Method)
  317. if p.Key.Method != "NONE" {
  318. p.buf.WriteString(",URI=\"")
  319. p.buf.WriteString(p.Key.URI)
  320. p.buf.WriteRune('"')
  321. if p.Key.IV != "" {
  322. p.buf.WriteString(",IV=")
  323. p.buf.WriteString(p.Key.IV)
  324. }
  325. if p.Key.Keyformat != "" {
  326. p.buf.WriteString(",KEYFORMAT=\"")
  327. p.buf.WriteString(p.Key.Keyformat)
  328. p.buf.WriteRune('"')
  329. }
  330. if p.Key.Keyformatversions != "" {
  331. p.buf.WriteString(",KEYFORMATVERSIONS=\"")
  332. p.buf.WriteString(p.Key.Keyformatversions)
  333. p.buf.WriteRune('"')
  334. }
  335. }
  336. p.buf.WriteRune('\n')
  337. }
  338. if p.Map != nil {
  339. p.buf.WriteString("#EXT-X-MAP:")
  340. p.buf.WriteString("URI=\"")
  341. p.buf.WriteString(p.Map.URI)
  342. p.buf.WriteRune('"')
  343. if p.Map.Limit > 0 {
  344. p.buf.WriteString(",BYTERANGE=")
  345. p.buf.WriteString(strconv.FormatInt(p.Map.Limit, 10))
  346. p.buf.WriteRune('@')
  347. p.buf.WriteString(strconv.FormatInt(p.Map.Offset, 10))
  348. }
  349. p.buf.WriteRune('\n')
  350. }
  351. if p.MediaType > 0 {
  352. p.buf.WriteString("#EXT-X-PLAYLIST-TYPE:")
  353. switch p.MediaType {
  354. case EVENT:
  355. p.buf.WriteString("EVENT\n")
  356. p.buf.WriteString("#EXT-X-ALLOW-CACHE:NO\n")
  357. case VOD:
  358. p.buf.WriteString("VOD\n")
  359. }
  360. }
  361. p.buf.WriteString("#EXT-X-MEDIA-SEQUENCE:")
  362. p.buf.WriteString(strconv.FormatUint(p.SeqNo, 10))
  363. p.buf.WriteRune('\n')
  364. p.buf.WriteString("#EXT-X-TARGETDURATION:")
  365. p.buf.WriteString(strconv.FormatInt(int64(math.Ceil(p.TargetDuration)), 10)) // due section 3.4.2 of M3U8 specs EXT-X-TARGETDURATION must be integer
  366. p.buf.WriteRune('\n')
  367. if p.Iframe {
  368. p.buf.WriteString("#EXT-X-I-FRAMES-ONLY\n")
  369. }
  370. // Widevine tags
  371. if p.WV != nil {
  372. if p.WV.AudioChannels != 0 {
  373. p.buf.WriteString("#WV-AUDIO-CHANNELS ")
  374. p.buf.WriteString(strconv.FormatUint(uint64(p.WV.AudioChannels), 10))
  375. p.buf.WriteRune('\n')
  376. }
  377. if p.WV.AudioFormat != 0 {
  378. p.buf.WriteString("#WV-AUDIO-FORMAT ")
  379. p.buf.WriteString(strconv.FormatUint(uint64(p.WV.AudioFormat), 10))
  380. p.buf.WriteRune('\n')
  381. }
  382. if p.WV.AudioProfileIDC != 0 {
  383. p.buf.WriteString("#WV-AUDIO-PROFILE-IDC ")
  384. p.buf.WriteString(strconv.FormatUint(uint64(p.WV.AudioProfileIDC), 10))
  385. p.buf.WriteRune('\n')
  386. }
  387. if p.WV.AudioSampleSize != 0 {
  388. p.buf.WriteString("#WV-AUDIO-SAMPLE-SIZE ")
  389. p.buf.WriteString(strconv.FormatUint(uint64(p.WV.AudioSampleSize), 10))
  390. p.buf.WriteRune('\n')
  391. }
  392. if p.WV.AudioSamplingFrequency != 0 {
  393. p.buf.WriteString("#WV-AUDIO-SAMPLING-FREQUENCY ")
  394. p.buf.WriteString(strconv.FormatUint(uint64(p.WV.AudioSamplingFrequency), 10))
  395. p.buf.WriteRune('\n')
  396. }
  397. if p.WV.CypherVersion != "" {
  398. p.buf.WriteString("#WV-CYPHER-VERSION ")
  399. p.buf.WriteString(p.WV.CypherVersion)
  400. p.buf.WriteRune('\n')
  401. }
  402. if p.WV.ECM != "" {
  403. p.buf.WriteString("#WV-ECM ")
  404. p.buf.WriteString(p.WV.ECM)
  405. p.buf.WriteRune('\n')
  406. }
  407. if p.WV.VideoFormat != 0 {
  408. p.buf.WriteString("#WV-VIDEO-FORMAT ")
  409. p.buf.WriteString(strconv.FormatUint(uint64(p.WV.VideoFormat), 10))
  410. p.buf.WriteRune('\n')
  411. }
  412. if p.WV.VideoFrameRate != 0 {
  413. p.buf.WriteString("#WV-VIDEO-FRAME-RATE ")
  414. p.buf.WriteString(strconv.FormatUint(uint64(p.WV.VideoFrameRate), 10))
  415. p.buf.WriteRune('\n')
  416. }
  417. if p.WV.VideoLevelIDC != 0 {
  418. p.buf.WriteString("#WV-VIDEO-LEVEL-IDC")
  419. p.buf.WriteString(strconv.FormatUint(uint64(p.WV.VideoLevelIDC), 10))
  420. p.buf.WriteRune('\n')
  421. }
  422. if p.WV.VideoProfileIDC != 0 {
  423. p.buf.WriteString("#WV-VIDEO-PROFILE-IDC ")
  424. p.buf.WriteString(strconv.FormatUint(uint64(p.WV.VideoProfileIDC), 10))
  425. p.buf.WriteRune('\n')
  426. }
  427. if p.WV.VideoResolution != "" {
  428. p.buf.WriteString("#WV-VIDEO-RESOLUTION ")
  429. p.buf.WriteString(p.WV.VideoResolution)
  430. p.buf.WriteRune('\n')
  431. }
  432. if p.WV.VideoSAR != "" {
  433. p.buf.WriteString("#WV-VIDEO-SAR ")
  434. p.buf.WriteString(p.WV.VideoSAR)
  435. p.buf.WriteRune('\n')
  436. }
  437. }
  438. var (
  439. seg *MediaSegment
  440. durationCache = make(map[float64]string)
  441. )
  442. head := p.head
  443. count := p.count
  444. for i := uint(0); (i < p.winsize || p.winsize == 0) && count > 0; count-- {
  445. seg = p.Segments[head]
  446. head = (head + 1) % p.capacity
  447. if seg == nil { // protection from badly filled chunklists
  448. continue
  449. }
  450. if p.winsize > 0 { // skip for VOD playlists, where winsize = 0
  451. i++
  452. }
  453. if seg.SCTE != nil {
  454. switch seg.SCTE.Syntax {
  455. case SCTE35_67_2014:
  456. p.buf.WriteString("#EXT-SCTE35:")
  457. p.buf.WriteString("CUE=\"")
  458. p.buf.WriteString(seg.SCTE.Cue)
  459. p.buf.WriteRune('"')
  460. if seg.SCTE.ID != "" {
  461. p.buf.WriteString(",ID=\"")
  462. p.buf.WriteString(seg.SCTE.ID)
  463. p.buf.WriteRune('"')
  464. }
  465. if seg.SCTE.Time != 0 {
  466. p.buf.WriteString(",TIME=")
  467. p.buf.WriteString(strconv.FormatFloat(seg.SCTE.Time, 'f', -1, 64))
  468. }
  469. p.buf.WriteRune('\n')
  470. case SCTE35_OATCLS:
  471. switch seg.SCTE.CueType {
  472. case SCTE35Cue_Start:
  473. p.buf.WriteString("#EXT-OATCLS-SCTE35:")
  474. p.buf.WriteString(seg.SCTE.Cue)
  475. p.buf.WriteRune('\n')
  476. p.buf.WriteString("#EXT-X-CUE-OUT:")
  477. p.buf.WriteString(strconv.FormatFloat(seg.SCTE.Time, 'f', -1, 64))
  478. p.buf.WriteRune('\n')
  479. case SCTE35Cue_Mid:
  480. p.buf.WriteString("#EXT-X-CUE-OUT-CONT:")
  481. p.buf.WriteString("ElapsedTime=")
  482. p.buf.WriteString(strconv.FormatFloat(seg.SCTE.Elapsed, 'f', -1, 64))
  483. p.buf.WriteString(",Duration=")
  484. p.buf.WriteString(strconv.FormatFloat(seg.SCTE.Time, 'f', -1, 64))
  485. p.buf.WriteString(",SCTE35=")
  486. p.buf.WriteString(seg.SCTE.Cue)
  487. p.buf.WriteRune('\n')
  488. case SCTE35Cue_End:
  489. p.buf.WriteString("#EXT-X-CUE-IN")
  490. p.buf.WriteRune('\n')
  491. }
  492. }
  493. }
  494. // check for key change
  495. if seg.Key != nil && p.Key != seg.Key {
  496. p.buf.WriteString("#EXT-X-KEY:")
  497. p.buf.WriteString("METHOD=")
  498. p.buf.WriteString(seg.Key.Method)
  499. if seg.Key.Method != "NONE" {
  500. p.buf.WriteString(",URI=\"")
  501. p.buf.WriteString(seg.Key.URI)
  502. p.buf.WriteRune('"')
  503. if seg.Key.IV != "" {
  504. p.buf.WriteString(",IV=")
  505. p.buf.WriteString(seg.Key.IV)
  506. }
  507. if seg.Key.Keyformat != "" {
  508. p.buf.WriteString(",KEYFORMAT=\"")
  509. p.buf.WriteString(seg.Key.Keyformat)
  510. p.buf.WriteRune('"')
  511. }
  512. if seg.Key.Keyformatversions != "" {
  513. p.buf.WriteString(",KEYFORMATVERSIONS=\"")
  514. p.buf.WriteString(seg.Key.Keyformatversions)
  515. p.buf.WriteRune('"')
  516. }
  517. }
  518. p.buf.WriteRune('\n')
  519. }
  520. if seg.Discontinuity {
  521. p.buf.WriteString("#EXT-X-DISCONTINUITY\n")
  522. }
  523. // ignore segment Map if default playlist Map is present
  524. if p.Map == nil && seg.Map != nil {
  525. p.buf.WriteString("#EXT-X-MAP:")
  526. p.buf.WriteString("URI=\"")
  527. p.buf.WriteString(seg.Map.URI)
  528. p.buf.WriteRune('"')
  529. if seg.Map.Limit > 0 {
  530. p.buf.WriteString(",BYTERANGE=")
  531. p.buf.WriteString(strconv.FormatInt(seg.Map.Limit, 10))
  532. p.buf.WriteRune('@')
  533. p.buf.WriteString(strconv.FormatInt(seg.Map.Offset, 10))
  534. }
  535. p.buf.WriteRune('\n')
  536. }
  537. if !seg.ProgramDateTime.IsZero() {
  538. p.buf.WriteString("#EXT-X-PROGRAM-DATE-TIME:")
  539. p.buf.WriteString(seg.ProgramDateTime.Format(DATETIME))
  540. p.buf.WriteRune('\n')
  541. }
  542. if seg.Limit > 0 {
  543. p.buf.WriteString("#EXT-X-BYTERANGE:")
  544. p.buf.WriteString(strconv.FormatInt(seg.Limit, 10))
  545. p.buf.WriteRune('@')
  546. p.buf.WriteString(strconv.FormatInt(seg.Offset, 10))
  547. p.buf.WriteRune('\n')
  548. }
  549. p.buf.WriteString("#EXTINF:")
  550. if str, ok := durationCache[seg.Duration]; ok {
  551. p.buf.WriteString(str)
  552. } else {
  553. if p.durationAsInt {
  554. // Old Android players has problems with non integer Duration.
  555. durationCache[seg.Duration] = strconv.FormatInt(int64(math.Ceil(seg.Duration)), 10)
  556. } else {
  557. // Wowza Mediaserver and some others prefer floats.
  558. durationCache[seg.Duration] = strconv.FormatFloat(seg.Duration, 'f', 3, 32)
  559. }
  560. p.buf.WriteString(durationCache[seg.Duration])
  561. }
  562. p.buf.WriteRune(',')
  563. p.buf.WriteString(seg.Title)
  564. p.buf.WriteRune('\n')
  565. p.buf.WriteString(seg.URI)
  566. if p.Args != "" {
  567. p.buf.WriteRune('?')
  568. p.buf.WriteString(p.Args)
  569. }
  570. p.buf.WriteRune('\n')
  571. }
  572. if p.Closed {
  573. p.buf.WriteString("#EXT-X-ENDLIST\n")
  574. }
  575. return &p.buf
  576. }
  577. // For compatibility with Stringer interface
  578. // For example fmt.Printf("%s", sampleMediaList) will encode
  579. // playist and print its string representation.
  580. func (p *MediaPlaylist) String() string {
  581. return p.Encode().String()
  582. }
  583. // TargetDuration will be int on Encode
  584. func (p *MediaPlaylist) DurationAsInt(yes bool) {
  585. if yes {
  586. // duration must be integers if protocol version is less than 3
  587. version(&p.ver, 3)
  588. }
  589. p.durationAsInt = yes
  590. }
  591. // Count tells us the number of items that are currently in the media playlist
  592. func (p *MediaPlaylist) Count() uint {
  593. return p.count
  594. }
  595. // Close sliding playlist and make them fixed.
  596. func (p *MediaPlaylist) Close() {
  597. if p.buf.Len() > 0 {
  598. p.buf.WriteString("#EXT-X-ENDLIST\n")
  599. }
  600. p.Closed = true
  601. }
  602. // Set encryption key appeared once in header of the playlist (pointer to MediaPlaylist.Key).
  603. // It useful when keys not changed during playback.
  604. // Set tag for the whole list.
  605. func (p *MediaPlaylist) SetDefaultKey(method, uri, iv, keyformat, keyformatversions string) error {
  606. // A Media Playlist MUST indicate a EXT-X-VERSION of 5 or higher if it
  607. // contains:
  608. // - The KEYFORMAT and KEYFORMATVERSIONS attributes of the EXT-X-KEY tag.
  609. if keyformat != "" || keyformatversions != "" {
  610. version(&p.ver, 5)
  611. }
  612. p.Key = &Key{method, uri, iv, keyformat, keyformatversions}
  613. return nil
  614. }
  615. // Set default Media Initialization Section values for playlist (pointer to MediaPlaylist.Map).
  616. // Set EXT-X-MAP tag for the whole playlist.
  617. func (p *MediaPlaylist) SetDefaultMap(uri string, limit, offset int64) {
  618. version(&p.ver, 5) // due section 4
  619. p.Map = &Map{uri, limit, offset}
  620. }
  621. // Mark medialist as consists of only I-frames (Intra frames).
  622. // Set tag for the whole list.
  623. func (p *MediaPlaylist) SetIframeOnly() {
  624. version(&p.ver, 4) // due section 4.3.3
  625. p.Iframe = true
  626. }
  627. // Set encryption key for the current segment of media playlist (pointer to Segment.Key)
  628. func (p *MediaPlaylist) SetKey(method, uri, iv, keyformat, keyformatversions string) error {
  629. if p.count == 0 {
  630. return errors.New("playlist is empty")
  631. }
  632. // A Media Playlist MUST indicate a EXT-X-VERSION of 5 or higher if it
  633. // contains:
  634. // - The KEYFORMAT and KEYFORMATVERSIONS attributes of the EXT-X-KEY tag.
  635. if keyformat != "" || keyformatversions != "" {
  636. version(&p.ver, 5)
  637. }
  638. p.Segments[p.last()].Key = &Key{method, uri, iv, keyformat, keyformatversions}
  639. return nil
  640. }
  641. // Set map for the current segment of media playlist (pointer to Segment.Map)
  642. func (p *MediaPlaylist) SetMap(uri string, limit, offset int64) error {
  643. if p.count == 0 {
  644. return errors.New("playlist is empty")
  645. }
  646. version(&p.ver, 5) // due section 4
  647. p.Segments[p.last()].Map = &Map{uri, limit, offset}
  648. return nil
  649. }
  650. // Set limit and offset for the current media segment (EXT-X-BYTERANGE support for protocol version 4).
  651. func (p *MediaPlaylist) SetRange(limit, offset int64) error {
  652. if p.count == 0 {
  653. return errors.New("playlist is empty")
  654. }
  655. version(&p.ver, 4) // due section 3.4.1
  656. p.Segments[p.last()].Limit = limit
  657. p.Segments[p.last()].Offset = offset
  658. return nil
  659. }
  660. // SetSCTE sets the SCTE cue format for the current media segment.
  661. //
  662. // Deprecated: Use SetSCTE35 instead.
  663. func (p *MediaPlaylist) SetSCTE(cue string, id string, time float64) error {
  664. return p.SetSCTE35(&SCTE{Syntax: SCTE35_67_2014, Cue: cue, ID: id, Time: time})
  665. }
  666. // SetSCTE35 sets the SCTE cue format for the current media segment
  667. func (p *MediaPlaylist) SetSCTE35(scte35 *SCTE) error {
  668. if p.count == 0 {
  669. return errors.New("playlist is empty")
  670. }
  671. p.Segments[p.last()].SCTE = scte35
  672. return nil
  673. }
  674. // Set discontinuity flag for the current media segment.
  675. // EXT-X-DISCONTINUITY indicates an encoding discontinuity between the media segment
  676. // that follows it and the one that preceded it (i.e. file format, number and type of tracks,
  677. // encoding parameters, encoding sequence, timestamp sequence).
  678. func (p *MediaPlaylist) SetDiscontinuity() error {
  679. if p.count == 0 {
  680. return errors.New("playlist is empty")
  681. }
  682. p.Segments[p.last()].Discontinuity = true
  683. return nil
  684. }
  685. // Set program date and time for the current media segment.
  686. // EXT-X-PROGRAM-DATE-TIME tag associates the first sample of a
  687. // media segment with an absolute date and/or time. It applies only
  688. // to the current media segment.
  689. // Date/time format is YYYY-MM-DDThh:mm:ssZ (ISO8601) and includes time zone.
  690. func (p *MediaPlaylist) SetProgramDateTime(value time.Time) error {
  691. if p.count == 0 {
  692. return errors.New("playlist is empty")
  693. }
  694. p.Segments[p.last()].ProgramDateTime = value
  695. return nil
  696. }
  697. // Version returns the current playlist version number
  698. func (p *MediaPlaylist) Version() uint8 {
  699. return p.ver
  700. }
  701. // SetVersion sets the playlist version number, note the version maybe changed
  702. // automatically by other Set methods.
  703. func (p *MediaPlaylist) SetVersion(ver uint8) {
  704. p.ver = ver
  705. }
  706. // WinSize returns the playlist's window size.
  707. func (p *MediaPlaylist) WinSize() uint {
  708. return p.winsize
  709. }
  710. // SetWinSize overwrites the playlist's window size.
  711. func (p *MediaPlaylist) SetWinSize(winsize uint) error {
  712. if winsize > p.capacity {
  713. return errors.New("capacity must be greater than winsize or equal")
  714. }
  715. p.winsize = winsize
  716. return nil
  717. }