streaming

Media streaming and broadcast systems in Go
Log | Files | Refs | README | LICENSE

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:
Mgo.mod | 12++++++++++++
Ago.sum | 14++++++++++++++
Ainternal/scte35/LICENSE | 201+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/NOTICE.md | 23+++++++++++++++++++++++
Ainternal/scte35/README | 7+++++++
Ainternal/scte35/audio_descriptor.go | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/avail_descriptor.go | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/bandwidth_reservation.go | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/crc_32.go | 321+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/delivery_restrictions.go | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/dtmf_descriptor.go | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/encrypted_packet.go | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/private_command.go | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/private_descriptor.go | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/scte35.go | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/scte35_test.go | 799+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/segmentation_descriptor.go | 612+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/segmentation_upid.go | 302++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/segmentation_upid_test.go | 29+++++++++++++++++++++++++++++
Ainternal/scte35/splice_command.go | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/splice_descriptor.go | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/splice_info_section.go | 465+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/splice_info_section_test.go | 252+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/splice_insert.go | 321+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/splice_null.go | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/splice_schedule.go | 249+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/splice_time.go | 27+++++++++++++++++++++++++++
Ainternal/scte35/table.go | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/time_descriptor.go | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/scte35/time_signal.go | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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 +}