commit 7a2953fc8d153c7191653af6053a3aca08e446bf
parent 6811a7400504184e126533848180e2aafb8e7aad
Author: Oliver Lowe <o@olowe.co>
Date: Tue, 30 Apr 2024 17:51:14 +1000
internal/scte35: import github.com/comcast/scte35-go@v1.4.5
Starting a soft fork in hopes of making it easier to maintain, fewer
dependencies and no CLA.
Diffstat:
30 files changed, 5133 insertions(+), 0 deletions(-)
diff --git a/go.mod b/go.mod
@@ -1,3 +1,15 @@
module github.com/untangledco/streaming
go 1.19
+
+require (
+ github.com/bamiaux/iobit v0.0.0-20170418073505-498159a04883
+ github.com/stretchr/testify v1.9.0
+ golang.org/x/text v0.14.0
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
@@ -0,0 +1,14 @@
+github.com/bamiaux/iobit v0.0.0-20170418073505-498159a04883 h1:XNtOMwxmV2PI/vuTHDZnFzGIFNUh8MK73q7+Kna7AXs=
+github.com/bamiaux/iobit v0.0.0-20170418073505-498159a04883/go.mod h1:9IjZnSQGh45J46HHS45pxuMJ6WFTtSXbaX0FoHDvxh8=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/scte35/LICENSE b/internal/scte35/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/internal/scte35/NOTICE.md b/internal/scte35/NOTICE.md
@@ -0,0 +1,23 @@
+SCTE-35
+
+Copyright 2021 Comcast Cable Communications Management, LLC
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+SPDX-License-Identifier: Apache-2.0
+
+This product includes software developed at Comcast (http://www.comcast.com/).
diff --git a/internal/scte35/README b/internal/scte35/README
@@ -0,0 +1,7 @@
+This is an in-progress fork of github.com/comcast/scte35-go@v1.4.5.
+The goals are:
+
+- remove dependencies
+- improve godoc with examples, idioms
+- no contributor license agreement (CLA)
+- ...
diff --git a/internal/scte35/audio_descriptor.go b/internal/scte35/audio_descriptor.go
@@ -0,0 +1,143 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "encoding/xml"
+ "fmt"
+ "strconv"
+
+ "github.com/bamiaux/iobit"
+)
+
+const (
+ // AudioDescriptorTag is the splice_descriptor_tag for a audio descriptor.
+ AudioDescriptorTag = 0x04
+)
+
+// AudioDescriptor is an implementation of a audio_descriptor. The
+// audio_descriptor() should be used when programmers and/or MVPDs do not
+// support dynamic signaling (e.g., signaling of audio language changes) and
+// with legacy audio formats that do not support dynamic signaling.
+type AudioDescriptor struct {
+ XMLName xml.Name `xml:"http://www.scte.org/schemas/35 AudioDescriptor" json:"-"`
+ JSONType uint32 `xml:"-" json:"type"`
+ AudioChannels []AudioChannel `xml:"http://www.scte.org/schemas/35 AudioChannel" json:"audioChannels"`
+}
+
+// Tag returns the splice_descriptor_tag.
+func (sd *AudioDescriptor) Tag() uint32 {
+ // ensure JSONType is set
+ sd.JSONType = AudioDescriptorTag
+ return AudioDescriptorTag
+}
+
+// decode updates this SpliceDescriptor from binary.
+func (sd *AudioDescriptor) decode(b []byte) error {
+ r := iobit.NewReader(b)
+ r.Skip(8) // splice_descriptor_tag
+ r.Skip(8) // descriptor_length
+ r.Skip(32) // identifier
+ audioCount := int(r.Uint32(4))
+ r.Skip(4) // reserved
+ sd.AudioChannels = make([]AudioChannel, audioCount)
+ for i := 0; i < audioCount; i++ {
+ ac := AudioChannel{}
+ ac.ComponentTag = r.Uint32(8)
+ ac.ISOCode = r.String(3)
+ ac.BitStreamMode = r.Uint32(3)
+ ac.NumChannels = r.Uint32(4)
+ ac.FullSrvcAudio = r.Bit()
+ sd.AudioChannels[i] = ac
+ }
+
+ if err := readerError(r); err != nil {
+ return fmt.Errorf("audio_descriptor: %w", err)
+ }
+ return nil
+}
+
+// encode this SpliceDescriptor to binary.
+func (sd *AudioDescriptor) encode() ([]byte, error) {
+ length := sd.length()
+
+ // add 2 bytes to contain splice_descriptor_tag & descriptor_length
+ buf := make([]byte, length+2)
+ iow := iobit.NewWriter(buf)
+ iow.PutUint32(8, AudioDescriptorTag)
+ iow.PutUint32(8, uint32(length))
+ iow.PutUint32(32, CUEIdentifier)
+ iow.PutUint32(8, uint32(len(sd.AudioChannels)))
+ iow.PutUint32(4, Reserved)
+ for _, ad := range sd.AudioChannels {
+ iow.PutUint32(8, ad.ComponentTag)
+ _, _ = iow.Write([]byte(ad.ISOCode))
+ iow.PutUint32(3, ad.BitStreamMode)
+ iow.PutUint32(4, ad.NumChannels)
+ iow.PutBit(ad.FullSrvcAudio)
+ }
+ return buf, nil
+}
+
+// descriptorLength returns the descriptor_length
+func (sd *AudioDescriptor) length() int {
+ length := 32 // identifier
+ length += 4 // audio_count
+ length += 4 // reserved
+ for i := range sd.AudioChannels {
+ length += sd.AudioChannels[i].length() * 8
+ }
+ return length / 8
+}
+
+// writeTo the given table.
+func (sd *AudioDescriptor) writeTo(t *table) {
+ t.row(0, "audio_descriptor() {", nil)
+ t.row(1, "splice_descriptor_tag", fmt.Sprintf("%#02x", sd.Tag()))
+ t.row(1, "descriptor_length", sd.length())
+ t.row(1, "identifier", fmt.Sprintf("%#08x, (%s)", CUEIdentifier, CUEIASCII))
+ t.row(1, "audio_count", len(sd.AudioChannels))
+ for i, ac := range sd.AudioChannels {
+ t.row(1, "audio_channel["+strconv.Itoa(i)+"] {", nil)
+ t.row(2, "component_tag", ac.ComponentTag)
+ t.row(2, "iso_code", ac.ISOCode)
+ t.row(2, "bit_stream_mode", ac.BitStreamMode)
+ t.row(2, "num_channels", ac.NumChannels)
+ t.row(2, "full_srvc_audio", ac.FullSrvcAudio)
+ t.row(1, "}", nil)
+ }
+ t.row(0, "}", nil)
+}
+
+// AudioChannel collects the audio PID details.
+type AudioChannel struct {
+ ComponentTag uint32 `xml:"componentTag,attr" json:"componentTag"`
+ ISOCode string `xml:"ISOCode,attr" json:"isoCode"`
+ BitStreamMode uint32 `xml:"BitStreamMode,attr" json:"bitStreamMode"`
+ NumChannels uint32 `xml:"NumChannels,attr" json:"numChannels"`
+ FullSrvcAudio bool `xml:"FullSrvcAudio,attr" json:"fullSrvcAudio"`
+}
+
+// length returns audio_channel length.
+func (ac *AudioChannel) length() int {
+ length := 8 // component_tag
+ length += 24 // iso_code
+ length += 3 // bit_stream_mode
+ length += 4 // num_channels
+ length++ // full_srvc_audio
+ return length / 8
+}
diff --git a/internal/scte35/avail_descriptor.go b/internal/scte35/avail_descriptor.go
@@ -0,0 +1,96 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "encoding/xml"
+ "fmt"
+
+ "github.com/bamiaux/iobit"
+)
+
+const (
+ // AvailDescriptorTag is the splice_descriptor_tag for an avail_descriptor
+ AvailDescriptorTag = 0x00
+)
+
+// AvailDescriptor is an implementation of a splice_descriptor. It provides an
+// optional extension to the splice_insert() command that allows an
+// authorization identifier to be sent for an avail. Multiple copies of this
+// descriptor may be included by using the loop mechanism provided. This
+// identifier is intended to replicate the functionality of the cue tone system
+// used in analog systems for ad insertion. This descriptor is intended only
+// for use with a splice_insert() command, within a splice_info_section.
+type AvailDescriptor struct {
+ XMLName xml.Name `xml:"http://www.scte.org/schemas/35 AvailDescriptor" json:"-"`
+ JSONType uint32 `xml:"-" json:"type"`
+ ProviderAvailID uint32 `xml:"providerAvailId,attr" json:"providerAvailId"`
+}
+
+// Tag returns the splice_descriptor_tag.
+func (sd *AvailDescriptor) Tag() uint32 {
+ // ensure JSONType is set
+ sd.JSONType = AvailDescriptorTag
+ return AvailDescriptorTag
+}
+
+// writeTo the given table.
+func (sd *AvailDescriptor) writeTo(t *table) {
+ t.row(0, "avail_descriptor() {", nil)
+ t.row(1, "splice_descriptor_tag", fmt.Sprintf("%#02x", AvailDescriptorTag))
+ t.row(1, "descriptor_length", sd.length())
+ t.row(1, "identifier", fmt.Sprintf("%#08x, (%s)", CUEIdentifier, CUEIASCII))
+ t.row(1, "provider_avail_id", sd.ProviderAvailID)
+ t.row(0, "}", nil)
+}
+
+// decode updates this splice_descriptor from binary.
+func (sd *AvailDescriptor) decode(b []byte) error {
+ r := iobit.NewReader(b)
+ r.Skip(8) // splice_descriptor_tag
+ r.Skip(8) // descriptor_length
+ r.Skip(32) // identifier
+ sd.ProviderAvailID = r.Uint32(32)
+
+ if err := readerError(r); err != nil {
+ return fmt.Errorf("avail_descriptor: %w", err)
+ }
+ return nil
+}
+
+// encode this splice_descriptor to binary.
+func (sd *AvailDescriptor) encode() ([]byte, error) {
+ length := sd.length()
+ // add 2 bytes to contain splice_descriptor_tag & descriptor_length
+ buf := make([]byte, length+2)
+ iow := iobit.NewWriter(buf)
+
+ iow.PutUint32(8, AvailDescriptorTag) // splice_descriptor_tag
+ iow.PutUint32(8, uint32(length)) // descriptor_length
+ iow.PutUint32(32, CUEIdentifier) // identifier
+ iow.PutUint32(32, sd.ProviderAvailID) // provider_avail_id
+
+ err := iow.Flush()
+ return buf, err
+}
+
+// descriptorLength returns the descriptor_length
+func (sd *AvailDescriptor) length() int {
+ length := 32 // identifier
+ length += 32 // provider_avail_id
+ return length / 8
+}
diff --git a/internal/scte35/bandwidth_reservation.go b/internal/scte35/bandwidth_reservation.go
@@ -0,0 +1,73 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "encoding/xml"
+ "fmt"
+)
+
+const (
+ // BandwidthReservationType is the splice_command_type for
+ // bandwidth_reservation()
+ BandwidthReservationType = 0x07
+)
+
+// BandwidthReservation command is provided for reserving bandwidth in a
+// multiplex. A typical usage would be in a satellite delivery system that
+// requires packets of a certain PID to always be present at the intended
+// repetition rate to guarantee a certain bandwidth for that PID. This message
+// differs from a splice_null() command so that it can easily be handled in a
+// unique way by receiving equipment (i.e. removed from the multiplex by a
+// satellite receiver). If a descriptor is sent with this command, it can not be
+// expected that it will be carried through the entire transmission chain and it
+// should be a private descriptor that is utilized only by the bandwidth
+// reservation process.
+type BandwidthReservation struct {
+ XMLName xml.Name `xml:"http://www.scte.org/schemas/35 BandwidthReservation" json:"-"`
+ JSONType uint32 `xml:"-" json:"type"`
+}
+
+// Type returns the splice_command_type.
+func (cmd *BandwidthReservation) Type() uint32 {
+ // ensure JSONType is set
+ cmd.JSONType = BandwidthReservationType
+ return BandwidthReservationType
+}
+
+// decode a binary bandwidth_reservation.
+func (cmd *BandwidthReservation) decode(b []byte) error {
+ if len(b) > 0 {
+ return fmt.Errorf("bandwidth_reservation: %w", ErrBufferOverflow)
+ }
+ return nil
+}
+
+// encode this bandwidth_reservation to binary.
+func (cmd *BandwidthReservation) encode() ([]byte, error) {
+ return nil, nil
+}
+
+// commandLength returns the splice_command_length
+func (cmd *BandwidthReservation) length() int {
+ return 0
+}
+
+// writeTo the given table.
+func (cmd *BandwidthReservation) writeTo(t *table) {
+ t.row(0, "bandwidth_reservation() {}", nil)
+}
diff --git a/internal/scte35/crc_32.go b/internal/scte35/crc_32.go
@@ -0,0 +1,321 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "errors"
+
+ "github.com/bamiaux/iobit"
+)
+
+var (
+ // ErrCRC32Invalid indicates that a splice_info_sections CRC_32 is
+ // invalid with respect to the binary payload.
+ ErrCRC32Invalid = errors.New("CRC_32 not valid")
+
+ // crc32Table provides values for calculating an SCTE35 object's CRC32 value.
+ crc32Table = [...]uint{
+ 0x00000000,
+ 0x04C11DB7,
+ 0x09823B6E,
+ 0x0D4326D9,
+ 0x130476DC,
+ 0x17C56B6B,
+ 0x1A864DB2,
+ 0x1E475005,
+ 0x2608EDB8,
+ 0x22C9F00F,
+ 0x2F8AD6D6,
+ 0x2B4BCB61,
+ 0x350C9B64,
+ 0x31CD86D3,
+ 0x3C8EA00A,
+ 0x384FBDBD,
+ 0x4C11DB70,
+ 0x48D0C6C7,
+ 0x4593E01E,
+ 0x4152FDA9,
+ 0x5F15ADAC,
+ 0x5BD4B01B,
+ 0x569796C2,
+ 0x52568B75,
+ 0x6A1936C8,
+ 0x6ED82B7F,
+ 0x639B0DA6,
+ 0x675A1011,
+ 0x791D4014,
+ 0x7DDC5DA3,
+ 0x709F7B7A,
+ 0x745E66CD,
+ 0x9823B6E0,
+ 0x9CE2AB57,
+ 0x91A18D8E,
+ 0x95609039,
+ 0x8B27C03C,
+ 0x8FE6DD8B,
+ 0x82A5FB52,
+ 0x8664E6E5,
+ 0xBE2B5B58,
+ 0xBAEA46EF,
+ 0xB7A96036,
+ 0xB3687D81,
+ 0xAD2F2D84,
+ 0xA9EE3033,
+ 0xA4AD16EA,
+ 0xA06C0B5D,
+ 0xD4326D90,
+ 0xD0F37027,
+ 0xDDB056FE,
+ 0xD9714B49,
+ 0xC7361B4C,
+ 0xC3F706FB,
+ 0xCEB42022,
+ 0xCA753D95,
+ 0xF23A8028,
+ 0xF6FB9D9F,
+ 0xFBB8BB46,
+ 0xFF79A6F1,
+ 0xE13EF6F4,
+ 0xE5FFEB43,
+ 0xE8BCCD9A,
+ 0xEC7DD02D,
+ 0x34867077,
+ 0x30476DC0,
+ 0x3D044B19,
+ 0x39C556AE,
+ 0x278206AB,
+ 0x23431B1C,
+ 0x2E003DC5,
+ 0x2AC12072,
+ 0x128E9DCF,
+ 0x164F8078,
+ 0x1B0CA6A1,
+ 0x1FCDBB16,
+ 0x018AEB13,
+ 0x054BF6A4,
+ 0x0808D07D,
+ 0x0CC9CDCA,
+ 0x7897AB07,
+ 0x7C56B6B0,
+ 0x71159069,
+ 0x75D48DDE,
+ 0x6B93DDDB,
+ 0x6F52C06C,
+ 0x6211E6B5,
+ 0x66D0FB02,
+ 0x5E9F46BF,
+ 0x5A5E5B08,
+ 0x571D7DD1,
+ 0x53DC6066,
+ 0x4D9B3063,
+ 0x495A2DD4,
+ 0x44190B0D,
+ 0x40D816BA,
+ 0xACA5C697,
+ 0xA864DB20,
+ 0xA527FDF9,
+ 0xA1E6E04E,
+ 0xBFA1B04B,
+ 0xBB60ADFC,
+ 0xB6238B25,
+ 0xB2E29692,
+ 0x8AAD2B2F,
+ 0x8E6C3698,
+ 0x832F1041,
+ 0x87EE0DF6,
+ 0x99A95DF3,
+ 0x9D684044,
+ 0x902B669D,
+ 0x94EA7B2A,
+ 0xE0B41DE7,
+ 0xE4750050,
+ 0xE9362689,
+ 0xEDF73B3E,
+ 0xF3B06B3B,
+ 0xF771768C,
+ 0xFA325055,
+ 0xFEF34DE2,
+ 0xC6BCF05F,
+ 0xC27DEDE8,
+ 0xCF3ECB31,
+ 0xCBFFD686,
+ 0xD5B88683,
+ 0xD1799B34,
+ 0xDC3ABDED,
+ 0xD8FBA05A,
+ 0x690CE0EE,
+ 0x6DCDFD59,
+ 0x608EDB80,
+ 0x644FC637,
+ 0x7A089632,
+ 0x7EC98B85,
+ 0x738AAD5C,
+ 0x774BB0EB,
+ 0x4F040D56,
+ 0x4BC510E1,
+ 0x46863638,
+ 0x42472B8F,
+ 0x5C007B8A,
+ 0x58C1663D,
+ 0x558240E4,
+ 0x51435D53,
+ 0x251D3B9E,
+ 0x21DC2629,
+ 0x2C9F00F0,
+ 0x285E1D47,
+ 0x36194D42,
+ 0x32D850F5,
+ 0x3F9B762C,
+ 0x3B5A6B9B,
+ 0x0315D626,
+ 0x07D4CB91,
+ 0x0A97ED48,
+ 0x0E56F0FF,
+ 0x1011A0FA,
+ 0x14D0BD4D,
+ 0x19939B94,
+ 0x1D528623,
+ 0xF12F560E,
+ 0xF5EE4BB9,
+ 0xF8AD6D60,
+ 0xFC6C70D7,
+ 0xE22B20D2,
+ 0xE6EA3D65,
+ 0xEBA91BBC,
+ 0xEF68060B,
+ 0xD727BBB6,
+ 0xD3E6A601,
+ 0xDEA580D8,
+ 0xDA649D6F,
+ 0xC423CD6A,
+ 0xC0E2D0DD,
+ 0xCDA1F604,
+ 0xC960EBB3,
+ 0xBD3E8D7E,
+ 0xB9FF90C9,
+ 0xB4BCB610,
+ 0xB07DABA7,
+ 0xAE3AFBA2,
+ 0xAAFBE615,
+ 0xA7B8C0CC,
+ 0xA379DD7B,
+ 0x9B3660C6,
+ 0x9FF77D71,
+ 0x92B45BA8,
+ 0x9675461F,
+ 0x8832161A,
+ 0x8CF30BAD,
+ 0x81B02D74,
+ 0x857130C3,
+ 0x5D8A9099,
+ 0x594B8D2E,
+ 0x5408ABF7,
+ 0x50C9B640,
+ 0x4E8EE645,
+ 0x4A4FFBF2,
+ 0x470CDD2B,
+ 0x43CDC09C,
+ 0x7B827D21,
+ 0x7F436096,
+ 0x7200464F,
+ 0x76C15BF8,
+ 0x68860BFD,
+ 0x6C47164A,
+ 0x61043093,
+ 0x65C52D24,
+ 0x119B4BE9,
+ 0x155A565E,
+ 0x18197087,
+ 0x1CD86D30,
+ 0x029F3D35,
+ 0x065E2082,
+ 0x0B1D065B,
+ 0x0FDC1BEC,
+ 0x3793A651,
+ 0x3352BBE6,
+ 0x3E119D3F,
+ 0x3AD08088,
+ 0x2497D08D,
+ 0x2056CD3A,
+ 0x2D15EBE3,
+ 0x29D4F654,
+ 0xC5A92679,
+ 0xC1683BCE,
+ 0xCC2B1D17,
+ 0xC8EA00A0,
+ 0xD6AD50A5,
+ 0xD26C4D12,
+ 0xDF2F6BCB,
+ 0xDBEE767C,
+ 0xE3A1CBC1,
+ 0xE760D676,
+ 0xEA23F0AF,
+ 0xEEE2ED18,
+ 0xF0A5BD1D,
+ 0xF464A0AA,
+ 0xF9278673,
+ 0xFDE69BC4,
+ 0x89B8FD09,
+ 0x8D79E0BE,
+ 0x803AC667,
+ 0x84FBDBD0,
+ 0x9ABC8BD5,
+ 0x9E7D9662,
+ 0x933EB0BB,
+ 0x97FFAD0C,
+ 0xAFB010B1,
+ 0xAB710D06,
+ 0xA6322BDF,
+ 0xA2F33668,
+ 0xBCB4666D,
+ 0xB8757BDA,
+ 0xB5365D03,
+ 0xB1F740B4,
+ }
+)
+
+// calculateCRC32 takes an byte array Writer containing all of the bytes of a
+// Splice Info Section object up to the CRC32 value, calculates a CRC32 value
+// matching the bits for those existing bytes, and returns it.
+func calculateCRC32(b []byte) uint32 {
+ crc := int32(-1)
+ for i := range b {
+ crc = (crc << 8) ^ int32(crc32Table[((crc>>24)^int32(b[i]))&0xFF])
+ }
+ return uint32(crc)
+}
+
+// verifyCRC32 verifies the base-64 encoded binary string is valid by confirming
+// the CRC_32 matches
+func verifyCRC32(b []byte) error {
+ if len(b) < 4 {
+ return ErrCRC32Invalid
+ }
+
+ r := iobit.NewReader(b)
+
+ payload := r.Bytes(len(b) - 4)
+ crc := r.Uint32(32)
+
+ calculated := calculateCRC32(payload)
+ if calculated != crc {
+ Logger.Printf("CRC_32 calculated (%d) != reported (%d)\n", calculated, crc)
+ return ErrCRC32Invalid
+ }
+
+ return nil
+}
diff --git a/internal/scte35/delivery_restrictions.go b/internal/scte35/delivery_restrictions.go
@@ -0,0 +1,54 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+const (
+ // DeviceRestrictionsGroup0 is the device_restrictions for Restrict Group 0
+ DeviceRestrictionsGroup0 = 0x00
+ // DeviceRestrictionsGroup1 is the device_restrictions for Restrict Group 1
+ DeviceRestrictionsGroup1 = 0x01
+ // DeviceRestrictionsGroup2 is the device_restrictions for Restrict Group 2
+ DeviceRestrictionsGroup2 = 0x02
+ // DeviceRestrictionsNone is the device_restrictions for None
+ DeviceRestrictionsNone = 0x03
+)
+
+// DeliveryRestrictions contains the specific delivery restriction flags as
+// defined within the SegmentationDescriptorType XML schema definition.
+type DeliveryRestrictions struct {
+ ArchiveAllowedFlag bool `xml:"archiveAllowedFlag,attr" json:"archiveAllowedFlag"`
+ WebDeliveryAllowedFlag bool `xml:"webDeliveryAllowedFlag,attr" json:"webDeliveryAllowedFlag"`
+ NoRegionalBlackoutFlag bool `xml:"noRegionalBlackoutFlag,attr" json:"noRegionalBlackoutFlag"`
+ DeviceRestrictions uint32 `xml:"deviceRestrictions,attr" json:"deviceRestrictions"`
+}
+
+// deviceRestrictionsName returns the human-readable name for the
+// device_restrictions.
+func (dr *DeliveryRestrictions) deviceRestrictionsName() string {
+ switch dr.DeviceRestrictions {
+ case DeviceRestrictionsGroup0:
+ return "Restrict Group 0"
+ case DeviceRestrictionsGroup1:
+ return "Restrict Group 1"
+ case DeviceRestrictionsGroup2:
+ return "Restrict Group 2"
+ case DeviceRestrictionsNone:
+ return "None"
+ default:
+ return "Unknown"
+ }
+}
diff --git a/internal/scte35/dtmf_descriptor.go b/internal/scte35/dtmf_descriptor.go
@@ -0,0 +1,107 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "encoding/xml"
+ "fmt"
+
+ "github.com/bamiaux/iobit"
+)
+
+const (
+ // DTMFDescriptorTag is the splice_descriptor_tag for a dtmf_descriptor
+ DTMFDescriptorTag = 0x01
+)
+
+// DTMFDescriptor is an implementation of a splice_descriptor. It provides an
+// optional extension to the splice_insert() command that allows a receiver
+// device to generate a legacy analog DTMF sequence based on a
+// splice_info_section being received.
+type DTMFDescriptor struct {
+ XMLName xml.Name `xml:"http://www.scte.org/schemas/35 DTMFDescriptor" json:"-"`
+ JSONType uint32 `xml:"-" json:"type"`
+ Preroll uint32 `xml:"preroll,attr" json:"preroll"`
+ DTMFChars string `xml:"chars,attr" json:"chars"`
+}
+
+// writeTo the given table.
+func (sd *DTMFDescriptor) writeTo(t *table) {
+ t.row(0, "dtmf_descriptor() {", nil)
+ t.row(1, "splice_descriptor_tag", fmt.Sprintf("%#02x", DTMFDescriptorTag))
+ t.row(1, "descriptor_length", sd.length())
+ t.row(1, "identifier", fmt.Sprintf("%#08x (%s)", CUEIdentifier, CUEIASCII))
+ t.row(1, "preroll", float32(sd.Preroll/10))
+ t.row(1, "dtmf_count", len(sd.DTMFChars))
+ t.row(1, "dtmf_chars", sd.DTMFChars)
+ t.row(0, "}", nil)
+}
+
+// Tag returns the splice_descriptor_tag.
+func (sd *DTMFDescriptor) Tag() uint32 {
+ // ensure JSONType is set
+ sd.JSONType = DTMFDescriptorTag
+ return DTMFDescriptorTag
+}
+
+// decode updates this splice_descriptor from binary.
+func (sd *DTMFDescriptor) decode(b []byte) error {
+ r := iobit.NewReader(b)
+ r.Skip(8) // splice_descriptor_tag
+ r.Skip(8) // descriptor_length
+ r.Skip(32) // identifier
+ sd.Preroll = r.Uint32(8)
+ dtmfCount := int(r.Uint32(3))
+ r.Skip(5) // reserved
+ sd.DTMFChars = r.String(dtmfCount)
+
+ if err := readerError(r); err != nil {
+ return fmt.Errorf("dtmf_descriptor: %w", err)
+ }
+ return readerError(r)
+}
+
+// encode this splice_descriptor to binary.
+func (sd *DTMFDescriptor) encode() ([]byte, error) {
+ length := sd.length()
+
+ // add 2 bytes to contain splice_descriptor_tag & descriptor_length
+ buf := make([]byte, length+2)
+ iow := iobit.NewWriter(buf)
+ iow.PutUint32(8, DTMFDescriptorTag) // splice_descriptor_tag
+ iow.PutUint32(8, uint32(length)) // descriptor_length
+ iow.PutUint32(32, CUEIdentifier) // identifier
+ iow.PutUint32(8, sd.Preroll) // preroll
+ iow.PutUint32(3, uint32(len(sd.DTMFChars))) // dtmf_count
+ iow.PutUint32(5, Reserved) // reserved
+ _, err := iow.Write([]byte(sd.DTMFChars)) // dtmf_chars
+ if err != nil {
+ return buf, err
+ }
+ err = iow.Flush()
+ return buf, err
+}
+
+// descriptorLength returns the descriptor_length.
+func (sd *DTMFDescriptor) length() int {
+ length := 32 // identifier
+ length += 8 // preroll
+ length += 3 // dtmf_count
+ length += 5 // reserved
+ length += len(sd.DTMFChars) * 8 // dtmf_char
+ return length / 8
+}
diff --git a/internal/scte35/encrypted_packet.go b/internal/scte35/encrypted_packet.go
@@ -0,0 +1,56 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+const (
+ // EncryptionAlgorithmNone is the encryption_algorithm for None.
+ EncryptionAlgorithmNone = 0
+ // EncryptionAlgorithmDESECB is the encryption_algorithm for DES - ECB Mode.
+ EncryptionAlgorithmDESECB = 1
+ // EncryptionAlgorithmDESCBC is the encryption_algorithm for DES - CBC Mode.
+ EncryptionAlgorithmDESCBC = 2
+ // EncryptionAlgorithmTripleDES is the encryption_algorithm for Triple DES
+ // EDE3 - ECB Mode.
+ EncryptionAlgorithmTripleDES = 3
+)
+
+// EncryptedPacket contains the encryption details if this payload has been
+// encrypted.
+type EncryptedPacket struct {
+ EncryptionAlgorithm uint32 `xml:"encryptionAlgorithm,attr,omitempty" json:"encryptionAlgorithm,omitempty"`
+ CWIndex uint32 `xml:"cwIndex,attr,omitempty" json:"cwIndex,omitempty"`
+}
+
+// encryptionAlgorithmName returns the user-friendly encryption algorithm name
+func (p *EncryptedPacket) encryptionAlgorithmName() string {
+ if p == nil {
+ return "No encryption"
+ }
+
+ switch p.EncryptionAlgorithm {
+ case EncryptionAlgorithmNone:
+ return "No encryption"
+ case EncryptionAlgorithmDESECB:
+ return "DES – ECB mode"
+ case EncryptionAlgorithmDESCBC:
+ return "DES – CBC mode"
+ case EncryptionAlgorithmTripleDES:
+ return "Triple DES EDE3 – ECB mode"
+ default:
+ return "User private"
+ }
+}
diff --git a/internal/scte35/private_command.go b/internal/scte35/private_command.go
@@ -0,0 +1,101 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "encoding/binary"
+ "encoding/xml"
+ "fmt"
+
+ "github.com/bamiaux/iobit"
+)
+
+const (
+ // PrivateCommandType is the splice_command_type for private_command()
+ PrivateCommandType = 0xFF
+)
+
+// PrivateCommand provides a means to distribute user-defined commands using the
+// SCTE 35 protocol. The first bit field in each user-defined command is a
+// 32-bit identifier, unique for each participating vendor. Receiving equipment
+// should skip any splice_info_section() messages containing private_command()
+// structures with unknown identifiers.
+type PrivateCommand struct {
+ XMLName xml.Name `xml:"http://www.scte.org/schemas/35 PrivateCommand" json:"-"`
+ JSONType uint32 `xml:"-" json:"type"`
+ Identifier uint32 `xml:"identifier,attr" json:"identifier"`
+ PrivateBytes Bytes `xml:",chardata" json:"privateBytes"`
+}
+
+// IdentifierString returns the identifier as a string.
+func (cmd *PrivateCommand) IdentifierString() string {
+ b := make([]byte, 4)
+ binary.BigEndian.PutUint32(b, cmd.Identifier)
+ return string(b)
+}
+
+// Type returns the splice_command_type.
+func (cmd *PrivateCommand) Type() uint32 {
+ // ensure JSONType is set
+ cmd.JSONType = PrivateCommandType
+ return PrivateCommandType
+}
+
+// decode a binary private_command.
+func (cmd *PrivateCommand) decode(b []byte) error {
+ r := iobit.NewReader(b)
+
+ cmd.Identifier = r.Uint32(32)
+ cmd.PrivateBytes = r.LeftBytes()
+ // LeftBytes doesnt advance position
+ r.Skip(uint(len(cmd.PrivateBytes) * 8))
+
+ if err := readerError(r); err != nil {
+ return fmt.Errorf("private_command: %w", err)
+ }
+ return readerError(r)
+}
+
+// encode this private_command to binary.
+func (cmd *PrivateCommand) encode() ([]byte, error) {
+ buf := make([]byte, cmd.length())
+
+ iow := iobit.NewWriter(buf)
+ iow.PutUint32(32, cmd.Identifier)
+ _, err := iow.Write(cmd.PrivateBytes)
+ if err != nil {
+ return buf, err
+ }
+
+ err = iow.Flush()
+ return buf, err
+}
+
+// commandLength returns the splice_command_length.
+func (cmd *PrivateCommand) length() int {
+ length := 32 // identifier
+ length += len(cmd.PrivateBytes) * 8 // private_bytes
+ return length / 8
+}
+
+// writeTo the given table.
+func (cmd *PrivateCommand) writeTo(t *table) {
+ t.row(0, "private_command() {", nil)
+ t.row(1, "identifier", fmt.Sprintf("%#08x, (%s)", cmd.Identifier, cmd.IdentifierString()))
+ t.row(1, "private_byte", fmt.Sprintf("%#0x", cmd.PrivateBytes))
+ t.row(0, "}", nil)
+}
diff --git a/internal/scte35/private_descriptor.go b/internal/scte35/private_descriptor.go
@@ -0,0 +1,101 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "encoding/binary"
+ "encoding/xml"
+ "fmt"
+
+ "github.com/bamiaux/iobit"
+)
+
+// PrivateDescriptor encapsulates the contents of non-CUEI descriptors
+type PrivateDescriptor struct {
+ XMLName xml.Name `xml:"http://www.scte.org/schemas/35 PrivateDescriptor" json:"-"`
+ JSONType uint32 `xml:"-" json:"type"`
+ PrivateTag uint32 `xml:"tag,attr" json:"tag"`
+ Identifier uint32 `xml:"identifier,attr" json:"identifier"`
+ PrivateBytes Bytes `xml:",chardata" json:"privateBytes"`
+}
+
+// IdentifierString returns the identifier as a string
+func (sd *PrivateDescriptor) IdentifierString() string {
+ b := make([]byte, 4)
+ binary.BigEndian.PutUint32(b, sd.Identifier)
+ return string(b)
+}
+
+// Tag returns the splice_descriptor_tag.
+func (sd *PrivateDescriptor) Tag() uint32 {
+ // ensure JSONType is set
+ sd.JSONType = sd.PrivateTag
+ return sd.PrivateTag
+}
+
+// decode updates this splice_descriptor from binary.
+func (sd *PrivateDescriptor) decode(b []byte) error {
+ r := iobit.NewReader(b)
+
+ sd.PrivateTag = r.Uint32(8)
+ r.Skip(8) // descriptor_length
+ sd.Identifier = r.Uint32(32)
+ sd.PrivateBytes = r.LeftBytes()
+ // LeftBytes doesnt advance position
+ r.Skip(uint(len(sd.PrivateBytes) * 8))
+
+ if err := readerError(r); err != nil {
+ return fmt.Errorf("private_descriptor: %w", err)
+ }
+ return nil
+}
+
+// encode this splice_descriptor to binary.
+func (sd *PrivateDescriptor) encode() ([]byte, error) {
+ length := sd.length()
+
+ // add 2 bytes to contain splice_descriptor_tag & descriptor_length
+ buf := make([]byte, length+2)
+ iow := iobit.NewWriter(buf)
+ iow.PutUint32(8, sd.PrivateTag)
+ iow.PutUint32(8, uint32(length))
+ iow.PutUint32(32, sd.Identifier)
+ _, err := iow.Write(sd.PrivateBytes)
+ if err != nil {
+ return buf, err
+ }
+ err = iow.Flush()
+
+ return buf, err
+}
+
+// descriptorLength returns the descriptor_length
+func (sd *PrivateDescriptor) length() int {
+ length := 32 // identifier
+ length += len(sd.PrivateBytes) * 8 // private_bytes
+ return length / 8
+}
+
+// table returns the tabular description of this PrivateDescriptor.
+func (sd *PrivateDescriptor) writeTo(t *table) {
+ t.row(0, "private_descriptor() {", nil)
+ t.row(1, "splice_descriptor_tag", fmt.Sprintf("%#02x", sd.Tag()))
+ t.row(1, "descriptor_length", sd.length())
+ t.row(1, "identifier", fmt.Sprintf("%#08x, (%s)", sd.Identifier, sd.IdentifierString()))
+ t.row(1, "private_bytes", fmt.Sprintf("%#0x", sd.PrivateBytes))
+ t.row(0, "}", nil)
+}
diff --git a/internal/scte35/scte35.go b/internal/scte35/scte35.go
@@ -0,0 +1,152 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+// Package scte35 contains the structs for encoding and/or decoding SCTE-35
+// signal.
+package scte35
+
+import (
+ "encoding/base64"
+ "encoding/hex"
+ "errors"
+ "io"
+ "log"
+ "math"
+ "strings"
+ "time"
+
+ "github.com/bamiaux/iobit"
+)
+
+const (
+ // Reserved bits shall be set to 1
+ Reserved = 0xFF
+ // TicksPerSecond is the number of 90KHz ticks per second
+ TicksPerSecond = 90000
+ // unixEpochToGPSEpoch is the number of seconds between 1970-01-01T00:00:00Z
+ // (Unix Epoch) and 1980-01-06T00:00:00Z (GPS Epoch).
+ unixEpochToGPSEpoch = uint32(315964800)
+)
+
+var (
+ // ErrBufferUnderflow is returned when decoding fails to fully consume the
+ // provided byte array.
+ ErrBufferUnderflow = errors.New("buffer underflow")
+ // ErrBufferOverflow is returned when decoding requires more bytes than are
+ // available.
+ ErrBufferOverflow = errors.New("buffer overflow")
+ // ErrUnsupportedEncoding is returned if the signal is not a base-64 encoded
+ // string.
+ ErrUnsupportedEncoding = errors.New("invalid or unsupported encoding")
+)
+
+// Logger for emitting debug messages.
+var Logger = log.New(io.Discard, "SCTE35 ", log.Ldate|log.Ltime|log.Llongfile)
+
+// DecodeBase64 is a convenience function for decoding a base-64 string into
+// a SpliceInfoSection. If an error occurs, the returned SpliceInfoSection
+// will contain the results of decoding up until the error condition
+// was encountered.
+func DecodeBase64(s string) (*SpliceInfoSection, error) {
+ sis := &SpliceInfoSection{}
+ b, err := base64.StdEncoding.DecodeString(s)
+ if err != nil {
+ return sis, ErrUnsupportedEncoding
+ }
+ err = sis.Decode(b)
+ return sis, err
+}
+
+// DecodeHex is a convenience function for decoding a hexadecimal string into
+// a SpliceInfoSection. If an error occurs, the returned SpliceInfoSection
+// will contains the results of decoding up until the error condition
+// was encountered.
+func DecodeHex(s string) (*SpliceInfoSection, error) {
+ sis := &SpliceInfoSection{}
+ b, err := hex.DecodeString(strings.TrimPrefix(s, "0x"))
+ if err != nil {
+ return sis, ErrUnsupportedEncoding
+ }
+ err = sis.Decode(b)
+ return sis, err
+}
+
+// DurationToTicks converts a duration to 90MhZ ticks.
+func DurationToTicks(d time.Duration) uint64 {
+ return uint64(math.Ceil(float64(d) * TicksPerSecond / float64(time.Second)))
+}
+
+// TicksToDuration converts 90MhZ ticks to a duration.
+func TicksToDuration(ticks uint64) time.Duration {
+ s := float64(ticks) / float64(TicksPerSecond)
+ return time.Duration(int64(s * float64(time.Second)))
+}
+
+// BreakDuration specifies the duration of the commercial break(s). It may be
+// used to give the splicer an indication of when the break will be over and
+// when the network In Point will occur.
+type BreakDuration struct {
+ AutoReturn bool `xml:"autoReturn,attr" json:"autoReturn"`
+ Duration uint64 `xml:"duration,attr" json:"duration"`
+}
+
+// Bytes is a byte array.
+type Bytes []byte
+
+// MarshalText encodes Bytes to a hexadecimal string.
+func (bb Bytes) MarshalText() ([]byte, error) {
+ tmp := hex.EncodeToString(bb)
+ return []byte(tmp), nil
+}
+
+// UnmarshalText decodes a hexadecimal string.
+func (bb *Bytes) UnmarshalText(b []byte) error {
+ tmp, err := hex.DecodeString(string(b))
+ if err != nil {
+ return err
+ }
+ *bb = tmp
+ return nil
+}
+
+// NewUTCSpliceTime creates a UTCSpliceTime representing the number of seconds
+// from GPS Epoch (01 Jan 1980, 00:00:00 UTC)
+func NewUTCSpliceTime(sec uint32) UTCSpliceTime {
+ return UTCSpliceTime{
+ time.Unix(int64(sec+unixEpochToGPSEpoch), 0),
+ }
+}
+
+// UTCSpliceTime is used to carry utc_splice_time values.
+type UTCSpliceTime struct {
+ time.Time
+}
+
+// GPSSeconds returns the seconds since GPS Epoch
+func (t UTCSpliceTime) GPSSeconds() uint32 {
+ return uint32(t.Time.Unix()) - unixEpochToGPSEpoch
+}
+
+// readerError returns the readers error state, if any.
+func readerError(r iobit.Reader) error {
+ if r.LeftBits() > 0 {
+ return ErrBufferUnderflow
+ }
+ if errors.Is(r.Error(), iobit.ErrOverflow) {
+ return ErrBufferOverflow
+ }
+ return nil
+}
diff --git a/internal/scte35/scte35_test.go b/internal/scte35/scte35_test.go
@@ -0,0 +1,799 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "encoding/binary"
+ "encoding/json"
+ "encoding/xml"
+ "io"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestDecodeBase64(t *testing.T) {
+ Logger.SetOutput(os.Stderr)
+ defer Logger.SetOutput(io.Discard)
+
+ // when adding tests that contain multiple splice descriptors, care must be
+ // taken to ensure they are in the order specified in the custom UnmarshalXML
+ // implementation, otherwise misleading error may occur
+ cases := map[string]struct {
+ binary string
+ err error
+ expected SpliceInfoSection
+ legacy bool
+ }{
+ "Sample 14.1 time_signal - Placement Opportunity Start": {
+ binary: "/DA0AAAAAAAA///wBQb+cr0AUAAeAhxDVUVJSAAAjn/PAAGlmbAICAAAAAAsoKGKNAIAmsnRfg==",
+ expected: SpliceInfoSection{
+ EncryptedPacket: EncryptedPacket{EncryptionAlgorithm: EncryptionAlgorithmNone, CWIndex: 255},
+ SpliceCommand: &TimeSignal{
+ SpliceTime: SpliceTime{
+ PTSTime: uint64ptr(0x072bd0050),
+ },
+ },
+ SpliceDescriptors: []SpliceDescriptor{
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ NoRegionalBlackoutFlag: true,
+ ArchiveAllowedFlag: true,
+ DeviceRestrictions: DeviceRestrictionsNone,
+ },
+ SegmentationEventID: uint32(0x4800008e),
+ SegmentationTypeID: SegmentationTypeProviderPOStart,
+ SegmentationDuration: uint64ptr(0x0001a599b0),
+ SegmentationUPIDs: []SegmentationUPID{
+ NewSegmentationUPID(SegmentationUPIDTypeTI, toBytes(0x000000002ca0a18a)),
+ },
+ SegmentNum: 2,
+ },
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ "Sample 14.2 splice_insert": {
+ binary: "/DAvAAAAAAAA///wFAVIAACPf+/+c2nALv4AUsz1AAAAAAAKAAhDVUVJAAABNWLbowo=",
+ expected: SpliceInfoSection{
+ EncryptedPacket: EncryptedPacket{EncryptionAlgorithm: EncryptionAlgorithmNone, CWIndex: 255},
+ SpliceCommand: &SpliceInsert{
+ BreakDuration: &BreakDuration{
+ AutoReturn: true,
+ Duration: uint64(0x00052ccf5),
+ },
+ SpliceEventID: uint32(0x4800008f),
+ OutOfNetworkIndicator: true,
+ Program: &SpliceInsertProgram{
+ SpliceTime: SpliceTime{
+ PTSTime: uint64ptr(0x07369c02e),
+ },
+ },
+ },
+ SpliceDescriptors: []SpliceDescriptor{
+ &AvailDescriptor{
+ ProviderAvailID: 0x00000135,
+ },
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ "Sample 14.3 time_signal - Placement Opportunity End": {
+ binary: "/DAvAAAAAAAA///wBQb+dGKQoAAZAhdDVUVJSAAAjn+fCAgAAAAALKChijUCAKnMZ1g=",
+ expected: SpliceInfoSection{
+ EncryptedPacket: EncryptedPacket{EncryptionAlgorithm: EncryptionAlgorithmNone, CWIndex: 255},
+ SpliceCommand: &TimeSignal{
+ SpliceTime: SpliceTime{
+ PTSTime: uint64ptr(0x0746290a0),
+ },
+ },
+ SpliceDescriptors: []SpliceDescriptor{
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ NoRegionalBlackoutFlag: true,
+ ArchiveAllowedFlag: true,
+ DeviceRestrictions: DeviceRestrictionsNone,
+ WebDeliveryAllowedFlag: true,
+ },
+ SegmentationEventID: 0x4800008e,
+ SegmentationTypeID: SegmentationTypeProviderPOEnd,
+ SegmentationUPIDs: []SegmentationUPID{
+ NewSegmentationUPID(SegmentationUPIDTypeTI, toBytes(0x000000002ca0a18a)),
+ },
+ SegmentNum: 2,
+ },
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ "Sample 14.4 time_signal - Program Start/End": {
+ binary: "/DBIAAAAAAAA///wBQb+ek2ItgAyAhdDVUVJSAAAGH+fCAgAAAAALMvDRBEAAAIXQ1VFSUgAABl/nwgIAAAAACyk26AQAACZcuND",
+ expected: SpliceInfoSection{
+ EncryptedPacket: EncryptedPacket{EncryptionAlgorithm: EncryptionAlgorithmNone, CWIndex: 255},
+ SpliceCommand: &TimeSignal{
+ SpliceTime: SpliceTime{
+ PTSTime: uint64ptr(0x07a4d88b6),
+ },
+ },
+ SpliceDescriptors: []SpliceDescriptor{
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ NoRegionalBlackoutFlag: true,
+ ArchiveAllowedFlag: true,
+ DeviceRestrictions: DeviceRestrictionsNone,
+ WebDeliveryAllowedFlag: true,
+ },
+ SegmentationEventID: 0x48000018,
+ SegmentationTypeID: SegmentationTypeProgramEnd,
+ SegmentationUPIDs: []SegmentationUPID{
+ NewSegmentationUPID(SegmentationUPIDTypeTI, toBytes(0x000000002ccbc344)),
+ },
+ },
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ NoRegionalBlackoutFlag: true,
+ ArchiveAllowedFlag: true,
+ DeviceRestrictions: DeviceRestrictionsNone,
+ WebDeliveryAllowedFlag: true,
+ },
+ SegmentationEventID: 0x48000019,
+ SegmentationTypeID: SegmentationTypeProgramStart,
+ SegmentationUPIDs: []SegmentationUPID{
+ NewSegmentationUPID(SegmentationUPIDTypeTI, toBytes(0x000000002ca4dba0)),
+ },
+ },
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ "Sample 14.5 time_signal - Program Overlap Start": {
+ binary: "/DAvAAAAAAAA///wBQb+rr//ZAAZAhdDVUVJSAAACH+fCAgAAAAALKVs9RcAAJUdsKg=",
+ expected: SpliceInfoSection{
+ EncryptedPacket: EncryptedPacket{EncryptionAlgorithm: EncryptionAlgorithmNone, CWIndex: 255},
+ SpliceCommand: &TimeSignal{
+ SpliceTime: SpliceTime{
+ PTSTime: uint64ptr(0x0aebfff64),
+ },
+ },
+ SpliceDescriptors: []SpliceDescriptor{
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ NoRegionalBlackoutFlag: true,
+ ArchiveAllowedFlag: true,
+ DeviceRestrictions: DeviceRestrictionsNone,
+ WebDeliveryAllowedFlag: true,
+ },
+ SegmentationEventID: 0x48000008,
+ SegmentationTypeID: SegmentationTypeProgramOverlapStart,
+ SegmentationUPIDs: []SegmentationUPID{
+ NewSegmentationUPID(SegmentationUPIDTypeTI, toBytes(0x000000002ca56cf5)),
+ },
+ },
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ "Sample 14.6 time_signal - Program Blackout Override / Program End": {
+ binary: "/DBIAAAAAAAA///wBQb+ky44CwAyAhdDVUVJSAAACn+fCAgAAAAALKCh4xgAAAIXQ1VFSUgAAAl/nwgIAAAAACygoYoRAAC0IX6w",
+ expected: SpliceInfoSection{
+ EncryptedPacket: EncryptedPacket{EncryptionAlgorithm: EncryptionAlgorithmNone, CWIndex: 255},
+ SpliceCommand: &TimeSignal{
+ SpliceTime: SpliceTime{
+ PTSTime: uint64ptr(0x0932e380b),
+ },
+ },
+ SpliceDescriptors: []SpliceDescriptor{
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ NoRegionalBlackoutFlag: true,
+ ArchiveAllowedFlag: true,
+ DeviceRestrictions: DeviceRestrictionsNone,
+ WebDeliveryAllowedFlag: true,
+ },
+ SegmentationEventID: 0x4800000a,
+ SegmentationTypeID: SegmentationTypeProgramBlackoutOverride,
+ SegmentationUPIDs: []SegmentationUPID{
+ NewSegmentationUPID(SegmentationUPIDTypeTI, toBytes(0x000000002ca0a1e3)),
+ },
+ },
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ NoRegionalBlackoutFlag: true,
+ ArchiveAllowedFlag: true,
+ DeviceRestrictions: DeviceRestrictionsNone,
+ WebDeliveryAllowedFlag: true,
+ },
+ SegmentationEventID: 0x48000009,
+ SegmentationTypeID: SegmentationTypeProgramEnd,
+ SegmentationUPIDs: []SegmentationUPID{
+ NewSegmentationUPID(SegmentationUPIDTypeTI, toBytes(0x000000002ca0a18a)),
+ },
+ },
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ "Sample 14.7 time_signal - Program End": {
+ binary: "/DAvAAAAAAAA///wBQb+rvF8TAAZAhdDVUVJSAAAB3+fCAgAAAAALKVslxEAAMSHai4=",
+ expected: SpliceInfoSection{
+ EncryptedPacket: EncryptedPacket{EncryptionAlgorithm: EncryptionAlgorithmNone, CWIndex: 255},
+ SpliceCommand: &TimeSignal{
+ SpliceTime: SpliceTime{
+ PTSTime: uint64ptr(0x0aef17c4c),
+ },
+ },
+ SpliceDescriptors: []SpliceDescriptor{
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ NoRegionalBlackoutFlag: true,
+ ArchiveAllowedFlag: true,
+ DeviceRestrictions: DeviceRestrictionsNone,
+ WebDeliveryAllowedFlag: true,
+ },
+ SegmentationEventID: 0x48000007,
+ SegmentationTypeID: SegmentationTypeProgramEnd,
+ SegmentationUPIDs: []SegmentationUPID{
+ NewSegmentationUPID(SegmentationUPIDTypeTI, toBytes(0x000000002ca56c97)),
+ },
+ },
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ "Sample 14.8 time_signal - Program Start/End - Placement Opportunity End": {
+ binary: "/DBhAAAAAAAA///wBQb+qM1E7QBLAhdDVUVJSAAArX+fCAgAAAAALLLXnTUCAAIXQ1VFSUgAACZ/nwgIAAAAACyy150RAAACF0NVRUlIAAAnf58ICAAAAAAsstezEAAAihiGnw==",
+ expected: SpliceInfoSection{
+ EncryptedPacket: EncryptedPacket{EncryptionAlgorithm: EncryptionAlgorithmNone, CWIndex: 255},
+ SpliceCommand: &TimeSignal{
+ SpliceTime: SpliceTime{
+ PTSTime: uint64ptr(0x0a8cd44ed),
+ },
+ },
+ SpliceDescriptors: []SpliceDescriptor{
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ NoRegionalBlackoutFlag: true,
+ ArchiveAllowedFlag: true,
+ DeviceRestrictions: DeviceRestrictionsNone,
+ WebDeliveryAllowedFlag: true,
+ },
+ SegmentationEventID: 0x480000ad,
+ SegmentationTypeID: SegmentationTypeProviderPOEnd,
+ SegmentationUPIDs: []SegmentationUPID{
+ NewSegmentationUPID(SegmentationUPIDTypeTI, toBytes(0x000000002cb2d79d)),
+ },
+ SegmentNum: 2,
+ },
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ NoRegionalBlackoutFlag: true,
+ ArchiveAllowedFlag: true,
+ DeviceRestrictions: DeviceRestrictionsNone,
+ WebDeliveryAllowedFlag: true,
+ },
+ SegmentationEventID: 0x48000026,
+ SegmentationTypeID: SegmentationTypeProgramEnd,
+ SegmentationUPIDs: []SegmentationUPID{
+ NewSegmentationUPID(SegmentationUPIDTypeTI, toBytes(0x000000002cb2d79d)),
+ },
+ },
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ NoRegionalBlackoutFlag: true,
+ ArchiveAllowedFlag: true,
+ DeviceRestrictions: DeviceRestrictionsNone,
+ WebDeliveryAllowedFlag: true,
+ },
+ SegmentationEventID: 0x48000027,
+ SegmentationTypeID: SegmentationTypeProgramStart,
+ SegmentationUPIDs: []SegmentationUPID{
+ NewSegmentationUPID(SegmentationUPIDTypeTI, toBytes(0x000000002cb2d7b3)),
+ },
+ },
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ "SpliceInsert With DTMF": {
+ binary: "/DAxAAAAAAAAAP/wFAVAAIeuf+/+0AWRK/4AUmXAAC0AfwAMAQpDVUVJUJ81MTkqo5/+gA==",
+ expected: SpliceInfoSection{
+ SpliceCommand: &SpliceInsert{
+ BreakDuration: &BreakDuration{AutoReturn: true, Duration: 5400000},
+ Program: NewSpliceInsertProgram(3490025771),
+ SpliceEventID: 1073776558,
+ SpliceEventCancelIndicator: false,
+ SpliceImmediateFlag: false,
+ OutOfNetworkIndicator: true,
+ UniqueProgramID: 45,
+ AvailNum: 0,
+ AvailsExpected: 127,
+ },
+ SpliceDescriptors: []SpliceDescriptor{
+ &DTMFDescriptor{
+ Preroll: 80,
+ DTMFChars: "519*",
+ },
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ "Time Signal with Segmentation Descriptors": {
+ binary: "/DBIAAAAAAAAAP/wBQb/tB67hgAyAhdDVUVJQAABEn+fCAgAAAAALzE8BTUAAAIXQ1VFSUAAAEV/nwgIAAAAAC8xPN4jAAAfiOPE",
+ expected: SpliceInfoSection{
+ SpliceCommand: NewTimeSignal(7316880262),
+ SpliceDescriptors: []SpliceDescriptor{
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ ArchiveAllowedFlag: true,
+ WebDeliveryAllowedFlag: true,
+ NoRegionalBlackoutFlag: true,
+ DeviceRestrictions: DeviceRestrictionsNone,
+ },
+ SegmentationUPIDs: []SegmentationUPID{
+ {
+ Type: 8,
+ Value: "791755781",
+ },
+ },
+ SegmentationTypeID: 53,
+ SegmentationEventID: 1073742098,
+ },
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ ArchiveAllowedFlag: true,
+ WebDeliveryAllowedFlag: true,
+ NoRegionalBlackoutFlag: true,
+ DeviceRestrictions: DeviceRestrictionsNone,
+ },
+ SegmentationUPIDs: []SegmentationUPID{
+ {
+ Type: 8,
+ Value: "791755998",
+ },
+ },
+ SegmentationTypeID: 35,
+ SegmentationEventID: 1073741893,
+ },
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ "Empty String": {
+ binary: "",
+ err: ErrBufferOverflow,
+ },
+ "Invalid Base64 Encoding": {
+ binary: "/DBaf%^",
+ err: ErrUnsupportedEncoding,
+ },
+ "Splice Insert with Avail Descriptor": {
+ binary: "/DAqAAAAAAAAAP/wDwUAAHn+f8/+QubGOQAAAAAACgAIQ1VFSQAAAADizteX",
+ expected: SpliceInfoSection{
+ SpliceCommand: &SpliceInsert{
+ Program: NewSpliceInsertProgram(1122420281),
+ SpliceEventID: 31230,
+ OutOfNetworkIndicator: true,
+ },
+ SpliceDescriptors: []SpliceDescriptor{
+ &AvailDescriptor{
+ ProviderAvailID: 0,
+ },
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ "Multiple SegmentationUPIDs": {
+ binary: "/DBrAAAAAAAAAP/wBQb/AAAAAABVAlNDVUVJAAAAAn+/DUQKDBR3i+Xj9gAAAAAAAAoMFHeL5eP2AAAAAAAACSZTSUdOQUw6THk5RU1HeEtSMGhGWlV0cE1IZENVVlpuUlVGblp6MTcBA6QTOe8=",
+ expected: SpliceInfoSection{
+ SpliceCommand: NewTimeSignal(4294967296),
+ SpliceDescriptors: []SpliceDescriptor{
+ &SegmentationDescriptor{
+ SegmentationUPIDs: []SegmentationUPID{
+ {
+ Type: SegmentationUPIDTypeEIDR,
+ Value: "10.5239/8BE5-E3F6-0000-0000-0000",
+ },
+ {
+ Type: SegmentationUPIDTypeEIDR,
+ Value: "10.5239/8BE5-E3F6-0000-0000-0000",
+ },
+ {
+ Type: SegmentationUPIDTypeADI,
+ Value: "SIGNAL:Ly9EMGxKR0hFZUtpMHdCUVZnRUFnZz1",
+ },
+ },
+ SegmentationEventID: 2,
+ SegmentationTypeID: SegmentationTypeDistributorPOEnd,
+ SegmentNum: 1,
+ SegmentsExpected: 3,
+ },
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ "Legacy splice_command_length: 0xFFF": {
+ binary: "/DA8AAAAAAAAAP///wb+06ACpQAmAiRDVUVJAACcHX//AACky4AMEERJU0NZTVdGMDQ1MjAwMEgxAQEMm4c0",
+ expected: SpliceInfoSection{
+ SpliceCommand: NewTimeSignal(3550479013),
+ SpliceDescriptors: []SpliceDescriptor{
+ &SegmentationDescriptor{
+ SegmentationUPIDs: []SegmentationUPID{
+ {
+ Type: SegmentationUPIDTypeMPU,
+ FormatIdentifier: uint32ptr(1145656131),
+ Value: "WU1XRjA0NTIwMDBI",
+ },
+ },
+ SegmentationDuration: uint64ptr(10800000),
+ SegmentationEventID: 39965,
+ SegmentationTypeID: SegmentationTypeProviderAdEnd,
+ SegmentNum: 1,
+ SegmentsExpected: 1,
+ },
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ legacy: true,
+ },
+ "SpliceInsert Time Specified With Invalid Component Count": {
+ binary: "FkC1lwP3uTQD0VvxHwVBEH89G6B7VjzaZ9eNuyUF9q8pYAIXsRM9ZpDCczBeDbytQhXkssQstGJVGcvjZ3tiIMULiA4BpRHlzLGFa0q6aVMtzk8ZRUeLcxtKibgVOKBBnkCbOQyhSflFiDkrAAIp+Fk+VRsByTSkPN3RvyK+lWcjHElhwa9hNFcAy4dm3DdeRXnrD3I2mISNc7DkgS0ReotPyp94FV77xMHT4D7SYL48XU20UM4bgg==",
+ err: ErrBufferOverflow,
+ },
+ "Signal with non-CUEI descriptor": {
+ binary: "/DBPAAAAAAAAAP/wBQb/Gq9LggA5AAVTQVBTCwIwQ1VFSf////9//wAAFI4PDxx1cm46bmJjdW5pLmNvbTpicmM6NDk5ODY2NDM0MQoBbM98zw==",
+ expected: SpliceInfoSection{
+ SpliceCommand: NewTimeSignal(4742663042),
+ SpliceDescriptors: []SpliceDescriptor{
+ &PrivateDescriptor{
+ Identifier: 1396789331,
+ PrivateBytes: []byte{11},
+ },
+ &SegmentationDescriptor{
+ SegmentationUPIDs: []SegmentationUPID{
+ {
+ Type: SegmentationUPIDTypeURI,
+ Value: "urn:nbcuni.com:brc:499866434",
+ },
+ },
+ SegmentationDuration: uint64ptr(1347087),
+ SegmentationEventID: 4294967295,
+ SegmentationTypeID: SegmentationTypeProviderAdEnd,
+ SegmentNum: 10,
+ SegmentsExpected: 1,
+ },
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ "Splice Null - Heartbeat": {
+ binary: "/DARAAAAAAAAAP/wAAAAAHpPv/8=",
+ expected: SpliceInfoSection{
+ SpliceCommand: &SpliceNull{},
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ "Invalid CRC_32": {
+ binary: "/DA4AAAAAAAAAP/wFAUABDEAf+//mWEhzP4Azf5gAQAAAAATAhFDVUVJAAAAAX+/AQIwNAEAAKeYO3Q=",
+ err: ErrCRC32Invalid,
+ },
+ "Alignment Stuffing without Encryption": {
+ binary: "/DAeAAAAAAAAAP///wViAA/nf18ACQAAAAAskJv+YPtE",
+ expected: SpliceInfoSection{
+ SpliceCommand: &SpliceInsert{
+ SpliceEventID: 1644171239,
+ Program: &SpliceInsertProgram{},
+ SpliceImmediateFlag: true,
+ UniqueProgramID: 9,
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ legacy: true, // binary wont match because of stuffing
+ },
+ "UPID with Valid ASCII Invalid UTF8": {
+ binary: "/DDHAAAAABc0AP/wBQb/tVo+agCxAhdDVUVJQA4hwH+fCAgAAAAAPj6IcCMAAAIXQ1VFSUAOI1x/nwgIAAAAAD4+iHARAAACF0NVRUlADiHgf58ICAAAAAA+Poi2EAAAAhxDVUVJQA4hyn/fAABSlKwICAAAAAA+Poi2IgAAAkZDVUVJQA4h1n/PAABSlKwNMgoMFHf5uXs0AAAAAAAADhh0eXBlPUxBJmR1cj02MDAwMCZ0aWVy/DDHAAAAAAAAAP8ABQb/HPCt2w==",
+ expected: SpliceInfoSection{
+ SpliceCommand: &TimeSignal{
+ SpliceTime: SpliceTime{
+ PTSTime: uint64ptr(7337557610),
+ },
+ },
+ SpliceDescriptors: SpliceDescriptors{
+ &SegmentationDescriptor{
+ SegmentationEventID: uint32(1074667968),
+ SegmentationTypeID: SegmentationTypeBreakEnd,
+ DeliveryRestrictions: &DeliveryRestrictions{
+ ArchiveAllowedFlag: true,
+ WebDeliveryAllowedFlag: true,
+ NoRegionalBlackoutFlag: true,
+ DeviceRestrictions: 3,
+ },
+ SegmentationUPIDs: []SegmentationUPID{
+ {Type: SegmentationUPIDTypeTI, Value: "1044285552"},
+ },
+ },
+ &SegmentationDescriptor{
+ SegmentationEventID: uint32(1074668380),
+ SegmentationTypeID: SegmentationTypeProgramEnd,
+ DeliveryRestrictions: &DeliveryRestrictions{
+ ArchiveAllowedFlag: true,
+ WebDeliveryAllowedFlag: true,
+ NoRegionalBlackoutFlag: true,
+ DeviceRestrictions: 3,
+ },
+ SegmentationUPIDs: []SegmentationUPID{
+ {Type: SegmentationUPIDTypeTI, Value: "1044285552"},
+ },
+ },
+ &SegmentationDescriptor{
+ SegmentationEventID: uint32(1074668000),
+ SegmentationTypeID: SegmentationTypeProgramStart,
+ DeliveryRestrictions: &DeliveryRestrictions{
+ ArchiveAllowedFlag: true,
+ WebDeliveryAllowedFlag: true,
+ NoRegionalBlackoutFlag: true,
+ DeviceRestrictions: 3,
+ },
+ SegmentationUPIDs: []SegmentationUPID{
+ {Type: SegmentationUPIDTypeTI, Value: "1044285622"},
+ },
+ },
+ &SegmentationDescriptor{
+ SegmentationEventID: uint32(1074667978),
+ SegmentationDuration: uint64ptr(5412012),
+ SegmentationTypeID: SegmentationTypeBreakStart,
+ DeliveryRestrictions: &DeliveryRestrictions{
+ ArchiveAllowedFlag: true,
+ WebDeliveryAllowedFlag: true,
+ NoRegionalBlackoutFlag: true,
+ DeviceRestrictions: 3,
+ },
+ SegmentationUPIDs: []SegmentationUPID{
+ {Type: SegmentationUPIDTypeTI, Value: "1044285622"},
+ },
+ },
+ &SegmentationDescriptor{
+ SegmentationEventID: uint32(1074667990),
+ SegmentationTypeID: 0x05,
+ SegmentationDuration: uint64ptr(5412012),
+ SegmentNum: 6,
+ SegmentsExpected: 255,
+ DeliveryRestrictions: &DeliveryRestrictions{
+ ArchiveAllowedFlag: true,
+ WebDeliveryAllowedFlag: false,
+ NoRegionalBlackoutFlag: true,
+ DeviceRestrictions: 3,
+ },
+ SegmentationUPIDs: []SegmentationUPID{
+ {Type: SegmentationUPIDTypeEIDR, Value: "10.5239/F9B9-7B34-0000-0000-0000"},
+ {Type: SegmentationUPIDTypeADS, Value: "type=LA&dur=60000&tierü0"},
+ {Type: uint32(199)},
+ {Type: uint32(0)},
+ {Type: uint32(0)},
+ {Type: uint32(0)},
+ {Type: uint32(255)},
+ },
+ },
+ },
+ PTSAdjustment: uint64(5940),
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ }
+
+ for k, c := range cases {
+ t.Run(k, func(t *testing.T) {
+ // decode the binary
+ sis, err := DecodeBase64(c.binary)
+ require.ErrorIs(t, err, c.err)
+ if err != nil {
+ return
+ }
+
+ // test encode/decode XML
+ encodedXML := toXML(sis)
+ assert.Equal(t, toXML(&c.expected), encodedXML)
+ decodedXML := SpliceInfoSection{}
+ assert.NoError(t, xml.Unmarshal([]byte(encodedXML), &decodedXML))
+
+ // legacy 35's produce an "updated" binary so will not match
+ if !c.legacy {
+ assert.Equal(t, c.binary, decodedXML.Base64())
+ }
+
+ // test encode/decode JSON
+ encodedJSON := toJSON(sis)
+ assert.Equal(t, toJSON(&c.expected), encodedJSON)
+ decodedJSON := SpliceInfoSection{}
+ require.NoError(t, json.Unmarshal([]byte(encodedJSON), &decodedJSON))
+
+ // legacy 35's produce an "updated" binary so will not match
+ if !c.legacy {
+ assert.Equal(t, c.binary, decodedJSON.Base64())
+ }
+
+ // uncomment this to verify the output as text
+ // Logger.Printf("\n%s", sis.Table("", "\t"))
+ })
+ }
+}
+
+func TestDecodeHex(t *testing.T) {
+ // when adding tests that contain multiple splice descriptors, care must be
+ // taken to ensure they are in the order specified in the custom UnmarshalXML
+ // implementation, otherwise misleading error may occur
+ cases := map[string]struct {
+ hex string
+ err error
+ expected SpliceInfoSection
+ }{
+ "Sample 14.1 time_signal - Placement Opportunity Start": {
+ hex: "0xFC3034000000000000FFFFF00506FE72BD0050001E021C435545494800008E7FCF0001A599B00808000000002CA0A18A3402009AC9D17E",
+ expected: SpliceInfoSection{
+ EncryptedPacket: EncryptedPacket{EncryptionAlgorithm: EncryptionAlgorithmNone, CWIndex: 255},
+ SpliceCommand: &TimeSignal{
+ SpliceTime: SpliceTime{
+ PTSTime: uint64ptr(0x072bd0050),
+ },
+ },
+ SpliceDescriptors: []SpliceDescriptor{
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ NoRegionalBlackoutFlag: true,
+ ArchiveAllowedFlag: true,
+ DeviceRestrictions: DeviceRestrictionsNone,
+ },
+ SegmentationEventID: uint32(0x4800008e),
+ SegmentationTypeID: SegmentationTypeProviderPOStart,
+ SegmentationDuration: uint64ptr(0x0001a599b0),
+ SegmentationUPIDs: []SegmentationUPID{
+ NewSegmentationUPID(SegmentationUPIDTypeTI, toBytes(0x000000002ca0a18a)),
+ },
+ SegmentNum: 2,
+ },
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ "Sample 14.2 splice_insert (no prefix)": {
+ hex: "FC302F000000000000FFFFF014054800008F7FEFFE7369C02EFE0052CCF500000000000A0008435545490000013562DBA30A",
+ expected: SpliceInfoSection{
+ EncryptedPacket: EncryptedPacket{EncryptionAlgorithm: EncryptionAlgorithmNone, CWIndex: 255},
+ SpliceCommand: &SpliceInsert{
+ BreakDuration: &BreakDuration{
+ AutoReturn: true,
+ Duration: uint64(0x00052ccf5),
+ },
+ SpliceEventID: uint32(0x4800008f),
+ OutOfNetworkIndicator: true,
+ Program: &SpliceInsertProgram{
+ SpliceTime: SpliceTime{
+ PTSTime: uint64ptr(0x07369c02e),
+ },
+ },
+ },
+ SpliceDescriptors: []SpliceDescriptor{
+ &AvailDescriptor{
+ ProviderAvailID: 0x00000135,
+ },
+ },
+ Tier: 4095,
+ SAPType: 3,
+ },
+ },
+ }
+
+ for k, c := range cases {
+ t.Run(k, func(t *testing.T) {
+ // decode the binary
+ sis, err := DecodeHex(c.hex)
+ require.Equal(t, c.err, err)
+ if err != nil {
+ return
+ }
+
+ // test encode/decode XML
+ encodedXML := toXML(sis)
+ assert.Equal(t, toXML(&c.expected), encodedXML)
+ decodedXML := SpliceInfoSection{}
+ assert.NoError(t, xml.Unmarshal([]byte(encodedXML), &decodedXML))
+
+ // test encode/decode JSON
+ encodedJSON := toJSON(sis)
+ assert.Equal(t, toJSON(&c.expected), encodedJSON)
+ decodedJSON := SpliceInfoSection{}
+ require.NoError(t, json.Unmarshal([]byte(encodedJSON), &decodedJSON))
+ })
+ }
+}
+
+func TestEncodeWithAlignmentStuffing(t *testing.T) {
+ cases := map[string]struct {
+ name string
+ binary string
+ }{
+ "SpliceInsert Program Out Point with 3 bytes alignment stuffing": {
+ binary: "/DA0AABS2+YAAACgFAUALJGCf+/+MSwPcX4AUmXAAAAAAAAMAQpDVUVJRp8xMjEq3pnIPCi6lw==",
+ },
+ "SpliceInsert Program In Point with 3 bytes alignment stuffing": {
+ binary: "/DAvAABS2+YAAACgDwUALJGEf0/+MX7z3AAAAAAADAEKQ1VFSQCfMTIxI6SMuQkzWQI=",
+ },
+ }
+
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ sis, err := DecodeBase64(c.binary)
+ require.NoError(t, err)
+ require.Equal(t, c.binary, sis.Base64())
+ })
+ }
+}
+
+func TestTicksToDuration(t *testing.T) {
+ // test a wide range of tick values
+ min := 29 * TicksPerSecond
+ max := 61 * TicksPerSecond
+ for i := min; i < max; i++ {
+ d := TicksToDuration(uint64(i))
+ require.Equal(t, i, int(DurationToTicks(d)))
+ }
+}
+
+// helper func to make test life a bit easier
+
+func toBytes(i uint64) []byte {
+ b := make([]byte, 8)
+ binary.BigEndian.PutUint64(b, i)
+ return b
+}
+
+func toJSON(sis *SpliceInfoSection) string {
+ b, _ := json.MarshalIndent(sis, "", "\t")
+ return string(b)
+}
+
+func toXML(sis *SpliceInfoSection) string {
+ b, _ := xml.MarshalIndent(sis, "", "\t")
+ return string(b)
+}
+
+func uint32ptr(i uint32) *uint32 {
+ return &i
+}
+
+func uint64ptr(i uint64) *uint64 {
+ return &i
+}
diff --git a/internal/scte35/segmentation_descriptor.go b/internal/scte35/segmentation_descriptor.go
@@ -0,0 +1,612 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "encoding/xml"
+ "fmt"
+ "strconv"
+
+ "github.com/bamiaux/iobit"
+)
+
+const (
+ // SegmentationDescriptorTag is the splice_descriptor_tag for
+ // segmentation_descriptor
+ SegmentationDescriptorTag = 0x02
+
+ // SegmentationTypeNotIndicated is the segmentation_type_id for Not Indicated.
+ SegmentationTypeNotIndicated = 0x00
+ // SegmentationTypeContentIdentification is the segmentation_type_id for
+ // Content Identification.
+ SegmentationTypeContentIdentification = 0x01
+ // SegmentationTypeProgramStart is the segmentation_type_id for Program Start.
+ SegmentationTypeProgramStart = 0x10
+ // SegmentationTypeProgramEnd is the segmentation_type_id for Program End.
+ SegmentationTypeProgramEnd = 0x11
+ // SegmentationTypeProgramEarlyTermination is the segmentation_type_id for
+ // Program Early Termination.
+ SegmentationTypeProgramEarlyTermination = 0x12
+ // SegmentationTypeProgramBreakaway is the segmentation_type_id for
+ // Program Breakaway.
+ SegmentationTypeProgramBreakaway = 0x13
+ // SegmentationTypeProgramResumption is the segmentation_type_id for Program
+ // Resumption.
+ SegmentationTypeProgramResumption = 0x14
+ // SegmentationTypeProgramRunoverPlanned is the segmentation_type_id for
+ // Program Runover Planned.
+ SegmentationTypeProgramRunoverPlanned = 0x15
+ // SegmentationTypeProgramRunoverUnplanned is the segmentation_type_id for
+ // Program Runover Unplanned.
+ SegmentationTypeProgramRunoverUnplanned = 0x16
+ // SegmentationTypeProgramOverlapStart is the segmentation_type_id for Program
+ // Overlap Start.
+ SegmentationTypeProgramOverlapStart = 0x17
+ // SegmentationTypeProgramBlackoutOverride is the segmentation_type_id for
+ // Program Blackout Override.
+ SegmentationTypeProgramBlackoutOverride = 0x18
+ // SegmentationTypeProgramStartInProgress is the segmentation_type_id for
+ // Program Start - In Progress.
+ SegmentationTypeProgramStartInProgress = 0x19
+ // SegmentationTypeChapterStart is the segmentation_type_id for Chapter Start.
+ SegmentationTypeChapterStart = 0x20
+ // SegmentationTypeChapterEnd is the segmentation_type_id for Chapter End.
+ SegmentationTypeChapterEnd = 0x21
+ // SegmentationTypeBreakStart is the segmentation_type_id for Break Start.
+ // Added in ANSI/SCTE 2017.
+ SegmentationTypeBreakStart = 0x22
+ // SegmentationTypeBreakEnd is the segmentation_type_id for Break End.
+ // Added in ANSI/SCTE 2017.
+ SegmentationTypeBreakEnd = 0x23
+ // SegmentationTypeOpeningCreditStart is the segmentation_type_id for
+ // Opening Credit Start. Added in ANSI/SCTE 2020.
+ SegmentationTypeOpeningCreditStart = 0x24
+ // SegmentationTypeOpeningCreditEnd is the segmentation_type_id for
+ // Opening Credit End. Added in ANSI/SCTE 2020.
+ SegmentationTypeOpeningCreditEnd = 0x25
+ // SegmentationTypeClosingCreditStart is the segmentation_type_id for
+ // Closing Credit Start. Added in ANSI/SCTE 2020.
+ SegmentationTypeClosingCreditStart = 0x26
+ // SegmentationTypeClosingCreditEnd is the segmentation_type_id for
+ // Closing Credit End. Added in ANSI/SCTE 2020.
+ SegmentationTypeClosingCreditEnd = 0x27
+ // SegmentationTypeProviderAdStart is the segmentation_type_id for Provider
+ // Ad Start.
+ SegmentationTypeProviderAdStart = 0x30
+ // SegmentationTypeProviderAdEnd is the segmentation_type_id for Provider Ad
+ // End.
+ SegmentationTypeProviderAdEnd = 0x31
+ // SegmentationTypeDistributorAdStart is the segmentation_type_id for
+ // Distributor Ad Start.
+ SegmentationTypeDistributorAdStart = 0x32
+ // SegmentationTypeDistributorAdEnd is the segmentation_type_id for
+ // Distributor Ad End.
+ SegmentationTypeDistributorAdEnd = 0x33
+ // SegmentationTypeProviderPOStart is the segmentation_type_id for Provider
+ // PO Start.
+ SegmentationTypeProviderPOStart = 0x34
+ // SegmentationTypeProviderPOEnd is the segmentation_type_id for Provider PO
+ // End.
+ SegmentationTypeProviderPOEnd = 0x35
+ // SegmentationTypeDistributorPOStart is the segmentation_type_id for
+ // Distributor PO Start.
+ SegmentationTypeDistributorPOStart = 0x36
+ // SegmentationTypeDistributorPOEnd is the segmentation_type_id for
+ // Distributor PO End.
+ SegmentationTypeDistributorPOEnd = 0x37
+ // SegmentationTypeProviderOverlayPOStart is the segmentation_type_id for
+ // Provider Overlay Placement Opportunity Start.
+ SegmentationTypeProviderOverlayPOStart = 0x38
+ // SegmentationTypeProviderOverlayPOEnd is the segmentation_type_id for
+ // Provider Overlay Placement Opportunity End.
+ SegmentationTypeProviderOverlayPOEnd = 0x39
+ // SegmentationTypeDistributorOverlayPOStart is the segmentation_type_id for
+ // Distributor Overlay Placement Opportunity Start.
+ SegmentationTypeDistributorOverlayPOStart = 0x3a
+ // SegmentationTypeDistributorOverlayPOEnd is the segmentation_type_id for
+ // Distributor Overlay Placement Opportunity End.
+ SegmentationTypeDistributorOverlayPOEnd = 0x3b
+ // SegmentationTypeProviderPromoStart is the segmentation_type_id for
+ // Provider Promo Start. Added in ANSI/SCTE 2020.
+ SegmentationTypeProviderPromoStart = 0x3c
+ // SegmentationTypeProviderPromoEnd is the segmentation_type_id for
+ // Provider Promo End. Added in ANSI/SCTE 2020.
+ SegmentationTypeProviderPromoEnd = 0x3d
+ // SegmentationTypeDistributorPromoStart is the segmentation_type_id for
+ // Distributor Promo Start. Added in ANSI/SCTE 2020.
+ SegmentationTypeDistributorPromoStart = 0x3e
+ // SegmentationTypeDistributorPromoEnd is the segmentation_type_id for
+ // Distributor Promo End. Added in ANSI/SCTE 2020.
+ SegmentationTypeDistributorPromoEnd = 0x3f
+ // SegmentationTypeUnscheduledEventStart is the segmentation_type_id for
+ // Unscheduled Event Start.
+ SegmentationTypeUnscheduledEventStart = 0x40
+ // SegmentationTypeUnscheduledEventEnd is the segmentation_type_id for
+ // Unscheduled Event End.
+ SegmentationTypeUnscheduledEventEnd = 0x41
+ // SegmentationTypeAltConOppStart is the segmentation_type_id for
+ // Alternate Content Opportunity Start. Added in ANSI/SCTE 2020.
+ SegmentationTypeAltConOppStart = 0x42
+ // SegmentationTypeAltConOppEnd is the segmentation_type_id for
+ // Alternate Content Opportunity End. Added in ANSI/SCTE 2020.
+ SegmentationTypeAltConOppEnd = 0x43
+ // SegmentationTypeProviderAdBlockStart is the segmentation_type_id for
+ // Provider Ad Block Start. Added in ANSI/SCTE 2020.
+ SegmentationTypeProviderAdBlockStart = 0x44
+ // SegmentationTypeProviderAdBlockEnd is the segmentation_type_id for
+ // Provider Ad Block End. Added in ANSI/SCTE 2020.
+ SegmentationTypeProviderAdBlockEnd = 0x45
+ // SegmentationTypeDistributorAdBlockStart is the segmentation_type_id for
+ // Distributor Ad Block Start. Added in ANSI/SCTE 2020.
+ SegmentationTypeDistributorAdBlockStart = 0x46
+ // SegmentationTypeDistributorAdBlockEnd is the segmentation_type_id for
+ // Distributor Ad Block End. Added in ANSI/SCTE 2020.
+ SegmentationTypeDistributorAdBlockEnd = 0x47
+ // SegmentationTypeNetworkStart is the segmentation_type_id for Network Start.
+ SegmentationTypeNetworkStart = 0x50
+ // SegmentationTypeNetworkEnd is the segmentation_type_id for Network End.
+ SegmentationTypeNetworkEnd = 0x51
+)
+
+// SegmentationDescriptor is an implementation of a splice_descriptor(). It
+// provides an optional extension to the time_signal() and splice_insert()
+// commands that allows for segmentation messages to be sent in a time/video
+// accurate method. This descriptor shall only be used with the time_signal(),
+// splice_insert() and the splice_null() commands. The time_signal() or
+// splice_insert() message should be sent at least once a minimum of 4 seconds
+// in advance of the signaled splice_time() to permit the insertion device to
+// place the splice_info_section( ) accurately.
+type SegmentationDescriptor struct {
+ XMLName xml.Name `xml:"http://www.scte.org/schemas/35 SegmentationDescriptor" json:"-"`
+ JSONType uint32 `xml:"-" json:"type"`
+ DeliveryRestrictions *DeliveryRestrictions `xml:"http://www.scte.org/schemas/35 DeliveryRestrictions" json:"deliveryRestrictions,omitempty"`
+ SegmentationUPIDs []SegmentationUPID `xml:"http://www.scte.org/schemas/35 SegmentationUpid" json:"segmentationUpids,omitempty"`
+ Components []SegmentationDescriptorComponent `xml:"http://www.scte.org/schemas/35 Component" json:"components,omitempty"`
+ SegmentationEventID uint32 `xml:"segmentationEventId,attr,omitempty" json:"segmentationEventId,omitempty"`
+ SegmentationEventCancelIndicator bool `xml:"segmentationEventCancelIndicator,attr,omitempty" json:"segmentationEventCancelIndicator,omitempty"`
+ SegmentationDuration *uint64 `xml:"segmentationDuration,attr" json:"segmentationDuration,omitempty"`
+ SegmentationTypeID uint32 `xml:"segmentationTypeId,attr,omitempty" json:"segmentationTypeId,omitempty"`
+ SegmentNum uint32 `xml:"segmentNum,attr,omitempty" json:"segmentNum,omitempty"`
+ SegmentsExpected uint32 `xml:"segmentsExpected,attr,omitempty" json:"segmentsExpected,omitempty"`
+ SubSegmentNum *uint32 `xml:"subSegmentNum,attr" json:"subSegmentNum,omitempty"`
+ SubSegmentsExpected *uint32 `xml:"subSegmentsExpected,attr" json:"subSegmentsExpected,omitempty"`
+}
+
+// Name returns the human-readable string for the segmentation_type_id.
+func (sd *SegmentationDescriptor) Name() string {
+ switch sd.SegmentationTypeID {
+ case SegmentationTypeNotIndicated:
+ return "Not Indicated"
+ case SegmentationTypeContentIdentification:
+ return "Content Identification"
+ case SegmentationTypeProgramStart:
+ return "Program Start"
+ case SegmentationTypeProgramEnd:
+ return "Program End"
+ case SegmentationTypeProgramEarlyTermination:
+ return "Program Early Termination"
+ case SegmentationTypeProgramBreakaway:
+ return "Program Breakaway"
+ case SegmentationTypeProgramResumption:
+ return "Program Resumption"
+ case SegmentationTypeProgramRunoverPlanned:
+ return "Program Runover Planned"
+ case SegmentationTypeProgramRunoverUnplanned:
+ return "Program Runover Unplanned"
+ case SegmentationTypeProgramOverlapStart:
+ return "Program Overlap Start"
+ case SegmentationTypeProgramBlackoutOverride:
+ return "Program Blackout Override"
+ case SegmentationTypeProgramStartInProgress:
+ return "Program Start - In Progress"
+ case SegmentationTypeChapterStart:
+ return "Chapter Start"
+ case SegmentationTypeChapterEnd:
+ return "Chapter End"
+ case SegmentationTypeBreakStart:
+ return "Break Start"
+ case SegmentationTypeBreakEnd:
+ return "Break End"
+ case SegmentationTypeOpeningCreditStart:
+ return "Opening Credit Start"
+ case SegmentationTypeOpeningCreditEnd:
+ return "Opening Credit End"
+ case SegmentationTypeClosingCreditStart:
+ return "Closing Credit Start"
+ case SegmentationTypeClosingCreditEnd:
+ return "Closing Credit End"
+ case SegmentationTypeProviderAdStart:
+ return "Provider Advertisement Start"
+ case SegmentationTypeProviderAdEnd:
+ return "Provider Advertisement End"
+ case SegmentationTypeDistributorAdStart:
+ return "Distributor Advertisement Start"
+ case SegmentationTypeDistributorAdEnd:
+ return "Distributor Advertisement End"
+ case SegmentationTypeProviderPOStart:
+ return "Provider Placement Opportunity Start"
+ case SegmentationTypeProviderPOEnd:
+ return "Provider Placement Opportunity End"
+ case SegmentationTypeDistributorPOStart:
+ return "Distributor Placement Opportunity Start"
+ case SegmentationTypeDistributorPOEnd:
+ return "Distributor Placement Opportunity End"
+ case SegmentationTypeProviderOverlayPOStart:
+ return "Provider Overlay Placement Opportunity Start"
+ case SegmentationTypeProviderOverlayPOEnd:
+ return "Provider Overlay Placement Opportunity End"
+ case SegmentationTypeDistributorOverlayPOStart:
+ return "Distributor Overlay Placement Opportunity Start"
+ case SegmentationTypeDistributorOverlayPOEnd:
+ return "Distributor Overlay Placement Opportunity End"
+ case SegmentationTypeProviderPromoStart:
+ return "Provider Promo Start"
+ case SegmentationTypeProviderPromoEnd:
+ return "Provider Promo End"
+ case SegmentationTypeDistributorPromoStart:
+ return "Distributor Promo Start"
+ case SegmentationTypeDistributorPromoEnd:
+ return "Distributor Promo End"
+ case SegmentationTypeUnscheduledEventStart:
+ return "Unscheduled Event Start"
+ case SegmentationTypeUnscheduledEventEnd:
+ return "Unscheduled Event End"
+ case SegmentationTypeAltConOppStart:
+ return "Alternate Content Opportunity Start"
+ case SegmentationTypeAltConOppEnd:
+ return "Alternate Content Opportunity End"
+ case SegmentationTypeProviderAdBlockStart:
+ return "Provider Ad Block Start"
+ case SegmentationTypeProviderAdBlockEnd:
+ return "Provider Ad Block End"
+ case SegmentationTypeDistributorAdBlockStart:
+ return "Distributor Ad Block Start"
+ case SegmentationTypeDistributorAdBlockEnd:
+ return "Distributor Ad Block End"
+ case SegmentationTypeNetworkStart:
+ return "Network Start"
+ case SegmentationTypeNetworkEnd:
+ return "Network End"
+ default:
+ return "Unknown"
+ }
+}
+
+// Tag returns the splice_descriptor_tag.
+func (sd *SegmentationDescriptor) Tag() uint32 {
+ // ensure JSONType is set
+ sd.JSONType = SegmentationDescriptorTag
+ return SegmentationDescriptorTag
+}
+
+// DeliveryNotRestrictedFlag returns the delivery_not_restricted_flag.
+func (sd *SegmentationDescriptor) DeliveryNotRestrictedFlag() bool {
+ return sd.DeliveryRestrictions == nil
+}
+
+// ProgramSegmentationFlag returns the program_segmentation_flag.
+func (sd *SegmentationDescriptor) ProgramSegmentationFlag() bool {
+ return len(sd.Components) == 0
+}
+
+// SegmentationDurationFlag returns the segmentation_duration_flag.
+func (sd *SegmentationDescriptor) SegmentationDurationFlag() bool {
+ return sd.SegmentationDuration != nil
+}
+
+// SegmentationUpidLength return the segmentation_upid_length
+func (sd *SegmentationDescriptor) SegmentationUpidLength() int {
+ length := 0
+ if len(sd.SegmentationUPIDs) == 1 {
+ length += len(sd.SegmentationUPIDs[0].valueBytes()) * 8 // segmentation_upid() (bytes -> bits)
+ } else if len(sd.SegmentationUPIDs) > 1 {
+ // for MID, include type & length with each contained upid
+ for _, upid := range sd.SegmentationUPIDs {
+ length += 8 // segmentation_upid_type
+ length += 8 // segmentation_upid_length
+ length += len(upid.valueBytes()) * 8 // segmentation_upid (bytes -> bits)
+ }
+ }
+ return length / 8
+}
+
+// decode updates this splice_descriptor from binary.
+func (sd *SegmentationDescriptor) decode(b []byte) error {
+ var err error
+
+ r := iobit.NewReader(b)
+ r.Skip(8) // splice_descriptor_tag
+ r.Skip(8) // descriptor_length
+ r.Skip(32) // identifier
+ sd.SegmentationEventID = r.Uint32(32)
+ sd.SegmentationEventCancelIndicator = r.Bit()
+ r.Skip(7) // reserved
+
+ if !sd.SegmentationEventCancelIndicator {
+ programSegmentationFlag := r.Bit()
+ segmentationDurationFlag := r.Bit()
+ deliveryNotRestrictedFlag := r.Bit()
+
+ if !deliveryNotRestrictedFlag {
+ sd.DeliveryRestrictions = &DeliveryRestrictions{}
+ sd.DeliveryRestrictions.WebDeliveryAllowedFlag = r.Bit()
+ sd.DeliveryRestrictions.NoRegionalBlackoutFlag = r.Bit()
+ sd.DeliveryRestrictions.ArchiveAllowedFlag = r.Bit()
+ sd.DeliveryRestrictions.DeviceRestrictions = r.Uint32(2)
+ } else {
+ r.Skip(5) // reserved
+ }
+
+ if !programSegmentationFlag {
+ componentCount := int(r.Uint32(8))
+ sd.Components = make([]SegmentationDescriptorComponent, componentCount)
+ for i := 0; i < componentCount; i++ {
+ c := SegmentationDescriptorComponent{}
+ c.Tag = r.Uint32(8)
+ r.Skip(7) // reserved
+ c.PTSOffset = r.Uint64(33)
+ sd.Components[i] = c
+ }
+ }
+
+ if segmentationDurationFlag {
+ dur := r.Uint64(40)
+ sd.SegmentationDuration = &dur
+ }
+
+ segmentationUpidType := r.Uint32(8)
+ segmentationUpidLength := int(r.Uint32(8))
+ if segmentationUpidLength > 0 {
+ segmentationUpidValue := r.Bytes(segmentationUpidLength)
+
+ if segmentationUpidType == SegmentationUPIDTypeMID {
+ upidr := iobit.NewReader(segmentationUpidValue)
+ sd.SegmentationUPIDs = []SegmentationUPID{}
+ for upidr.LeftBits() > 0 {
+ upidType := upidr.Uint32(8)
+ upidLength := int(upidr.Uint32(8))
+ upidValue := upidr.Bytes(upidLength)
+ if len(upidValue) < upidLength {
+ Logger.Printf("Cannot read value for segmentation_upid_type %d; %d of %d bytes remaining.", upidType, len(upidValue), upidLength)
+ }
+ sd.SegmentationUPIDs = append(
+ sd.SegmentationUPIDs,
+ NewSegmentationUPID(upidType, upidValue),
+ )
+ }
+ } else {
+ sd.SegmentationUPIDs = []SegmentationUPID{
+ NewSegmentationUPID(segmentationUpidType, segmentationUpidValue),
+ }
+ }
+ }
+
+ sd.SegmentationTypeID = r.Uint32(8)
+ sd.SegmentNum = r.Uint32(8)
+ sd.SegmentsExpected = r.Uint32(8)
+
+ // these fields are new in 2016 so we need a secondary check whether
+ // they were actually included in the binary payload
+ if sd.SegmentationTypeID == SegmentationTypeProviderPOStart || sd.SegmentationTypeID == SegmentationTypeDistributorPOStart {
+ if r.LeftBits() == 16 {
+ n := r.Uint32(8)
+ e := r.Uint32(8)
+ sd.SubSegmentNum = &n
+ sd.SubSegmentsExpected = &e
+ }
+ }
+ }
+
+ if err != nil {
+ return err
+ }
+ if err := readerError(r); err != nil {
+ return fmt.Errorf("segmentation_descriptor: %w", err)
+ }
+ return nil
+}
+
+// encode this splice_descriptor to binary.
+func (sd *SegmentationDescriptor) encode() ([]byte, error) {
+ length := sd.length()
+
+ // add 2 bytes to contain splice_descriptor_tag & descriptor_length
+ buf := make([]byte, length+2)
+ iow := iobit.NewWriter(buf)
+ iow.PutUint32(8, SegmentationDescriptorTag)
+ iow.PutUint32(8, uint32(length))
+ iow.PutUint32(32, CUEIdentifier)
+ iow.PutUint32(32, sd.SegmentationEventID)
+ iow.PutBit(sd.SegmentationEventCancelIndicator)
+ iow.PutUint32(7, Reserved)
+
+ if !sd.SegmentationEventCancelIndicator {
+ iow.PutBit(sd.ProgramSegmentationFlag())
+ iow.PutBit(sd.SegmentationDurationFlag())
+
+ iow.PutBit(sd.DeliveryNotRestrictedFlag())
+ if sd.DeliveryRestrictions != nil {
+ iow.PutBit(sd.DeliveryRestrictions.WebDeliveryAllowedFlag)
+ iow.PutBit(sd.DeliveryRestrictions.NoRegionalBlackoutFlag)
+ iow.PutBit(sd.DeliveryRestrictions.ArchiveAllowedFlag)
+ iow.PutUint32(2, sd.DeliveryRestrictions.DeviceRestrictions)
+ } else {
+ iow.PutUint32(5, Reserved)
+ }
+
+ if !sd.ProgramSegmentationFlag() {
+ iow.PutUint32(8, uint32(len(sd.Components)))
+ for _, c := range sd.Components {
+ iow.PutUint32(8, c.Tag)
+ iow.PutUint32(7, Reserved)
+ iow.PutUint64(33, c.PTSOffset)
+ }
+ }
+
+ if sd.SegmentationDurationFlag() {
+ iow.PutUint64(40, *sd.SegmentationDuration)
+ }
+
+ switch len(sd.SegmentationUPIDs) {
+ case 0:
+ iow.PutUint32(8, 0x00) // segmentation_upid_type
+ iow.PutUint32(8, 0x00) // segmentation_upid_length
+ case 1:
+ vb := sd.SegmentationUPIDs[0].valueBytes()
+ iow.PutUint32(8, sd.SegmentationUPIDs[0].Type)
+ iow.PutUint32(8, uint32(len(vb)))
+ _, _ = iow.Write(vb)
+ default:
+ iow.PutUint32(8, SegmentationUPIDTypeMID)
+ iow.PutUint32(8, uint32(sd.SegmentationUpidLength()))
+ for _, upid := range sd.SegmentationUPIDs {
+ vb := upid.valueBytes()
+ iow.PutUint32(8, upid.Type)
+ iow.PutUint32(8, uint32(len(vb)))
+ _, _ = iow.Write(vb)
+ }
+ }
+
+ iow.PutUint32(8, sd.SegmentationTypeID)
+ iow.PutUint32(8, sd.SegmentNum)
+ iow.PutUint32(8, sd.SegmentsExpected)
+
+ if sd.SubSegmentNum != nil {
+ iow.PutUint32(8, *sd.SubSegmentNum)
+ }
+ if sd.SubSegmentsExpected != nil {
+ iow.PutUint32(8, *sd.SubSegmentsExpected)
+ }
+ }
+
+ err := iow.Flush()
+ return buf, err
+}
+
+// descriptorLength returns the descriptor_length
+func (sd *SegmentationDescriptor) length() int {
+ length := 32 // identifier
+ length += 32 // segmentation_event_id
+ length++ // segmentation_event_cancel_indicator
+ length += 7 // reserved
+
+ // if segmentation_event_cancel_indicator == 0
+ if !sd.SegmentationEventCancelIndicator {
+ length++ // program_segmentation_flag
+ length++ // segmentation_duration_flag
+ length++ // delivery_not_restricted_flag
+ length += 5 // delivery restriction flags or reserved
+
+ // if program_segmentation_flag == 0
+ if !sd.ProgramSegmentationFlag() {
+ length += 8 // component_count
+
+ // for i=0 to component_count
+ for range sd.Components {
+ length += 8 // component_tag
+ length += 7 // reserved
+ length += 33 // pts_offset
+ }
+ }
+ if sd.SegmentationDurationFlag() {
+ length += 40 // segmentation_duration
+ }
+ length += 8 // segmentation_upid_type
+ length += 8 // segmentation_upid_length
+ length += sd.SegmentationUpidLength() * 8 // segmentation_upid() (bytes -> bits)
+ length += 8 // segmentation_type_id
+ length += 8 // segment_num
+ length += 8 // segments_expected
+
+ if sd.SubSegmentNum != nil {
+ length += 8 // sub_segment_num
+ }
+ if sd.SubSegmentsExpected != nil {
+ length += 8 // sub_segments_expected
+ }
+ }
+
+ return length / 8
+}
+
+// table returns the tabular description of this SegmentationDescriptor.
+func (sd *SegmentationDescriptor) writeTo(t *table) {
+ t.row(0, "segmentation_descriptor() {", nil)
+ t.row(1, "splice_descriptor_tag", fmt.Sprintf("%#02x", sd.Tag()))
+ t.row(1, "descriptor_length", sd.length())
+ t.row(1, "identifier", fmt.Sprintf("%#08x (%s)", CUEIdentifier, CUEIASCII))
+ t.row(1, "segmentation_event_id", sd.SegmentationEventID)
+ t.row(1, "segmentation_event_cancel_indicator", sd.SegmentationEventCancelIndicator)
+ if !sd.SegmentationEventCancelIndicator {
+ t.row(1, "program_segmentation_flag", sd.ProgramSegmentationFlag())
+ t.row(1, "segmentation_duration_flag", sd.SegmentationDurationFlag())
+ t.row(1, "delivery_not_restricted_flag", sd.DeliveryNotRestrictedFlag())
+ if sd.DeliveryRestrictions != nil {
+ t.row(1, "web_delivery_allowed_flag", sd.DeliveryRestrictions.WebDeliveryAllowedFlag)
+ t.row(1, "no_regional_blackout_flag", sd.DeliveryRestrictions.NoRegionalBlackoutFlag)
+ t.row(1, "archive_allowed_flag", sd.DeliveryRestrictions.ArchiveAllowedFlag)
+ t.row(1, "device_restrictions", fmt.Sprintf("%d (%s)", sd.DeliveryRestrictions.DeviceRestrictions, sd.DeliveryRestrictions.deviceRestrictionsName()))
+ }
+ if len(sd.Components) > 0 {
+ t.row(1, "component_count", len(sd.Components))
+ for i, c := range sd.Components {
+ t.row(1, "component["+strconv.Itoa(i)+"] {", nil)
+ t.row(2, "component_tag", c.Tag)
+ t.row(2, "pts_offset", c.PTSOffset)
+ t.row(1, "}", nil)
+ }
+ }
+ if sd.SegmentationDurationFlag() {
+ t.row(1, "segmentation_duration", sd.SegmentationDuration)
+ }
+
+ t.row(1, "segmentation_upid_length", sd.SegmentationUpidLength())
+ for i, u := range sd.SegmentationUPIDs {
+ t.row(1, "segmentation_upid["+strconv.Itoa(i)+"] {", nil)
+ t.row(2, "segmentation_upid_type", fmt.Sprintf("%#02x (%s)", u.Type, u.Name()))
+ if u.Type == SegmentationUPIDTypeMPU {
+ t.row(2, "format_identifier", u.formatIdentifierString())
+ }
+ t.row(2, "segmentation_upid", u.Value)
+ t.row(1, "}", nil)
+ }
+ }
+
+ t.row(1, "segmentation_type_id", fmt.Sprintf("%#02x (%s)", sd.SegmentationTypeID, sd.Name()))
+ t.row(1, "segment_num", sd.SegmentNum)
+ t.row(1, "segments_expected", sd.SegmentsExpected)
+ switch sd.SegmentationTypeID {
+ case SegmentationTypeProviderPOStart,
+ SegmentationTypeDistributorPOStart,
+ SegmentationTypeProviderOverlayPOStart,
+ SegmentationTypeDistributorOverlayPOStart:
+ if sd.SubSegmentNum != nil {
+ t.row(1, "sub_segment_num", sd.SubSegmentNum)
+ }
+ if sd.SubSegmentsExpected != nil {
+ t.row(1, "sub_segments_expected", sd.SubSegmentsExpected)
+ }
+ }
+ t.row(0, "}", nil)
+}
+
+// SegmentationDescriptorComponent describes the Component element contained
+// within the SegmentationDescriptorType XML schema definition.
+type SegmentationDescriptorComponent struct {
+ Tag uint32 `xml:"componentTag,attr" json:"componentTag"`
+ PTSOffset uint64 `xml:"ptsOffset,attr" json:"ptsOffset"`
+}
diff --git a/internal/scte35/segmentation_upid.go b/internal/scte35/segmentation_upid.go
@@ -0,0 +1,302 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/binary"
+ "encoding/hex"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/bamiaux/iobit"
+ "golang.org/x/text/encoding/charmap"
+)
+
+const (
+ // SegmentationUPIDTypeNotUsed is the segmentation_upid_type for Not Used.
+ SegmentationUPIDTypeNotUsed = 0x00
+ // SegmentationUPIDTypeUserDefined is the segmentation_upid_type for User
+ // Defined.
+ SegmentationUPIDTypeUserDefined = 0x01
+ // SegmentationUPIDTypeISCI is the segmentation_upid_type for ISCI
+ SegmentationUPIDTypeISCI = 0x02
+ // SegmentationUPIDTypeAdID is the segmentation_upid_type for Ad-ID
+ SegmentationUPIDTypeAdID = 0x03
+ // SegmentationUPIDTypeUMID is the segmentation_upid_type for UMID
+ SegmentationUPIDTypeUMID = 0x04
+ // SegmentationUPIDTypeISANDeprecated is the segmentation_upid_type for
+ // ISAN Deprecated.
+ SegmentationUPIDTypeISANDeprecated = 0x05
+ // SegmentationUPIDTypeISAN is the segmentation_upid_type for ISAN.
+ SegmentationUPIDTypeISAN = 0x06
+ // SegmentationUPIDTypeTID is the segmentation_upid_type for TID.
+ SegmentationUPIDTypeTID = 0x07
+ // SegmentationUPIDTypeTI is the segmentation_upid_type for TI.
+ SegmentationUPIDTypeTI = 0x08
+ // SegmentationUPIDTypeADI is the segmentation_upid_type for ADI.
+ SegmentationUPIDTypeADI = 0x09
+ // SegmentationUPIDTypeEIDR is the segmentation_upid_type for EIDR.
+ SegmentationUPIDTypeEIDR = 0x0a
+ // SegmentationUPIDTypeATSC is the segmentation_upid_type for ATSC Content
+ // Identifier.
+ SegmentationUPIDTypeATSC = 0x0b
+ // SegmentationUPIDTypeMPU is the segmentation_upid_type for MPU().
+ SegmentationUPIDTypeMPU = 0x0c
+ // SegmentationUPIDTypeMID is the segmentation_upid_type for MID().
+ SegmentationUPIDTypeMID = 0x0d
+ // SegmentationUPIDTypeADS is the segmentation_upid_type for ADS Information.
+ SegmentationUPIDTypeADS = 0x0e
+ // SegmentationUPIDTypeURI is the segmentation_upid_type for URI.
+ SegmentationUPIDTypeURI = 0x0f
+ // SegmentationUPIDTypeUUID is the segmentation_upid_type for UUID.
+ SegmentationUPIDTypeUUID = 0x10
+ // SegmentationUPIDTypeSCR is the segmentation_upid_type for SCR.
+ SegmentationUPIDTypeSCR = 0x11
+)
+
+// NewSegmentationUPID construct a new SegmentationUPID
+func NewSegmentationUPID(upidType uint32, buf []byte) SegmentationUPID {
+ r := iobit.NewReader(buf)
+
+ switch upidType {
+ // EIDR - custom
+ case SegmentationUPIDTypeEIDR:
+ return SegmentationUPID{
+ Type: upidType,
+ Value: canonicalEIDR(r.LeftBytes()),
+ }
+ // ISAN - base64
+ case SegmentationUPIDTypeISAN, SegmentationUPIDTypeISANDeprecated:
+ return SegmentationUPID{
+ Type: upidType,
+ Value: base64.StdEncoding.EncodeToString(r.LeftBytes()),
+ }
+ // MPU - custom
+ case SegmentationUPIDTypeMPU:
+ fi := r.Uint32(32)
+ return SegmentationUPID{
+ Type: upidType,
+ FormatIdentifier: &fi,
+ Value: base64.StdEncoding.EncodeToString(r.LeftBytes()),
+ }
+ // TI - unsigned int
+ case SegmentationUPIDTypeTI:
+ return SegmentationUPID{
+ Type: upidType,
+ Value: strconv.FormatUint(r.Uint64(r.LeftBits()), 10),
+ }
+ // everything else - plain text
+ default:
+ // decode troublesome Latin1 characters to their UTF8 equivalents
+ b, _ := charmap.ISO8859_1.NewDecoder().Bytes(r.LeftBytes())
+ return SegmentationUPID{
+ Type: upidType,
+ Value: string(b),
+ }
+ }
+}
+
+// SegmentationUPID is used to express a UPID in an XML document.
+type SegmentationUPID struct {
+ Type uint32 `xml:"segmentationUpidType,attr" json:"segmentationUpidType"`
+ FormatIdentifier *uint32 `xml:"formatIdentifier,attr,omitempty" json:"formatIdentifier,omitempty"`
+ Value string `xml:",chardata" json:"value"`
+ // Deprecated: no longer used and will be removed in a future release
+ Format string `xml:"-" json:"-"`
+}
+
+// Name returns the name for the segmentation_upid_type.
+func (upid *SegmentationUPID) Name() string {
+ switch upid.Type {
+ case SegmentationUPIDTypeNotUsed:
+ return "Not Used"
+ case SegmentationUPIDTypeUserDefined:
+ return "User Defined"
+ case SegmentationUPIDTypeISCI:
+ return "ISCI"
+ case SegmentationUPIDTypeAdID:
+ return "Ad-ID"
+ case SegmentationUPIDTypeUMID:
+ return "UMID"
+ case SegmentationUPIDTypeISANDeprecated:
+ return "ISAN (Deprecated)"
+ case SegmentationUPIDTypeISAN:
+ return "ISAN"
+ case SegmentationUPIDTypeTID:
+ return "TID"
+ case SegmentationUPIDTypeTI:
+ return "TI"
+ case SegmentationUPIDTypeADI:
+ return "ADI"
+ case SegmentationUPIDTypeEIDR:
+ return "EIDR: " + upid.eidrTypeName()
+ case SegmentationUPIDTypeATSC:
+ return "ATSC Content Identifier"
+ case SegmentationUPIDTypeMPU:
+ return "MPU()"
+ case SegmentationUPIDTypeMID:
+ return "MID()"
+ case SegmentationUPIDTypeADS:
+ return "ADS Information"
+ case SegmentationUPIDTypeURI:
+ return "URI"
+ case SegmentationUPIDTypeUUID:
+ return "UUID"
+ case SegmentationUPIDTypeSCR:
+ return "SCR"
+ default:
+ return "Unknown"
+ }
+}
+
+// ASCIIValue returns Value as an ASCII string. Characters outside the printable
+// range are represented by a dot (".").
+func (upid *SegmentationUPID) ASCIIValue() string {
+ b := upid.valueBytes()
+ rs := make([]byte, len(b))
+ for i := range b {
+ if b[i] > 31 && b[i] < 127 {
+ rs[i] = b[i]
+ continue
+ }
+ rs[i] = '.'
+ }
+ return string(rs)
+}
+
+// compressEIRD returns a compressed EIDR.
+func (upid *SegmentationUPID) compressEIDR(s string) []byte {
+ parts := strings.FieldsFunc(s, func(r rune) bool {
+ return r == '.' || r == '/'
+ })
+
+ if len(parts) != 3 {
+ Logger.Printf("EIDR string contains too many parts: %s", s)
+ return []byte(s)
+ }
+
+ i, err := strconv.Atoi(parts[1])
+ if err != nil {
+ Logger.Printf("Non-canonical EIDR prefix: '%s'", s)
+ return []byte(s)
+ }
+
+ b := make([]byte, 12)
+ iow := iobit.NewWriter(b)
+ iow.PutUint32(16, uint32(i))
+
+ h, err := hex.DecodeString(strings.ReplaceAll(parts[2], "-", ""))
+ if err != nil {
+ Logger.Printf("Non-canonical EIDR suffix: '%s'", s)
+ return []byte(s)
+ }
+
+ _, _ = iow.Write(h)
+ _ = iow.Flush()
+
+ return b
+}
+
+// eidrTypeName returns the EIDR type name.
+func (upid *SegmentationUPID) eidrTypeName() string {
+ if strings.HasPrefix(upid.Value, "10.5237") {
+ return "Party ID"
+ }
+ if strings.HasPrefix(upid.Value, "10.5238") {
+ return "User ID"
+ }
+ if strings.HasPrefix(upid.Value, "10.5239") {
+ return "Service ID"
+ }
+ if strings.HasPrefix(upid.Value, "10.5240") {
+ return "Content ID"
+ }
+ return ""
+}
+
+// formatIdentifierString returns the format identifier as a string
+func (upid *SegmentationUPID) formatIdentifierString() string {
+ b := make([]byte, 4)
+ binary.BigEndian.PutUint32(b, *upid.FormatIdentifier)
+ return string(b)
+}
+
+// valueBytes returns the value as a byte array.
+func (upid *SegmentationUPID) valueBytes() []byte {
+ upid.Value = strings.TrimSpace(upid.Value)
+
+ // this switch should align with the constructor above
+ switch upid.Type {
+ // EIDR - custom
+ case SegmentationUPIDTypeEIDR:
+ return upid.compressEIDR(upid.Value)
+ // ISAN - base64
+ case SegmentationUPIDTypeISAN, SegmentationUPIDTypeISANDeprecated:
+ b, err := base64.StdEncoding.DecodeString(upid.Value)
+ if err != nil {
+ Logger.Printf("Error parsing UPID value: %s", err)
+ return b
+ }
+ return b
+ // MPU - custom
+ case SegmentationUPIDTypeMPU:
+ b := make([]byte, 4)
+ binary.BigEndian.PutUint32(b, *upid.FormatIdentifier)
+ v, err := base64.StdEncoding.DecodeString(upid.Value)
+ if err != nil {
+ Logger.Printf("Error parsing UPID value: %s", err)
+ return b
+ }
+ b = append(b, v...)
+ return b
+ // TI - unsigned int
+ case SegmentationUPIDTypeTI:
+ b := make([]byte, 8)
+ i, err := strconv.ParseUint(strings.TrimSpace(upid.Value), 10, 64)
+ if err != nil {
+ Logger.Printf("Error parsing UPID value: %s", err)
+ return b
+ }
+ binary.BigEndian.PutUint64(b, i)
+ return b
+ // everything else - plain text
+ default:
+ // encode UTF8 values as Latin1 (reversing the Decode above)
+ b, _ := charmap.ISO8859_1.NewEncoder().Bytes([]byte(upid.Value))
+ return b
+ }
+}
+
+// canonicalEIDR returns a canonical EIDR.
+func canonicalEIDR(b []byte) string {
+ // already canonical
+ if bytes.Contains(b, []byte("/")) {
+ return string(b)
+ }
+
+ // dunno what this is
+ if len(b) != 12 {
+ Logger.Printf("Unexpected eidr value received: %s", b)
+ return ""
+ }
+
+ i := int(binary.BigEndian.Uint16(b[:2]))
+ return fmt.Sprintf("10.%d/%X-%X-%X-%X-%X", i, b[2:4], b[4:6], b[6:8], b[8:10], b[10:12])
+}
diff --git a/internal/scte35/segmentation_upid_test.go b/internal/scte35/segmentation_upid_test.go
@@ -0,0 +1,29 @@
+package scte35_test
+
+import (
+ "testing"
+
+ "github.com/untangledco/streaming/internal/scte35"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSegmentationUPID_ASCIIValue(t *testing.T) {
+ cases := map[string]struct {
+ upid scte35.SegmentationUPID
+ expected string
+ }{
+ "Simple": {
+ upid: scte35.SegmentationUPID{
+ Type: 0x09,
+ Value: "SIGNAL:z1sFOMjCnV4AAAAAAAABAQ==",
+ },
+ expected: "SIGNAL:z1sFOMjCnV4AAAAAAAABAQ==",
+ },
+ }
+
+ for k, c := range cases {
+ t.Run(k, func(t *testing.T) {
+ require.Equal(t, c.expected, c.upid.ASCIIValue())
+ })
+ }
+}
diff --git a/internal/scte35/splice_command.go b/internal/scte35/splice_command.go
@@ -0,0 +1,54 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+// NewSpliceCommand returns the splice command appropriate for the given type.
+func NewSpliceCommand(spliceCommandType uint32) SpliceCommand {
+ switch spliceCommandType {
+ case SpliceNullType:
+ return &SpliceNull{}
+ case SpliceScheduleType:
+ return &SpliceSchedule{}
+ case SpliceInsertType:
+ return &SpliceInsert{}
+ case TimeSignalType:
+ return &TimeSignal{}
+ case BandwidthReservationType:
+ return &BandwidthReservation{}
+ default:
+ return &PrivateCommand{}
+ }
+}
+
+// SpliceCommand is an interface for splice_command.
+type SpliceCommand interface {
+ Type() uint32
+ decode(b []byte) error
+ encode() ([]byte, error)
+ length() int
+ writeTo(t *table)
+}
+
+// decodeSpliceCommand decodes the supplied byte array into the desired
+// splice_command_type.
+func decodeSpliceCommand(spliceCommandType uint32, b []byte) (SpliceCommand, error) {
+ cmd := NewSpliceCommand(spliceCommandType)
+ if err := cmd.decode(b); err != nil {
+ return cmd, err
+ }
+ return cmd, nil
+}
diff --git a/internal/scte35/splice_descriptor.go b/internal/scte35/splice_descriptor.go
@@ -0,0 +1,179 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "encoding/json"
+ "encoding/xml"
+ "fmt"
+
+ "github.com/bamiaux/iobit"
+)
+
+const (
+ // CUEIdentifier is 32-bit number used to identify the owner of the
+ // descriptor. The identifier shall have a value of 0x43554549 (ASCII “CUEI”).
+ CUEIdentifier = 0x43554549
+ // CUEIASCII is the CUEIIdentifier ASCII value
+ CUEIASCII = "CUEI"
+)
+
+// NewSpliceDescriptor returns the appropriate splice_descriptor for the given
+// identifier and tag
+func NewSpliceDescriptor(identifier uint32, tag uint32) SpliceDescriptor {
+ if identifier == CUEIdentifier {
+ switch tag {
+ case AvailDescriptorTag:
+ return &AvailDescriptor{}
+ case DTMFDescriptorTag:
+ return &DTMFDescriptor{}
+ case SegmentationDescriptorTag:
+ return &SegmentationDescriptor{}
+ case TimeDescriptorTag:
+ return &TimeDescriptor{}
+ case AudioDescriptorTag:
+ return &AudioDescriptor{}
+ }
+ }
+ // as a last resort, fall back to private_descriptor. This is not strictly
+ // compliant but allows us to deal with a wider array of quirky signals.
+ return &PrivateDescriptor{Identifier: identifier}
+
+}
+
+// SpliceDescriptor is a prototype for adding new fields to the
+// splice_info_section. All descriptors included use the same syntax for the
+// first six bytes. In order to allow private information to be added we have
+// included the ‘identifier’ code. This removes the need for a registration
+// descriptor in the descriptor loop.
+//
+// Any receiving equipment should skip any descriptors with unknown identifiers
+// or unknown descriptor tags. For descriptors with known identifiers, the
+// receiving equipment should skip descriptors with an unknown
+// splice_descriptor_tag.
+type SpliceDescriptor interface {
+ Tag() uint32
+ decode(b []byte) error
+ encode() ([]byte, error)
+ length() int // named to differentiate from splice_command
+ writeTo(t *table)
+}
+
+// SpliceDescriptors is a slice of SpliceDescriptor.
+type SpliceDescriptors []SpliceDescriptor
+
+// UnmarshalJSON decodes a JSON array into a slice of SpliceDescriptors.
+func (sds *SpliceDescriptors) UnmarshalJSON(data []byte) error {
+ // split the array into individual JSON objects
+ var items []json.RawMessage
+ if err := json.Unmarshal(data, &items); err != nil {
+ return err
+ }
+
+ // slice to hold decoded splice descriptors
+ results := make([]SpliceDescriptor, len(items))
+
+ // struct to extract splice descriptor type data
+ type sdtype struct {
+ Identifier uint32 `json:"identifier"`
+ Type uint32 `json:"type"`
+ }
+
+ // decode each item
+ for i := range items {
+ sdt := sdtype{Identifier: CUEIdentifier}
+ if err := json.Unmarshal(items[i], &sdt); err != nil {
+ return err
+ }
+
+ sd := NewSpliceDescriptor(sdt.Identifier, sdt.Type)
+ if err := json.Unmarshal(items[i], &sd); err != nil {
+ return err
+ }
+
+ results[i] = sd
+ }
+
+ // replace the slice
+ *sds = results
+ return nil
+}
+
+// UnmarshalXML decodes an XML payload into a slice of SpliceDescriptors.
+//
+// Unlike UnmarshalJSON, this function is executed once per SpliceDescriptor
+// rather than once per slice.
+func (sds *SpliceDescriptors) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ var tmp SpliceDescriptors
+
+ // determine the type
+ var sd SpliceDescriptor
+ switch start.Name.Local {
+ case "AudioDescriptor":
+ sd = &AudioDescriptor{}
+ case "AvailDescriptor":
+ sd = &AvailDescriptor{}
+ case "DTMFDescriptor":
+ sd = &DTMFDescriptor{}
+ case "PrivateDescriptor":
+ sd = &PrivateDescriptor{}
+ case "SegmentationDescriptor":
+ sd = &SegmentationDescriptor{}
+ case "TimeDescriptor":
+ sd = &TimeDescriptor{}
+ default:
+ return fmt.Errorf("unsupported splice_descriptor tag: %s", start.Name.Local)
+ }
+
+ // decode it
+ if err := d.DecodeElement(&sd, &start); err != nil {
+ return err
+ }
+
+ // add it to the slice
+ tmp = append(*sds, sd)
+ *sds = tmp
+
+ return nil
+}
+
+// decodeSpliceDescriptors returns a slice of SpliceDescriptors from decoding
+// the supplied byte array.
+func decodeSpliceDescriptors(b []byte) ([]SpliceDescriptor, error) {
+ r := iobit.NewReader(b)
+
+ var sds []SpliceDescriptor
+ for r.LeftBits() > 0 {
+ // Peek to get splice_descriptor_tag, descriptor_length, and
+ // identifier
+ sdr := r.Peek()
+ spliceDescriptorTag := sdr.Uint32(8)
+ descriptorLength := int(sdr.Uint32(8))
+ identifier := sdr.Uint32(32)
+
+ // Decode the full splice_descriptor (including splice_descriptor_tag
+ // and descriptor_length).
+ sd := NewSpliceDescriptor(identifier, spliceDescriptorTag)
+ err := sd.decode(r.Bytes(descriptorLength + 2))
+ if err != nil {
+ return sds, err
+ }
+ sds = append(sds, sd)
+ }
+
+ return sds, nil
+}
diff --git a/internal/scte35/splice_info_section.go b/internal/scte35/splice_info_section.go
@@ -0,0 +1,465 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "encoding/base64"
+ "encoding/hex"
+ "encoding/json"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/bamiaux/iobit"
+)
+
+const (
+ // TableID is an 8-bit field that shall be 0xFC.
+ TableID = 0xFC
+ // SectionSyntaxIndicator is a 1-bit field that should always be set to '0'.
+ SectionSyntaxIndicator = false
+ // PrivateIndicator is a 1-bit flag that shall be set to 0.
+ PrivateIndicator = false
+
+ // SAPType1 indicates closed GOP with no leading pictures.
+ SAPType1 = uint32(0x0)
+ // SAPType2 indicates closed GOP with leading pictures.
+ SAPType2 = uint32(0x1)
+ // SAPType3 indicates Open GOP.
+ SAPType3 = uint32(0x2)
+ // SAPTypeNotSpecified indicates the type of SAP, if any, is not signaled.
+ SAPTypeNotSpecified = uint32(0x3)
+)
+
+// SpliceInfoSection shall be carried in transport packets whereby only one
+// section or partial section may be in any transport packet.
+// Splice_info_sections shall always start at the beginning of a transport
+// packet payload. When a section begins in a transport packet and this is the
+// first packet of the splice_info_section, the pointer_field shall be present
+// and equal to 0x00 and the payload_unit_start_indicator bit shall be equal to
+// one (per the requirements of section syntax usage per [MPEG Systems]).
+type SpliceInfoSection struct {
+ XMLName xml.Name `xml:"http://www.scte.org/schemas/35 SpliceInfoSection"`
+ EncryptedPacket EncryptedPacket `xml:"http://www.scte.org/schemas/35 EncryptedPacket,omitempty"`
+ SpliceCommand SpliceCommand `xml:""`
+ SpliceDescriptors SpliceDescriptors `xml:""`
+ SAPType uint32 `xml:"sapType,attr"`
+ PreRollMilliSeconds uint32 `xml:"preRollMilliSeconds,attr,omitempty"` // no corresponding binary field
+ PTSAdjustment uint64 `xml:"ptsAdjustment,attr,omitempty"`
+ ProtocolVersion uint32 `xml:"protocolVersion,attr,omitempty"`
+ Tier uint32 `xml:"tier,attr"`
+ alignmentStuffing []byte // alignment_stuffing
+ ecrc32 []byte // decoded e_crc_32
+ crc32 []byte // decoded crc_32
+}
+
+// Base64 returns the SpliceInfoSection as a base64 encoded string.
+func (sis *SpliceInfoSection) Base64() string {
+ b, err := sis.Encode()
+ if err != nil {
+ Logger.Printf("Error encoding splice_info_section: %s\n", err)
+ return ""
+ }
+ return base64.StdEncoding.EncodeToString(b)
+}
+
+// Decode the contents of a byte array into this SpliceInfoSection.
+func (sis *SpliceInfoSection) Decode(b []byte) (err error) {
+ r := iobit.NewReader(b)
+ r.Skip(8) // table_id (shall be 0xFC)
+ r.Skip(1) // section_syntax_indicator (shall be 0)
+ r.Skip(1) // private_indicator (shall be 0)
+ sis.SAPType = r.Uint32(2)
+ r.Skip(12) // section_length (informative, can ignore)
+
+ sis.ProtocolVersion = r.Uint32(8)
+ encryptedPacket := r.Bit()
+ sis.EncryptedPacket.EncryptionAlgorithm = r.Uint32(6)
+ sis.PTSAdjustment = r.Uint64(33)
+ sis.EncryptedPacket.CWIndex = r.Uint32(8)
+ sis.Tier = r.Uint32(12)
+
+ spliceCommandLength := int(r.Uint32(12)) // in bytes
+ spliceCommandType := r.Uint32(8)
+ switch spliceCommandLength {
+ case 0xFFF:
+ // legacy signal, decode and skip (buffer underflow expected here)
+ r2 := r.Peek()
+ sis.SpliceCommand, err = decodeSpliceCommand(spliceCommandType, r2.LeftBytes())
+ if err != nil && !errors.Is(err, ErrBufferUnderflow) {
+ return err
+ }
+ r.Skip(uint(sis.SpliceCommand.length() * 8))
+ default:
+ // standard signal, decode as usual
+ sis.SpliceCommand, err = decodeSpliceCommand(spliceCommandType, r.Bytes(spliceCommandLength))
+ if err != nil {
+ return err
+ }
+ }
+
+ descriptorLoopLength := int(r.Uint32(16)) // bytes
+ sis.SpliceDescriptors, err = decodeSpliceDescriptors(r.Bytes(descriptorLoopLength))
+ if err != nil {
+ return err
+ }
+
+ if encryptedPacket {
+ stuffedBytes := (int(r.LeftBits()) - 64) / 8
+ if stuffedBytes > 0 {
+ sis.alignmentStuffing = r.Bytes(stuffedBytes)
+ }
+ sis.ecrc32 = r.Bytes(4)
+ } else {
+ stuffedBytes := (int(r.LeftBits()) - 32) / 8
+ if stuffedBytes > 0 {
+ sis.alignmentStuffing = r.Bytes(stuffedBytes)
+ }
+ }
+ sis.crc32 = r.Bytes(4)
+
+ if err := readerError(r); err != nil {
+ return fmt.Errorf("splice_info_section: %w", err)
+ }
+
+ if err = verifyCRC32(b); err != nil {
+ return fmt.Errorf("splice_info_section: %w", err)
+ }
+
+ return nil
+}
+
+// Duration attempts to return the duration of the signal.
+func (sis *SpliceInfoSection) Duration() time.Duration {
+ // if this is a splice insert with a duration, use it
+ if sc, ok := sis.SpliceCommand.(*SpliceInsert); ok {
+ if sc.BreakDuration != nil {
+ return TicksToDuration(sc.BreakDuration.Duration)
+ }
+ }
+
+ ticks := uint64(0)
+ for _, sd := range sis.SpliceDescriptors {
+ if sdt, ok := sd.(*SegmentationDescriptor); ok {
+ if sdt.SegmentationDuration != nil {
+ ticks += *sdt.SegmentationDuration
+ }
+ }
+ }
+ return TicksToDuration(ticks)
+}
+
+// Encode returns the binary representation of this SpliceInfoSection as a
+// byte array.
+func (sis *SpliceInfoSection) Encode() ([]byte, error) {
+ buf := make([]byte, sis.length())
+
+ iow := iobit.NewWriter(buf)
+ iow.PutUint32(8, TableID)
+ iow.PutBit(SectionSyntaxIndicator)
+ iow.PutBit(PrivateIndicator)
+ iow.PutUint32(2, sis.SAPType)
+ iow.PutUint32(12, uint32(sis.sectionLength()))
+ iow.PutUint32(8, sis.ProtocolVersion)
+
+ iow.PutBit(sis.EncryptedPacketFlag())
+ iow.PutUint32(6, sis.EncryptedPacket.EncryptionAlgorithm)
+ iow.PutUint64(33, sis.PTSAdjustment)
+ iow.PutUint32(8, sis.EncryptedPacket.CWIndex)
+ iow.PutUint32(12, sis.Tier)
+
+ if sis.SpliceCommand != nil {
+ iow.PutUint32(12, uint32(sis.SpliceCommand.length()))
+ iow.PutUint32(8, sis.SpliceCommand.Type())
+ sc, err := sis.SpliceCommand.encode()
+ if err != nil {
+ return buf, err
+ }
+ if _, err = iow.Write(sc); err != nil {
+ return buf, err
+ }
+ }
+
+ iow.PutUint32(16, uint32(sis.descriptorLoopLength()))
+ for _, sd := range sis.SpliceDescriptors {
+ sde, err := sd.encode()
+ if err != nil {
+ return buf, err
+ }
+ if _, err = iow.Write(sde); err != nil {
+ return buf, err
+ }
+ }
+
+ // alignment_stuffing
+ _, _ = iow.Write(sis.alignmentStuffing)
+
+ // Encoding encrypted signals is untested.
+ if sis.EncryptedPacket.EncryptionAlgorithm != EncryptionAlgorithmNone {
+ iow.PutUint32(32, calculateCRC32(buf[:iow.Index()/8])) // E_CRC_32
+ }
+
+ // Re-calculate CRC_32 to ensure correctness
+ iow.PutUint32(32, calculateCRC32(buf[:iow.Index()/8])) // CRC_32
+
+ err := iow.Flush()
+ return buf, err
+}
+
+// EncryptedPacketFlag returns the value of encrypted_packet_flag
+func (sis *SpliceInfoSection) EncryptedPacketFlag() bool {
+ return sis.EncryptedPacket.EncryptionAlgorithm != EncryptionAlgorithmNone
+}
+
+// Hex returns the SpliceInfoSection as a hexadecimal encoded string.
+func (sis *SpliceInfoSection) Hex() string {
+ b, err := sis.Encode()
+ if err != nil {
+ Logger.Printf("Error encoding splice_info_section: %s\n", err)
+ return ""
+ }
+ return hex.EncodeToString(b)
+}
+
+// SAPTypeName returns the Stream Access Point type name.
+func (sis *SpliceInfoSection) SAPTypeName() string {
+ switch sis.SAPType {
+ case SAPType1:
+ return "Type 1"
+ case SAPType2:
+ return "Type 2"
+ case SAPType3:
+ return "Type 3"
+ default:
+ return "Not Specified"
+ }
+}
+
+// Table returns the tabular description of this SpliceInfoSection as described
+// in ANSI/SCTE 35 Table 5.
+func (sis *SpliceInfoSection) Table(prefix, indent string) string {
+ // top level table is not indented
+ t := newTable(prefix, indent)
+
+ t.row(0, "splice_info_section() {", nil)
+ t.row(1, "table_id", fmt.Sprintf("%#02x", TableID))
+ t.row(1, "section_syntax_indicator", SectionSyntaxIndicator)
+ t.row(1, "private_indicator", PrivateIndicator)
+ t.row(1, "sap_type", fmt.Sprintf("%d (%s)", sis.SAPType, sis.SAPTypeName()))
+ t.row(1, "section_length", sis.sectionLength())
+ t.row(0, "}", nil)
+ t.row(0, "protocol_version", sis.ProtocolVersion)
+ t.row(0, "encryption_algorithm", fmt.Sprintf("%d (%s)", sis.EncryptedPacket.EncryptionAlgorithm, sis.EncryptedPacket.encryptionAlgorithmName()))
+ t.row(0, "pts_adjustment", sis.PTSAdjustment)
+ t.row(0, "cw_index", sis.EncryptedPacket.CWIndex)
+ t.row(0, "tier", sis.Tier)
+
+ if sis.SpliceCommand != nil {
+ t.row(0, "splice_command_length", sis.SpliceCommand.length())
+ t.row(0, "splice_command_type", fmt.Sprintf("%#02x", sis.SpliceCommand.Type()))
+ sis.SpliceCommand.writeTo(t)
+ }
+
+ t.row(0, "descriptor_loop_length", sis.descriptorLoopLength())
+ for _, sd := range sis.SpliceDescriptors {
+ sd.writeTo(t)
+ }
+ return t.String()
+}
+
+// MarshalJSON encodes a SpliceInfoSection to JSON.
+func (sis *SpliceInfoSection) MarshalJSON() ([]byte, error) {
+ // ensure JSONTypes are all set before marshalling. These are included in
+ // each SpliceCommand.Type() and SpliceDescriptor.Tag() implementation.
+ sis.SpliceCommand.Type()
+ for i := range sis.SpliceDescriptors {
+ sis.SpliceDescriptors[i].Tag()
+ }
+
+ m := map[string]interface{}{
+ "encryptedPacket": sis.EncryptedPacket,
+ "spliceCommand": sis.SpliceCommand,
+ "sapType": sis.SAPType,
+ "tier": sis.Tier,
+ }
+ if sis.ProtocolVersion > 0 {
+ m["protocolVersion"] = sis.ProtocolVersion
+ }
+ if sis.PTSAdjustment > 0 {
+ m["ptsAdjustment"] = sis.PTSAdjustment
+ }
+ if len(sis.SpliceDescriptors) > 0 {
+ m["spliceDescriptors"] = sis.SpliceDescriptors
+ }
+ return json.Marshal(m)
+}
+
+// UnmarshalJSON decodes a SpliceInfoSection from JSON.
+func (sis *SpliceInfoSection) UnmarshalJSON(b []byte) error {
+ var tmp iSIS
+ if err := json.Unmarshal(b, &tmp); err != nil {
+ return err
+ }
+
+ sis.EncryptedPacket = tmp.EncryptedPacket
+ sis.SpliceCommand = tmp.SpliceCommand()
+ sis.SpliceDescriptors = tmp.SpliceDescriptors
+ if tmp.SAPType != nil {
+ sis.SAPType = *tmp.SAPType
+ } else {
+ sis.SAPType = SAPTypeNotSpecified
+ }
+ sis.PTSAdjustment = tmp.PTSAdjustment
+ sis.ProtocolVersion = tmp.ProtocolVersion
+ sis.Tier = tmp.Tier
+ return nil
+}
+
+// UnmarshalXML decodes a SpliceInfoSection from XML.
+func (sis *SpliceInfoSection) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ var tmp iSIS
+ if err := d.DecodeElement(&tmp, &start); err != nil {
+ return err
+ }
+ sis.EncryptedPacket = tmp.EncryptedPacket
+ sis.SpliceCommand = tmp.SpliceCommand()
+ sis.SpliceDescriptors = tmp.SpliceDescriptors
+ if tmp.SAPType != nil {
+ sis.SAPType = *tmp.SAPType
+ } else {
+ sis.SAPType = SAPTypeNotSpecified
+ }
+ sis.PTSAdjustment = tmp.PTSAdjustment
+ sis.ProtocolVersion = tmp.ProtocolVersion
+ sis.Tier = tmp.Tier
+ return nil
+}
+
+// length returns the expected length of the encoded splice_info_section, in
+// bytes.
+func (sis *SpliceInfoSection) length() int {
+ length := 8 // table_id
+ length++ // section_syntax_indicator
+ length++ // private_indicator
+ length += 2 // reserved
+ length += 12 // section_length (bytes remaining)
+ length += sis.sectionLength() * 8 // everything else (bytes -> bits)
+ return length / 8
+}
+
+// sectionLength returns the section_length, in bytes
+func (sis *SpliceInfoSection) sectionLength() int {
+ length := 8 // protocol_version
+ length++ // encrypted_packet
+ length += 6 // encryption_algorithm
+ length += 33 // pts_adjustment
+ length += 8 // cw_index
+ length += 12 // tier
+ length += 12 // splice_command_length
+ length += 8 // splice_command_type
+ if sis.SpliceCommand != nil {
+ length += sis.SpliceCommand.length() * 8 // bytes -> bits
+ }
+ length += 16 // descriptor_loop_length (bytes remaining value)
+ length += sis.descriptorLoopLength() * 8 // bytes -> bits
+ length += len(sis.alignmentStuffing) * 8 // bytes -> bits
+ // we don't officially support encrypted signals so this section is untested.
+ // It's implemented here as a base-line if/when we decide to support
+ // encryption (ie, use at your own risk)
+ if sis.EncryptedPacket.EncryptionAlgorithm != EncryptionAlgorithmNone {
+ length += 32 // ECRC_32
+ }
+
+ length += 32 // CRC_32
+ return length / 8
+}
+
+// descriptorLoopLength return the descriptor_loop_length
+func (sis *SpliceInfoSection) descriptorLoopLength() int {
+ length := 0
+ for _, d := range sis.SpliceDescriptors {
+ length += 8 // splice_descriptor_tag
+ length += 8 // descriptor_length
+ length += d.length() * 8 // splice_descriptor()
+ }
+ return length / 8
+}
+
+// iSIS is an internal SpliceInfoSection used to support (un)marshalling
+// polymorphic fields.
+type iSIS struct {
+ EncryptedPacket EncryptedPacket `xml:"http://www.scte.org/schemas/35 EncryptedPacket,omitempty" json:"encryptedPacket,omitempty"`
+ SpliceCommandRaw json.RawMessage `xml:"-" json:"spliceCommand"`
+ SpliceNull *SpliceNull `xml:"http://www.scte.org/schemas/35 SpliceNull" json:"-"`
+ SpliceSchedule *SpliceSchedule `xml:"http://www.scte.org/schemas/35 SpliceSchedule" json:"-"`
+ SpliceInsert *SpliceInsert `xml:"http://www.scte.org/schemas/35 SpliceInsert" json:"-"`
+ TimeSignal *TimeSignal `xml:"http://www.scte.org/schemas/35 TimeSignal" json:"-"`
+ BandwidthReservation *BandwidthReservation `xml:"http://www.scte.org/schemas/35 BandwidthReservation" json:"-"`
+ PrivateCommand *PrivateCommand `xml:"http://www.scte.org/schemas/35 PrivateCommand" json:"-"`
+ SpliceDescriptors SpliceDescriptors `xml:",any" json:"spliceDescriptors"`
+ SAPType *uint32 `xml:"sapType,attr" json:"sapType,omitempty"`
+ PTSAdjustment uint64 `xml:"ptsAdjustment,attr" json:"ptsAdjustment"`
+ ProtocolVersion uint32 `xml:"protocolVersion,attr" json:"protocolVersion"`
+ Tier uint32 `xml:"tier,attr" json:"tier"`
+}
+
+// SpliceCommand returns the polymorphic splice_command.
+func (i *iSIS) SpliceCommand() SpliceCommand {
+ // xml unmarshalls to the corresponding struct
+ if i.SpliceNull != nil {
+ return i.SpliceNull
+ }
+ if i.SpliceSchedule != nil {
+ return i.SpliceSchedule
+ }
+ if i.SpliceInsert != nil {
+ return i.SpliceInsert
+ }
+ if i.TimeSignal != nil {
+ return i.TimeSignal
+ }
+ if i.BandwidthReservation != nil {
+ return i.BandwidthReservation
+ }
+
+ // no valid splice_command?
+ if i.SpliceCommandRaw == nil {
+ return nil
+ }
+
+ // struct to determine the splice command's type
+ type sctype struct {
+ Type uint32 `json:"type"`
+ }
+
+ // get the type
+ var st sctype
+ if err := json.Unmarshal(i.SpliceCommandRaw, &st); err != nil {
+ Logger.Printf("error unmarshalling splice command type: %s", err)
+ return nil
+ }
+
+ // and decode it
+ sc := NewSpliceCommand(st.Type)
+ if err := json.Unmarshal(i.SpliceCommandRaw, sc); err != nil {
+ Logger.Printf("error unmarshalling splice command: %s", err)
+ return nil
+ }
+
+ return sc
+}
diff --git a/internal/scte35/splice_info_section_test.go b/internal/scte35/splice_info_section_test.go
@@ -0,0 +1,252 @@
+package scte35
+
+import (
+ "encoding/json"
+ "encoding/xml"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestSpliceInfoSection_UnmarshalXML(t *testing.T) {
+ cases := map[string]struct {
+ xml string
+ expected *SpliceInfoSection
+ }{
+ "SAPType Missing": {
+ xml: `
+ <SpliceInfoSection xmlns="http://www.scte.org/schemas/35" tier="4095">
+ <EncryptedPacket xmlns="http://www.scte.org/schemas/35" cwIndex="255"></EncryptedPacket>
+ <TimeSignal xmlns="http://www.scte.org/schemas/35">
+ <SpliceTime xmlns="http://www.scte.org/schemas/35" ptsTime="1924989008"></SpliceTime>
+ </TimeSignal>
+ <SegmentationDescriptor xmlns="http://www.scte.org/schemas/35" segmentationEventId="1207959694" segmentationDuration="27630000" segmentationTypeId="52" segmentNum="2">
+ <DeliveryRestrictions xmlns="http://www.scte.org/schemas/35" archiveAllowedFlag="true" webDeliveryAllowedFlag="false" noRegionalBlackoutFlag="true" deviceRestrictions="3"></DeliveryRestrictions>
+ <SegmentationUpid xmlns="http://www.scte.org/schemas/35" segmentationUpidType="8">748724618</SegmentationUpid>
+ </SegmentationDescriptor>
+ </SpliceInfoSection>`,
+ expected: &SpliceInfoSection{
+ Tier: uint32(4095),
+ SAPType: SAPTypeNotSpecified,
+ EncryptedPacket: EncryptedPacket{CWIndex: 255},
+ SpliceCommand: &TimeSignal{
+ SpliceTime: SpliceTime{PTSTime: uint64ptr(1924989008)},
+ },
+ SpliceDescriptors: SpliceDescriptors{
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ ArchiveAllowedFlag: true,
+ WebDeliveryAllowedFlag: false,
+ NoRegionalBlackoutFlag: true,
+ DeviceRestrictions: 3,
+ },
+ SegmentationUPIDs: []SegmentationUPID{
+ {
+ Type: SegmentationUPIDTypeTI,
+ Value: "748724618",
+ },
+ },
+ SegmentationEventID: uint32(1207959694),
+ SegmentationDuration: uint64ptr(27630000),
+ SegmentationTypeID: SegmentationTypeProviderPOStart,
+ SegmentNum: 2,
+ },
+ },
+ },
+ },
+ "SAPType Specified": {
+ xml: `
+ <SpliceInfoSection xmlns="http://www.scte.org/schemas/35" tier="4095" sapType="0">
+ <EncryptedPacket xmlns="http://www.scte.org/schemas/35" cwIndex="255"></EncryptedPacket>
+ <TimeSignal xmlns="http://www.scte.org/schemas/35">
+ <SpliceTime xmlns="http://www.scte.org/schemas/35" ptsTime="1924989008"></SpliceTime>
+ </TimeSignal>
+ <SegmentationDescriptor xmlns="http://www.scte.org/schemas/35" segmentationEventId="1207959694" segmentationDuration="27630000" segmentationTypeId="52" segmentNum="2">
+ <DeliveryRestrictions xmlns="http://www.scte.org/schemas/35" archiveAllowedFlag="true" webDeliveryAllowedFlag="false" noRegionalBlackoutFlag="true" deviceRestrictions="3"></DeliveryRestrictions>
+ <SegmentationUpid xmlns="http://www.scte.org/schemas/35" segmentationUpidType="8">748724618</SegmentationUpid>
+ </SegmentationDescriptor>
+ </SpliceInfoSection>`,
+ expected: &SpliceInfoSection{
+ Tier: uint32(4095),
+ SAPType: SAPType1,
+ EncryptedPacket: EncryptedPacket{CWIndex: 255},
+ SpliceCommand: &TimeSignal{
+ SpliceTime: SpliceTime{PTSTime: uint64ptr(1924989008)},
+ },
+ SpliceDescriptors: SpliceDescriptors{
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ ArchiveAllowedFlag: true,
+ WebDeliveryAllowedFlag: false,
+ NoRegionalBlackoutFlag: true,
+ DeviceRestrictions: 3,
+ },
+ SegmentationUPIDs: []SegmentationUPID{
+ {
+ Type: SegmentationUPIDTypeTI,
+ Value: "748724618",
+ },
+ },
+ SegmentationEventID: uint32(1207959694),
+ SegmentationDuration: uint64ptr(27630000),
+ SegmentationTypeID: SegmentationTypeProviderPOStart,
+ SegmentNum: 2,
+ },
+ },
+ },
+ },
+ }
+
+ for k, c := range cases {
+ t.Run(k, func(t *testing.T) {
+ var sis SpliceInfoSection
+ require.NoError(t, xml.Unmarshal([]byte(c.xml), &sis))
+ require.Equal(t, toXML(c.expected), toXML(&sis))
+ })
+ }
+}
+
+func TestSpliceInfoSection_UnmarshalJSON(t *testing.T) {
+ cases := map[string]struct {
+ json string
+ expected *SpliceInfoSection
+ }{
+ "SAPType Missing": {
+ json: `{
+ "encryptedPacket": {
+ "cwIndex": 255
+ },
+ "spliceCommand": {
+ "type": 6,
+ "spliceTime": {
+ "ptsTime": 1924989008
+ }
+ },
+ "spliceDescriptors": [
+ {
+ "type": 2,
+ "deliveryRestrictions": {
+ "archiveAllowedFlag": true,
+ "webDeliveryAllowedFlag": false,
+ "noRegionalBlackoutFlag": true,
+ "deviceRestrictions": 3
+ },
+ "segmentationUpids": [
+ {
+ "segmentationUpidType": 8,
+ "format": "text",
+ "value": "748724618"
+ }
+ ],
+ "segmentationEventId": 1207959694,
+ "segmentationDuration": 27630000,
+ "segmentationTypeId": 52,
+ "segmentNum": 2
+ }
+ ],
+ "tier": 4095
+ }`,
+ expected: &SpliceInfoSection{
+ Tier: uint32(4095),
+ SAPType: SAPTypeNotSpecified,
+ EncryptedPacket: EncryptedPacket{CWIndex: 255},
+ SpliceCommand: &TimeSignal{
+ SpliceTime: SpliceTime{PTSTime: uint64ptr(1924989008)},
+ },
+ SpliceDescriptors: SpliceDescriptors{
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ ArchiveAllowedFlag: true,
+ WebDeliveryAllowedFlag: false,
+ NoRegionalBlackoutFlag: true,
+ DeviceRestrictions: 3,
+ },
+ SegmentationUPIDs: []SegmentationUPID{
+ {
+ Type: SegmentationUPIDTypeTI,
+ Value: "748724618",
+ },
+ },
+ SegmentationEventID: uint32(1207959694),
+ SegmentationDuration: uint64ptr(27630000),
+ SegmentationTypeID: SegmentationTypeProviderPOStart,
+ SegmentNum: 2,
+ },
+ },
+ },
+ },
+ "SAPType Specified": {
+ json: `{
+ "encryptedPacket": {
+ "cwIndex": 255
+ },
+ "sapType": 0,
+ "spliceCommand": {
+ "type": 6,
+ "spliceTime": {
+ "ptsTime": 1924989008
+ }
+ },
+ "spliceDescriptors": [
+ {
+ "type": 2,
+ "deliveryRestrictions": {
+ "archiveAllowedFlag": true,
+ "webDeliveryAllowedFlag": false,
+ "noRegionalBlackoutFlag": true,
+ "deviceRestrictions": 3
+ },
+ "segmentationUpids": [
+ {
+ "segmentationUpidType": 8,
+ "format": "text",
+ "value": "748724618"
+ }
+ ],
+ "segmentationEventId": 1207959694,
+ "segmentationDuration": 27630000,
+ "segmentationTypeId": 52,
+ "segmentNum": 2
+ }
+ ],
+ "tier": 4095
+ }`,
+ expected: &SpliceInfoSection{
+ Tier: uint32(4095),
+ SAPType: SAPType1,
+ EncryptedPacket: EncryptedPacket{CWIndex: 255},
+ SpliceCommand: &TimeSignal{
+ SpliceTime: SpliceTime{PTSTime: uint64ptr(1924989008)},
+ },
+ SpliceDescriptors: SpliceDescriptors{
+ &SegmentationDescriptor{
+ DeliveryRestrictions: &DeliveryRestrictions{
+ ArchiveAllowedFlag: true,
+ WebDeliveryAllowedFlag: false,
+ NoRegionalBlackoutFlag: true,
+ DeviceRestrictions: 3,
+ },
+ SegmentationUPIDs: []SegmentationUPID{
+ {
+ Type: SegmentationUPIDTypeTI,
+ Value: "748724618",
+ },
+ },
+ SegmentationEventID: uint32(1207959694),
+ SegmentationDuration: uint64ptr(27630000),
+ SegmentationTypeID: SegmentationTypeProviderPOStart,
+ SegmentNum: 2,
+ },
+ },
+ },
+ },
+ }
+
+ for k, c := range cases {
+ t.Run(k, func(t *testing.T) {
+ var sis SpliceInfoSection
+ require.NoError(t, json.Unmarshal([]byte(c.json), &sis))
+ require.Equal(t, toXML(c.expected), toXML(&sis))
+ })
+ }
+}
diff --git a/internal/scte35/splice_insert.go b/internal/scte35/splice_insert.go
@@ -0,0 +1,321 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "encoding/xml"
+ "fmt"
+ "strconv"
+
+ "github.com/bamiaux/iobit"
+)
+
+const (
+ // SpliceInsertType is the splice_command_type for splice_insert()
+ SpliceInsertType = 0x05
+)
+
+// SpliceInsert is a command shall be sent at least once for every splice
+// event.
+type SpliceInsert struct {
+ XMLName xml.Name `xml:"http://www.scte.org/schemas/35 SpliceInsert" json:"-"`
+ JSONType uint32 `xml:"-" json:"type"`
+ Program *SpliceInsertProgram `xml:"http://www.scte.org/schemas/35 Program" json:"program,omitempty"`
+ Components []SpliceInsertComponent `xml:"http://www.scte.org/schemas/35 Component" json:"components,omitempty"`
+ BreakDuration *BreakDuration `xml:"http://www.scte.org/schemas/35 BreakDuration" json:"breakDuration,omitempty"`
+ SpliceEventID uint32 `xml:"spliceEventId,attr" json:"spliceEventId,omitempty"`
+ SpliceEventCancelIndicator bool `xml:"spliceEventCancelIndicator,attr" json:"spliceEventCancelIndicator"`
+ SpliceImmediateFlag bool `xml:"spliceImmediateFlag,attr" json:"spliceImmediateFlag"`
+ OutOfNetworkIndicator bool `xml:"outOfNetworkIndicator,attr" json:"outOfNetworkIndicator"`
+ UniqueProgramID uint32 `xml:"uniqueProgramId,attr" json:"uniqueProgramId,omitempty"`
+ AvailNum uint32 `xml:"availNum,attr" json:"availNum,omitempty"`
+ AvailsExpected uint32 `xml:"availsExpected,attr" json:"availsExpected,omitempty"`
+}
+
+// DurationFlag returns the duration_flag.
+func (cmd *SpliceInsert) DurationFlag() bool {
+ return cmd.BreakDuration != nil
+}
+
+// ProgramSpliceFlag returns the program_splice_flag.
+func (cmd *SpliceInsert) ProgramSpliceFlag() bool {
+ return cmd.Program != nil
+}
+
+// TimeSpecifiedFlag returns the time_specified_flag
+func (cmd *SpliceInsert) TimeSpecifiedFlag() bool {
+ return cmd != nil && cmd.Program != nil && cmd.Program.SpliceTime.TimeSpecifiedFlag()
+}
+
+// Type returns the splice_command_type.
+func (cmd *SpliceInsert) Type() uint32 {
+ cmd.JSONType = SpliceInsertType
+ return SpliceInsertType
+}
+
+// writeTo the given table.
+func (cmd *SpliceInsert) writeTo(t *table) {
+ t.row(0, "splice_insert() {", nil)
+ t.row(1, "splice_event_id", cmd.SpliceEventID)
+ t.row(1, "splice_event_cancel_indicator", cmd.SpliceEventCancelIndicator)
+ if !cmd.SpliceEventCancelIndicator {
+ t.row(1, "out_of_network_indicator", cmd.OutOfNetworkIndicator)
+ t.row(1, "program_splice_flag", cmd.ProgramSpliceFlag())
+ t.row(1, "duration_flag", cmd.DurationFlag())
+ t.row(1, "splice_immediate_flag", cmd.SpliceImmediateFlag)
+ if cmd.ProgramSpliceFlag() && !cmd.SpliceImmediateFlag {
+ t.row(1, "time_specified_flag", cmd.TimeSpecifiedFlag())
+ if cmd.TimeSpecifiedFlag() {
+ t.row(1, "pts_time", cmd.Program.SpliceTime.PTSTime)
+ }
+ }
+ if !cmd.ProgramSpliceFlag() {
+ t.row(1, "component_count", len(cmd.Components))
+ for i, c := range cmd.Components {
+ t.row(1, "component["+strconv.Itoa(i)+"]", nil)
+ t.row(2, "component_tag", c.Tag)
+ if !cmd.SpliceImmediateFlag {
+ t.row(2, "time_specified_flag", c.TimeSpecifiedFlag())
+ if c.TimeSpecifiedFlag() {
+ t.row(2, "pts_time", c.SpliceTime.PTSTime)
+ }
+ }
+ t.row(1, "}", nil)
+ }
+ }
+ if cmd.DurationFlag() {
+ t.row(1, "auto_return", cmd.BreakDuration.AutoReturn)
+ t.row(1, "duration", cmd.BreakDuration.Duration)
+ }
+ t.row(1, "unique_program_id", cmd.UniqueProgramID)
+ t.row(1, "avail_num", cmd.AvailNum)
+ t.row(1, "avails_expected", cmd.AvailsExpected)
+ }
+ t.row(0, "}", nil)
+}
+
+// decode a binary splice_insert.
+func (cmd *SpliceInsert) decode(b []byte) error {
+ r := iobit.NewReader(b)
+
+ cmd.SpliceEventID = r.Uint32(32)
+ cmd.SpliceEventCancelIndicator = r.Bit()
+ r.Skip(7) // reserved
+ if !cmd.SpliceEventCancelIndicator {
+ cmd.OutOfNetworkIndicator = r.Bit()
+ programSpliceFlag := r.Bit()
+ durationFlag := r.Bit()
+ cmd.SpliceImmediateFlag = r.Bit()
+ r.Skip(4) // reserved
+ if programSpliceFlag {
+ cmd.Program = &SpliceInsertProgram{}
+ if !cmd.SpliceImmediateFlag {
+ timeSpecifiedFlag := r.Bit()
+ if timeSpecifiedFlag {
+ r.Skip(6) // reserved
+ ptsTime := r.Uint64(33)
+ cmd.Program.SpliceTime.PTSTime = &ptsTime
+ } else {
+ r.Skip(7) // reserved
+ }
+ }
+ } else {
+ componentCount := int(r.Uint32(8))
+ cmd.Components = make([]SpliceInsertComponent, componentCount)
+ for i := 0; i < componentCount; i++ {
+ c := SpliceInsertComponent{}
+ c.Tag = r.Uint32(8)
+ if !cmd.SpliceImmediateFlag {
+ timeSpecifiedFlag := r.Bit()
+ if timeSpecifiedFlag {
+ r.Skip(6) // reserved
+ ptsTime := r.Uint64(33)
+ c.SpliceTime = &SpliceTime{
+ PTSTime: &ptsTime,
+ }
+ } else {
+ r.Skip(7) // reserved
+ }
+ }
+ cmd.Components[i] = c
+ }
+ }
+ if durationFlag {
+ cmd.BreakDuration = &BreakDuration{}
+ cmd.BreakDuration.AutoReturn = r.Bit()
+ r.Skip(6) // reserved
+ cmd.BreakDuration.Duration = r.Uint64(33)
+ }
+ }
+ cmd.UniqueProgramID = r.Uint32(16)
+ cmd.AvailNum = r.Uint32(8)
+ cmd.AvailsExpected = r.Uint32(8)
+
+ if err := readerError(r); err != nil {
+ return fmt.Errorf("splice_insert: %w", err)
+ }
+ return nil
+}
+
+// encode this splice_insert to binary.
+func (cmd *SpliceInsert) encode() ([]byte, error) {
+ buf := make([]byte, cmd.length())
+
+ iow := iobit.NewWriter(buf)
+ iow.PutUint32(32, cmd.SpliceEventID)
+ iow.PutBit(cmd.SpliceEventCancelIndicator)
+ iow.PutUint32(7, Reserved)
+ if !cmd.SpliceEventCancelIndicator {
+ iow.PutBit(cmd.OutOfNetworkIndicator)
+ iow.PutBit(cmd.ProgramSpliceFlag())
+ iow.PutBit(cmd.DurationFlag())
+ iow.PutBit(cmd.SpliceImmediateFlag)
+ iow.PutUint32(4, Reserved)
+ if cmd.ProgramSpliceFlag() && !cmd.SpliceImmediateFlag {
+ if cmd.Program.TimeSpecifiedFlag() {
+ iow.PutBit(true)
+ iow.PutUint32(6, Reserved)
+ iow.PutUint64(33, *cmd.Program.SpliceTime.PTSTime)
+ } else {
+ iow.PutBit(false)
+ iow.PutUint32(7, Reserved)
+ }
+ }
+ if !cmd.ProgramSpliceFlag() {
+ iow.PutUint32(8, uint32(len(cmd.Components)))
+ for _, c := range cmd.Components {
+ iow.PutUint32(8, c.Tag)
+ if !cmd.SpliceImmediateFlag {
+ if c.TimeSpecifiedFlag() {
+ iow.PutBit(true)
+ iow.PutUint32(6, Reserved)
+ iow.PutUint64(33, *c.SpliceTime.PTSTime)
+ } else {
+ iow.PutBit(false)
+ iow.PutUint32(7, Reserved)
+ }
+ }
+ }
+ }
+ if cmd.DurationFlag() {
+ iow.PutBit(cmd.BreakDuration.AutoReturn)
+ iow.PutUint32(6, Reserved)
+ iow.PutUint64(33, cmd.BreakDuration.Duration)
+ }
+ iow.PutUint32(16, cmd.UniqueProgramID)
+ iow.PutUint32(8, cmd.AvailNum)
+ iow.PutUint32(8, cmd.AvailsExpected)
+ }
+
+ err := iow.Flush()
+ return buf, err
+}
+
+// length returns the splice_command_length.
+func (cmd SpliceInsert) length() int {
+ length := 32 // splice_event_id
+ length++ // splice_event_cancel_indicator
+ length += 7 // reserved
+
+ // if splice_event_cancel_indicator == 0
+ if !cmd.SpliceEventCancelIndicator {
+ length++ // out_of_network_indicator
+ length++ // program_splice_flag
+ length++ // duration_flag
+ length++ // splice_immediate_flag
+ length += 4 // reserved
+
+ // if program_splice_flag == 1 && splice_immediate_flag == 0
+ if cmd.ProgramSpliceFlag() && !cmd.SpliceImmediateFlag {
+ length++ // time_specified_flag
+
+ // if time_specified_flag == 1
+ if cmd.Program.TimeSpecifiedFlag() {
+ length += 6 // reserved
+ length += 33 // pts_time
+ } else {
+ length += 7 // reserved
+ }
+ }
+
+ // if program_splice_flag == 0
+ if !cmd.ProgramSpliceFlag() {
+ length += 8 // component_count
+
+ // for i = 0 to component_count
+ for _, c := range cmd.Components {
+ length += 8 // component_tag
+
+ // if splice_immediate_flag == 0
+ if !cmd.SpliceImmediateFlag {
+ length++ // time_specified_flag
+
+ // if time_specified_flag == 1
+ if c.TimeSpecifiedFlag() {
+ length += 6 // reserved
+ length += 33 // pts_time
+ } else {
+ length += 7 // reserved
+ }
+ }
+ }
+ }
+
+ // if duration_flag == 1
+ if cmd.DurationFlag() {
+ length++ // auto_return
+ length += 6 // reserved
+ length += 33 // duration
+ }
+
+ length += 16 // unique_program_id
+ length += 8 // avail_num
+ length += 8 // avails_expected
+ }
+
+ return length / 8
+}
+
+// SpliceInsertComponent contains the Splice Point in Component Splice Mode.
+type SpliceInsertComponent struct {
+ Tag uint32 `xml:"componentTag,attr" json:"componentTag,omitempty"`
+ SpliceTime *SpliceTime `xml:"http://www.scte.org/schemas/35 SpliceTime" json:"spliceTime,omitempty"`
+}
+
+// TimeSpecifiedFlag returns the time_specified_flag.
+func (c *SpliceInsertComponent) TimeSpecifiedFlag() bool {
+ return c != nil && c.SpliceTime != nil && c.SpliceTime.PTSTime != nil
+}
+
+// NewSpliceInsertProgram returns a SpliceInsertProgram with the given ptsTime.
+func NewSpliceInsertProgram(ptsTime uint64) *SpliceInsertProgram {
+ return &SpliceInsertProgram{
+ SpliceTime: SpliceTime{
+ PTSTime: &ptsTime,
+ },
+ }
+}
+
+// SpliceInsertProgram contains the Splice Point in Program Splice Mode.
+type SpliceInsertProgram struct {
+ SpliceTime SpliceTime `xml:"http://www.scte.org/schemas/35 SpliceTime" json:"spliceTime"`
+}
+
+// TimeSpecifiedFlag returns the time_specified_flag.
+func (p *SpliceInsertProgram) TimeSpecifiedFlag() bool {
+ return p != nil && p.SpliceTime.PTSTime != nil
+}
diff --git a/internal/scte35/splice_null.go b/internal/scte35/splice_null.go
@@ -0,0 +1,68 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "encoding/xml"
+ "fmt"
+)
+
+const (
+ // SpliceNullType is the splice_command_type for splice_null()
+ SpliceNullType = 0x00
+)
+
+// SpliceNull is the command is provided for extensibility of the standard. The
+// splice_null() command allows a splice_info_table to be sent that can carry
+// descriptors without having to send one of the other defined commands. This
+// command may also be used as a “heartbeat message” for monitoring cue
+// injection equipment integrity and link integrity.
+type SpliceNull struct {
+ XMLName xml.Name `xml:"http://www.scte.org/schemas/35 SpliceNull" json:"-"`
+ JSONType uint32 `xml:"-" json:"type"`
+}
+
+// Type returns the splice_command_type.
+func (cmd *SpliceNull) Type() uint32 {
+ // ensure JSONType is set
+ cmd.JSONType = SpliceNullType
+ return SpliceNullType
+}
+
+// decode a binary splice_null.
+func (cmd *SpliceNull) decode(b []byte) error {
+ if len(b) > 0 {
+ return fmt.Errorf("splice_null: %w", ErrBufferOverflow)
+ }
+ return nil
+}
+
+// encode this splice_null to binary.
+func (cmd *SpliceNull) encode() ([]byte, error) {
+ var b []byte
+ return b, nil
+}
+
+// commandLength returns the splice_command_length.
+func (cmd *SpliceNull) length() int {
+ return 0
+}
+
+// writeTo the given table.
+func (cmd *SpliceNull) writeTo(t *table) {
+ t.row(0, "splice_null() {}", nil)
+}
diff --git a/internal/scte35/splice_schedule.go b/internal/scte35/splice_schedule.go
@@ -0,0 +1,249 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "encoding/xml"
+ "fmt"
+ "strconv"
+
+ "github.com/bamiaux/iobit"
+)
+
+const (
+ // SpliceScheduleType is the splice_command_type for the splice_schedule()
+ // command
+ SpliceScheduleType = 0x04
+)
+
+// SpliceSchedule is provided to allow a schedule of splice events to be
+// conveyed in advance.
+type SpliceSchedule struct {
+ XMLName xml.Name `xml:"http://www.scte.org/schemas/35 SpliceSchedule" json:"-"`
+ JSONType uint32 `xml:"-" json:"type"`
+ Events []Event `xml:"http://www.scte.org/schemas/35 Event" json:"events"`
+}
+
+// Type returns the splice_command_type
+func (cmd *SpliceSchedule) Type() uint32 {
+ // ensure the JSONType is set correctly.
+ cmd.JSONType = SpliceScheduleType
+ return SpliceScheduleType
+}
+
+// decode a binary splice_schedule.
+func (cmd *SpliceSchedule) decode(b []byte) error {
+ r := iobit.NewReader(b)
+
+ spliceCount := int(r.Uint32(8))
+ cmd.Events = make([]Event, spliceCount)
+ for i := 0; i < spliceCount; i++ {
+ e := Event{}
+ e.SpliceEventID = r.Uint32(32)
+ e.SpliceEventCancelIndicator = r.Bit()
+ if !e.SpliceEventCancelIndicator {
+ e.OutOfNetworkIndicator = r.Bit()
+ programSpliceFlag := r.Bit()
+ durationFlag := r.Bit()
+ r.Skip(5) // reserved
+ if programSpliceFlag {
+ e.Program = &EventProgram{}
+ e.Program.UTCSpliceTime = NewUTCSpliceTime(r.Uint32(32))
+ } else {
+ componentCount := int(r.Uint32(8))
+ e.Components = make([]EventComponent, componentCount)
+ for j := 0; j < componentCount; j++ {
+ c := EventComponent{}
+ c.Tag = r.Uint32(8)
+ c.UTCSpliceTime = NewUTCSpliceTime(r.Uint32(32))
+ e.Components[j] = c
+ }
+ }
+ if durationFlag {
+ e.BreakDuration = &BreakDuration{}
+ e.BreakDuration.AutoReturn = r.Bit()
+ r.Skip(6) // reserved
+ e.BreakDuration.Duration = r.Uint64(33)
+ }
+ }
+ e.UniqueProgramID = r.Uint32(16)
+ e.AvailNum = r.Uint32(8)
+ e.AvailsExpected = r.Uint32(8)
+ cmd.Events[i] = e
+ }
+
+ if err := readerError(r); err != nil {
+ return fmt.Errorf("splice_schedule: %w", err)
+ }
+ return nil
+}
+
+// encode this splice_schedule to binary.
+func (cmd *SpliceSchedule) encode() ([]byte, error) {
+ buf := make([]byte, cmd.length())
+ iow := iobit.NewWriter(buf)
+
+ iow.PutUint32(8, uint32(len(cmd.Events)))
+ for _, e := range cmd.Events {
+ iow.PutUint32(32, e.SpliceEventID)
+ iow.PutBit(e.SpliceEventCancelIndicator)
+ iow.PutUint32(7, Reserved) // reserved
+ if !e.SpliceEventCancelIndicator {
+ iow.PutBit(e.OutOfNetworkIndicator)
+ iow.PutBit(e.ProgramSpliceFlag())
+ iow.PutBit(e.DurationFlag())
+ iow.PutUint32(5, Reserved) // reserved
+ if e.ProgramSpliceFlag() {
+ iow.PutUint32(32, e.Program.UTCSpliceTime.GPSSeconds())
+ } else {
+ iow.PutUint32(8, uint32(len(e.Components)))
+ for _, c := range e.Components {
+ iow.PutUint32(8, c.Tag)
+ iow.PutUint32(32, c.UTCSpliceTime.GPSSeconds())
+ }
+ }
+ if e.DurationFlag() {
+ iow.PutBit(e.BreakDuration.AutoReturn)
+ iow.PutUint32(6, Reserved)
+ iow.PutUint64(33, e.BreakDuration.Duration)
+ }
+ }
+ iow.PutUint32(16, e.UniqueProgramID)
+ iow.PutUint32(8, e.AvailNum)
+ iow.PutUint32(8, e.AvailsExpected)
+ }
+
+ err := iow.Flush()
+
+ return buf, err
+}
+
+// commandLength returns the splice_command_length
+func (cmd SpliceSchedule) length() int {
+ length := 8 // splice_count
+
+ // for i = 0 to splice_count
+ for _, e := range cmd.Events {
+ length += 32 // splice_event_id
+ length++ // splice_event_cancel_indicator
+ length += 7 // reserved
+
+ // if splice_event_cancel_indicator == 0
+ if !e.SpliceEventCancelIndicator {
+ length++ // out_of_network_indicator
+ length++ // program_splice_flag
+ length++ // duration_flag
+ length += 5 // reserved
+
+ if e.ProgramSpliceFlag() {
+ // program_splice_flag == 1
+ length += 32 // utc_splice_time
+ } else {
+ // program_splice_flag == 0
+ length += 8 // component_count
+ for range e.Components {
+ length += 8 // component_tag
+ length += 32 // utc_splice_time
+ }
+ }
+
+ // if duration_flag == 1
+ if e.DurationFlag() {
+ length++ // auto_return
+ length += 6 // reserved
+ length += 33 // duration
+ }
+
+ length += 16 // unique_program_id
+ length += 8 // avail_num
+ length += 8 // avails_expected
+ }
+ }
+
+ return length / 8
+}
+
+// writeTo the given table.
+func (cmd *SpliceSchedule) writeTo(t *table) {
+ t.row(0, "splice_schedule() {", nil)
+ t.row(1, "splice_count", strconv.Itoa(len(cmd.Events)))
+ for i, e := range cmd.Events {
+ t.row(1, "event["+strconv.Itoa(i)+"]", nil)
+ t.row(2, "splice_event_id", e.SpliceEventID)
+ t.row(2, "splice_event_cancel_indicator", e.SpliceEventCancelIndicator)
+ if !e.SpliceEventCancelIndicator {
+ t.row(2, "out_of_network_indicator", e.OutOfNetworkIndicator)
+ t.row(2, "program_splice_flag", e.ProgramSpliceFlag())
+ t.row(2, "duration_flag", e.DurationFlag())
+ if e.ProgramSpliceFlag() {
+ t.row(2, "utc_splice_time", e.Program.UTCSpliceTime)
+ } else {
+ t.row(2, "component_count", len(e.Components))
+ for j, c := range e.Components {
+ t.row(2, "component["+strconv.Itoa(j)+"]", nil)
+ t.row(3, "component_tag", c.Tag)
+ t.row(3, "utc_splice_time", c.UTCSpliceTime)
+ t.row(2, "}", nil)
+ }
+ }
+ if e.DurationFlag() {
+ t.row(1, "auto_return", e.BreakDuration.AutoReturn)
+ t.row(1, "duration", e.BreakDuration.Duration)
+ }
+ t.row(1, "unique_program_id", e.UniqueProgramID)
+ t.row(1, "avail_num", e.AvailNum)
+ t.row(1, "avails_expected", e.AvailsExpected)
+ }
+ t.row(1, "}", nil)
+ }
+ t.row(0, "}", nil)
+}
+
+// Event is a single event within a splice_schedule.
+type Event struct {
+ Program *EventProgram `xml:"http://www.scte.org/schemas/35 Program" json:"program"`
+ Components []EventComponent `xml:"http://www.scte.org/schemas/35 Component" json:"components"`
+ BreakDuration *BreakDuration `xml:"http://www.scte.org/schemas/35 BreakDuration" json:"breakDuration"`
+ SpliceEventID uint32 `xml:"spliceEventId,attr" json:"spliceEventId"`
+ SpliceEventCancelIndicator bool `xml:"spliceEventCancelIndicator,attr" json:"spliceEventCancelIndicator"`
+ OutOfNetworkIndicator bool `xml:"outOfNetworkIndicator,attr" json:"outOfNetworkIndicator,omitempty"`
+ UniqueProgramID uint32 `xml:"uniqueProgramId,attr" json:"uniqueProgramId,omitempty"`
+ AvailNum uint32 `xml:"availNum,attr" json:"availNum,omitempty"`
+ AvailsExpected uint32 `xml:"availsExpected,attr" json:"availsExpected,omitempty"`
+}
+
+// DurationFlag returns the duration_flag.
+func (e *Event) DurationFlag() bool {
+ return e != nil && e.BreakDuration != nil
+}
+
+// ProgramSpliceFlag returns the program_splice_flag.
+func (e *Event) ProgramSpliceFlag() bool {
+ return e != nil && e.Program != nil
+}
+
+// EventComponent contains the Splice Points in Component Splice Mode.
+type EventComponent struct {
+ Tag uint32 `xml:"componentTag,attr" json:"componentTag"`
+ UTCSpliceTime UTCSpliceTime `xml:"utcSpliceTime,attr" json:"utcSpliceTime"`
+}
+
+// EventProgram contains the Splice Point in Program Splice Mode
+type EventProgram struct {
+ XMLName xml.Name `xml:"http://www.scte.org/schemas/35 Program" json:"-"`
+ UTCSpliceTime UTCSpliceTime `xml:"utcSpliceTime,attr" json:"utcSpliceTime"`
+}
diff --git a/internal/scte35/splice_time.go b/internal/scte35/splice_time.go
@@ -0,0 +1,27 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+// SpliceTime specifies the time of the splice event.
+type SpliceTime struct {
+ PTSTime *uint64 `xml:"ptsTime,attr" json:"ptsTime,omitempty"`
+}
+
+// TimeSpecifiedFlag returns true if PTSTime is not nil.
+func (t *SpliceTime) TimeSpecifiedFlag() bool {
+ return t != nil && t.PTSTime != nil
+}
diff --git a/internal/scte35/table.go b/internal/scte35/table.go
@@ -0,0 +1,91 @@
+package scte35
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// newTable creates a new table with the given parameters.
+func newTable(prefix, indent string) *table {
+ return &table{
+ prefix: prefix,
+ indent: indent,
+ b: &strings.Builder{},
+ }
+}
+
+// table simplifies construction of splice_info_section tables.
+type table struct {
+ prefix string
+ indent string
+ b *strings.Builder
+}
+
+// row0 writes a new row with 0 indents.
+func (t *table) row(indents int, key string, value any) {
+ _, _ = t.b.WriteString(t.prefix)
+ for i := 0; i < indents; i++ {
+ _, _ = t.b.WriteString(t.indent)
+ }
+ _, _ = t.b.WriteString(key)
+ if value != nil {
+ _, _ = t.b.WriteString(": ")
+ _, _ = t.b.WriteString(valueString(value))
+ }
+ _, _ = t.b.WriteRune('\n')
+}
+
+// String returns the table string.
+func (t *table) String() string {
+ return t.b.String()
+}
+
+// valueString converts the given value to a string
+func valueString(value any) string {
+ switch vt := value.(type) {
+ case string:
+ return vt
+ case int:
+ return strconv.FormatInt(int64(vt), 10)
+ case int8:
+ return strconv.FormatInt(int64(vt), 10)
+ case int16:
+ return strconv.FormatInt(int64(vt), 10)
+ case int32:
+ return strconv.FormatInt(int64(vt), 10)
+ case int64:
+ return strconv.FormatInt(vt, 10)
+ case uint:
+ return strconv.FormatUint(uint64(vt), 10)
+ case uintptr:
+ return strconv.FormatUint(uint64(vt), 10)
+ case uint8:
+ return strconv.FormatUint(uint64(vt), 10)
+ case uint16:
+ return strconv.FormatUint(uint64(vt), 10)
+ case uint32:
+ return strconv.FormatUint(uint64(vt), 10)
+ case uint64:
+ return strconv.FormatUint(vt, 10)
+ case *uint64:
+ if vt == nil {
+ return ""
+ }
+ return strconv.FormatUint(*vt, 10)
+ case bool:
+ if vt {
+ return "true"
+ }
+ return "false"
+ case time.Time:
+ return vt.Format(time.RFC3339)
+ case float32:
+ return fmt.Sprintf("%.2f", vt)
+ case []byte:
+ return fmt.Sprintf("%#02x", vt)
+ default:
+ return fmt.Sprintf("%s", vt)
+ }
+}
diff --git a/internal/scte35/time_descriptor.go b/internal/scte35/time_descriptor.go
@@ -0,0 +1,102 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "encoding/xml"
+ "fmt"
+
+ "github.com/bamiaux/iobit"
+)
+
+const (
+ // TimeDescriptorTag is the splice_descriptor_tag for a time_descriptor.
+ TimeDescriptorTag = 0x03
+)
+
+// TimeDescriptor is an implementation of a splice_descriptor. It provides an
+// optional extension to the splice_insert(), splice_null() and time_signal()
+// commands that allows a programmer’s wall clock time to be sent to a client.
+// For the highest accuracy, this descriptor should be used with the
+// time_signal() or splice_insert( ) command. This command may be inserted using
+// SCTE 104 or by out of band provisioning on the device inserting this message.
+type TimeDescriptor struct {
+ XMLName xml.Name `xml:"TimeDescriptor" json:"-"`
+ JSONType uint32 `xml:"-" json:"type"`
+ TAISeconds uint64 `xml:"taiSeconds,attr" json:"taiSeconds"`
+ TAINS uint32 `xml:"taiNs,attr" json:"taiNs"`
+ UTCOffset uint32 `xml:"utcOffset,attr" json:"utcOffset"`
+}
+
+// Tag returns the splice_descriptor_tag.
+func (sd *TimeDescriptor) Tag() uint32 {
+ // ensure JSONType is set
+ sd.JSONType = TimeDescriptorTag
+ return TimeDescriptorTag
+}
+
+// table returns the tabular description of this TimeDescriptor.
+func (sd *TimeDescriptor) writeTo(t *table) {
+ t.row(0, "time_descriptor() {", nil)
+ t.row(1, "splice_descriptor_tag", fmt.Sprintf("%#02x", TimeDescriptorTag))
+ t.row(1, "descriptor_length", sd.length())
+ t.row(1, "identifier", fmt.Sprintf("%#08x, (%s)", CUEIdentifier, CUEIASCII))
+ t.row(1, "tai_seconds", sd.TAISeconds)
+ t.row(1, "tai_ns", sd.TAINS)
+ t.row(1, "utc_offset", sd.UTCOffset)
+ t.row(0, "}", nil)
+}
+
+// decode updates this splice_descriptor from binary.
+func (sd *TimeDescriptor) decode(b []byte) error {
+ r := iobit.NewReader(b)
+ r.Skip(8) // splice_descriptor_tag
+ r.Skip(8) // descriptor_length
+ r.Skip(32) // identifier
+ sd.TAISeconds = r.Uint64(48)
+ sd.TAINS = r.Uint32(32)
+ sd.UTCOffset = r.Uint32(16)
+
+ return readerError(r)
+}
+
+// encode this splice_descriptor to binary.
+func (sd *TimeDescriptor) encode() ([]byte, error) {
+ length := sd.length()
+
+ // add 2 bytes to contain splice_descriptor_tag & descriptor_length
+ buf := make([]byte, length+2)
+ iow := iobit.NewWriter(buf)
+ iow.PutUint32(8, TimeDescriptorTag)
+ iow.PutUint32(8, uint32(length))
+ iow.PutUint32(32, CUEIdentifier)
+ iow.PutUint64(48, sd.TAISeconds)
+ iow.PutUint32(32, sd.TAINS)
+ iow.PutUint32(16, sd.UTCOffset)
+
+ err := iow.Flush()
+ return buf, err
+}
+
+// descriptorLength returns descriptor_length.
+func (sd *TimeDescriptor) length() int {
+ length := 32 // identifier
+ length += 48 // TAI_seconds
+ length += 32 // TAI_ns
+ length += 16 // UTC_offset
+ return length / 8
+}
diff --git a/internal/scte35/time_signal.go b/internal/scte35/time_signal.go
@@ -0,0 +1,122 @@
+// Copyright 2021 Comcast Cable Communications Management, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package scte35
+
+import (
+ "encoding/xml"
+ "fmt"
+
+ "github.com/bamiaux/iobit"
+)
+
+const (
+ // TimeSignalType is the splice_command_type for a time_signal SpliceCommand.
+ TimeSignalType = 0x06
+)
+
+// NewTimeSignal constructs a new time_signal command with the
+// given pts_time value
+func NewTimeSignal(ptsTime uint64) *TimeSignal {
+ return &TimeSignal{
+ SpliceTime: SpliceTime{
+ PTSTime: &ptsTime,
+ },
+ }
+}
+
+// TimeSignal provides a time synchronized data delivery mechanism. The syntax
+// of the time_signal() allows for the synchronization of the information
+// carried in this message with the System Time Clock (STC). The unique payload
+// of the message is carried in the descriptor, however the syntax and transport
+// capabilities afforded to splice_insert() messages are also afforded to the
+// time_signal(). The carriage however can be in a different PID than that
+// carrying the other cue messages used for signaling splice points.
+type TimeSignal struct {
+ XMLName xml.Name `xml:"http://www.scte.org/schemas/35 TimeSignal" json:"-"`
+ JSONType uint32 `xml:"-" json:"type"`
+ SpliceTime SpliceTime `xml:"http://www.scte.org/schemas/35 SpliceTime" json:"spliceTime"`
+}
+
+// Type returns the splice_command_type.
+func (cmd *TimeSignal) Type() uint32 {
+ // ensure JSONType is set
+ cmd.JSONType = TimeSignalType
+ return TimeSignalType
+}
+
+// decode a binary time_signal
+func (cmd *TimeSignal) decode(b []byte) error {
+ r := iobit.NewReader(b)
+ timeSpecifiedFlag := r.Bit()
+ if timeSpecifiedFlag {
+ r.Skip(6) // reserved
+ ptsTime := r.Uint64(33)
+ cmd.SpliceTime.PTSTime = &ptsTime
+ } else {
+ r.Skip(7) // reserved
+ }
+
+ if err := readerError(r); err != nil {
+ return fmt.Errorf("%s: %w", cmd.XMLName, err)
+ }
+ return nil
+}
+
+// encode this time_signal as binary.
+func (cmd *TimeSignal) encode() ([]byte, error) {
+ buf := make([]byte, cmd.length())
+
+ iow := iobit.NewWriter(buf)
+ if cmd.timeSpecifiedFlag() {
+ iow.PutBit(true)
+ iow.PutUint32(6, Reserved) // reserved
+ iow.PutUint64(33, *cmd.SpliceTime.PTSTime)
+ } else {
+ iow.PutBit(false)
+ iow.PutUint32(7, Reserved) // reserved
+ }
+
+ err := iow.Flush()
+ return buf, err
+}
+
+// commandLength returns the splice_command_length.
+func (cmd *TimeSignal) length() int {
+ length := 1 // time_specified_flag
+ if cmd.timeSpecifiedFlag() {
+ length += 6 // reserved
+ length += 33 // pts_time
+ } else {
+ length += 7 // reserved
+ }
+ return length / 8
+}
+
+// writeTo the given table.
+func (cmd *TimeSignal) writeTo(t *table) {
+ t.row(0, "time_signal() {", nil)
+ t.row(1, "time_specified_flag", cmd.timeSpecifiedFlag())
+ if cmd.timeSpecifiedFlag() {
+ t.row(1, "pts_time", cmd.SpliceTime.PTSTime)
+ }
+ t.row(0, "}", nil)
+}
+
+// timeSpecifiedFlag return the time_specified_flag.
+func (cmd *TimeSignal) timeSpecifiedFlag() bool {
+ return cmd != nil && cmd.SpliceTime.PTSTime != nil
+}