diff --git a/go.mod b/go.mod index 4a1758e..2429348 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,9 @@ module github.com/Eyevinn/VMAP -go 1.22 +go 1.23.0 + +toolchain go1.24.2 require github.com/matryer/is v1.4.1 + +require github.com/CarlLindqvist/xmltokenizer v0.0.10 diff --git a/go.sum b/go.sum index f95502a..f99e3f4 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ +github.com/CarlLindqvist/xmltokenizer v0.0.10 h1:pdp+yJZTOijVnGR6oeuqecXY9zIt7O28A5JXbL6Yp00= +github.com/CarlLindqvist/xmltokenizer v0.0.10/go.mod h1:OlBoGMMzCOY2cnz7NLSuBQjlVRYYbarlqbFelQf14XM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= diff --git a/vmap/decoder.go b/vmap/decoder.go new file mode 100644 index 0000000..157a488 --- /dev/null +++ b/vmap/decoder.go @@ -0,0 +1,551 @@ +package vmap + +import ( + "bytes" + "errors" + "io" + "strconv" + + "github.com/CarlLindqvist/xmltokenizer" +) + +func DecodeVast(input []byte) (VAST, error) { + var vast VAST + found := false + f := bytes.NewReader([]byte(input)) + + tok := xmltokenizer.New(f, xmltokenizer.WithAttrBufferSize(5)) + + for { + token, err := tok.Token() // Token is only valid until next tok.Token() invocation (short-lived object). + if err == io.EOF { + break + } + if err != nil { + panic(err) + } + switch string(token.Name.Local) { + case "VAST": + found = true + // Reuse Token object in the sync.Pool since we only use it temporarily. + se := xmltokenizer.GetToken().Copy(token) + err = vast.UnmarshalToken(tok, se) + xmltokenizer.PutToken(se) // Put back to sync.Pool. + if err != nil { + return vast, err + } + } + } + + if !found { + return vast, errors.New("no VAST token found in document") + } + return vast, nil +} + +func DecodeVmap(input []byte) (VMAP, error) { + var vmap VMAP + found := false + + f := bytes.NewReader([]byte(input)) + + tok := xmltokenizer.New(f, xmltokenizer.WithAttrBufferSize(5)) + + for { + token, err := tok.Token() // Token is only valid until next tok.Token() invocation (short-lived object). + if err == io.EOF { + break + } + if err != nil { + panic(err) + } + switch string(token.Name.Local) { + case "VMAP": + found = true + for i := range token.Attrs { + attr := &token.Attrs[i] + switch string(attr.Name.Local) { + case "version": + vmap.Version = string(attr.Value) + case "vmap": + vmap.Vmap = string(attr.Value) + vmap.XMLName.Space = string(attr.Value) + } + vmap.XMLName.Local = "VMAP" + } + + case "AdBreak": + var adBreak AdBreak + // Reuse Token object in the sync.Pool since we only use it temporarily. + se := xmltokenizer.GetToken().Copy(token) + err = adBreak.UnmarshalToken(tok, se) + xmltokenizer.PutToken(se) // Put back to sync.Pool. + if err != nil { + return vmap, err + } + vmap.AdBreaks = append(vmap.AdBreaks, adBreak) + } + } + + if !found { + return vmap, errors.New("no VMAP token found in document") + } + return vmap, nil +} + +func (adBreak *AdBreak) UnmarshalToken(tok *xmltokenizer.Tokenizer, se *xmltokenizer.Token) error { + adBreak.AdSource = &AdSource{ + VASTData: &VASTData{}, + } + var err error + for i := range se.Attrs { + attr := &se.Attrs[i] + switch string(attr.Name.Local) { + case "breakId": + adBreak.Id = string(attr.Value) + case "breakType": + adBreak.BreakType = string(attr.Value) + case "timeOffset": + err = adBreak.TimeOffset.UnmarshalText(attr.Value) + if err != nil { + return err + } + } + } + + for { + token, err := tok.Token() + if err != nil { + return err + } + if token.IsEndElementOf(se) { // Reach desired EndElement + return nil + } + if token.IsEndElement { // Ignore child's EndElements + continue + } + switch string(token.Name.Local) { + case "VAST": + var vast VAST + // Reuse Token object in the sync.Pool since we only use it temporarily. + se := xmltokenizer.GetToken().Copy(token) + err = vast.UnmarshalToken(tok, se) + xmltokenizer.PutToken(se) // Put back to sync.Pool. + if err != nil { + return err + } + adBreak.AdSource.VASTData.VAST = &vast + case "Tracking": + if adBreak.TrackingEvents == nil { + adBreak.TrackingEvents = []TrackingEvent{} + } + var t TrackingEvent + for i := range token.Attrs { + attr := &token.Attrs[i] + switch string(attr.Name.Local) { + case "event": + t.Event = string(attr.Value) + } + } + if token.WasCDATA { + t.Text = string(token.Data) + } else { + t.Text = string(xmlStringToString(token.Data)) + } + adBreak.TrackingEvents = append(adBreak.TrackingEvents, t) + } + } +} + +func (vast *VAST) UnmarshalToken(tok *xmltokenizer.Tokenizer, se *xmltokenizer.Token) error { + for i := range se.Attrs { + attr := &se.Attrs[i] + switch string(attr.Name.Local) { + case "version": + vast.Version = string(attr.Value) + } + } + + for { + token, err := tok.Token() + if err != nil { + return err + } + if token.IsEndElementOf(se) { // Reach desired EndElement + return nil + } + if token.IsEndElement { // Ignore child's EndElements + continue + } + switch string(token.Name.Local) { + case "Ad": + var ad Ad + // Reuse Token object in the sync.Pool since we only use it temporarily. + se := xmltokenizer.GetToken().Copy(token) + err = ad.UnmarshalToken(tok, se) + xmltokenizer.PutToken(se) // Put back to sync.Pool. + if err != nil { + return err + } + vast.Ad = append(vast.Ad, ad) + } + } +} + +func (ad *Ad) UnmarshalToken(tok *xmltokenizer.Tokenizer, se *xmltokenizer.Token) error { + for i := range se.Attrs { + attr := &se.Attrs[i] + switch string(attr.Name.Local) { + case "sequence": + seq, err := strconv.Atoi(string(attr.Value)) + if err != nil { + return err + } + ad.Sequence = seq + case "id": + ad.Id = string(attr.Value) + } + } + for { + token, err := tok.Token() + if err != nil { + return err + } + if token.IsEndElementOf(se) { // Reach desired EndElement + return nil + } + if token.IsEndElement { // Ignore child's EndElements + continue + } + switch string(token.Name.Local) { + case "InLine": + var inline InLine + // Reuse Token object in the sync.Pool since we only use it temporarily. + se := xmltokenizer.GetToken().Copy(token) + err = inline.UnmarshalToken(tok, se) + xmltokenizer.PutToken(se) // Put back to sync.Pool. + if err != nil { + return err + } + ad.InLine = &inline + } + } +} + +func (inline *InLine) UnmarshalToken(tok *xmltokenizer.Tokenizer, se *xmltokenizer.Token) error { + for { + token, err := tok.Token() + if err != nil { + return err + } + if token.IsEndElementOf(se) { // Reach desired EndElement + return nil + } + if token.IsEndElement { // Ignore child's EndElements + continue + } + switch string(token.Name.Local) { + case "Creative": + var c Creative + se := xmltokenizer.GetToken().Copy(token) + err = c.UnmarshalToken(tok, se) + xmltokenizer.PutToken(se) // Put back to sync.Pool. + if err != nil { + return err + } + inline.Creatives = append(inline.Creatives, c) + case "Impression": + var imp Impression + for i := range token.Attrs { + attr := &token.Attrs[i] + switch string(attr.Name.Local) { + case "id": + imp.Id = string(attr.Value) + } + } + if token.WasCDATA { + imp.Text = string(token.Data) + } else { + imp.Text = string(xmlStringToString(token.Data)) + } + inline.Impression = append(inline.Impression, imp) + case "AdSystem": + if token.WasCDATA { + inline.AdSystem = string(token.Data) + } else { + inline.AdSystem = string(xmlStringToString(token.Data)) + } + case "AdTitle": + if token.WasCDATA { + inline.AdTitle = string(token.Data) + } else { + inline.AdTitle = string(xmlStringToString(token.Data)) + } + case "Extension": + var e Extension + // Reuse Token object in the sync.Pool since we only use it temporarily. + se := xmltokenizer.GetToken().Copy(token) + err = e.UnmarshalToken(tok, se) + xmltokenizer.PutToken(se) // Put back to sync.Pool. + if err != nil { + return err + } + inline.Extensions = append(inline.Extensions, e) + case "Error": + var er Error + er.Value = string(token.Data) + if token.WasCDATA { + er.Value = string(token.Data) + } else { + er.Value = string(xmlStringToString(token.Data)) + } + inline.Error = &er + } + } +} + +func (c *Creative) UnmarshalToken(tok *xmltokenizer.Tokenizer, se *xmltokenizer.Token) error { + for i := range se.Attrs { + attr := &se.Attrs[i] + switch string(attr.Name.Local) { + case "id": + c.Id = string(attr.Value) + case "adId": + c.AdId = string(attr.Value) + case "sequence": + //TODO + } + } + + for { + token, err := tok.Token() + if err != nil { + return err + } + if token.IsEndElementOf(se) { // Reach desired EndElement + return nil + } + if token.IsEndElement { // Ignore child's EndElements + continue + } + + switch string(token.Name.Local) { + case "UniversalAdId": + var uaid UniversalAdId + for i := range token.Attrs { + attr := &token.Attrs[i] + switch string(attr.Name.Local) { + case "idRegistry": + uaid.IdRegistry = string(attr.Value) + } + } + if token.WasCDATA { + uaid.Id = string(token.Data) + } else { + uaid.Id = string(xmlStringToString(token.Data)) + } + c.UniversalAdId = &uaid + case "Tracking": + if c.Linear == nil { + c.Linear = &Linear{} + } + var t TrackingEvent + for i := range token.Attrs { + attr := &token.Attrs[i] + switch string(attr.Name.Local) { + case "event": + t.Event = string(attr.Value) + } + } + if token.WasCDATA { + t.Text = string(token.Data) + } else { + t.Text = string(xmlStringToString(token.Data)) + } + c.Linear.TrackingEvents = append(c.Linear.TrackingEvents, t) + case "ClickThrough": + c.Linear.ClickThrough = &ClickThrough{} + for i := range token.Attrs { + attr := &token.Attrs[i] + switch string(attr.Name.Local) { + case "id": + c.Linear.ClickThrough.Id = string(attr.Value) + } + } + if token.WasCDATA { + c.Linear.ClickThrough.Text = string(token.Data) + } else { + c.Linear.ClickThrough.Text = string(xmlStringToString(token.Data)) + } + case "ClickTracking": + if c.Linear == nil { + c.Linear = &Linear{} + } + var ct ClickTracking + for i := range token.Attrs { + attr := &token.Attrs[i] + switch string(attr.Name.Local) { + case "id": + ct.Id = string(attr.Value) + } + } + if token.WasCDATA { + ct.Text = string(token.Data) + } else { + ct.Text = string(xmlStringToString(token.Data)) + } + c.Linear.ClickTracking = append(c.Linear.ClickTracking, ct) + case "Duration": + if c.Linear == nil { + c.Linear = &Linear{} + } + if token.WasCDATA { + err = c.Linear.Duration.UnmarshalText(token.Data) + } else { + err = c.Linear.Duration.UnmarshalText(xmlStringToString(token.Data)) + } + + if err != nil { + return err + } + case "MediaFile": + if c.Linear == nil { + c.Linear = &Linear{} + } + var m MediaFile + for i := range token.Attrs { + attr := &token.Attrs[i] + switch string(attr.Name.Local) { + case "bitrate": + m.Bitrate, err = strconv.Atoi(string(attr.Value)) + if err != nil { + return err + } + case "height": + m.Height, err = strconv.Atoi(string(attr.Value)) + if err != nil { + return err + } + case "width": + m.Width, err = strconv.Atoi(string(attr.Value)) + if err != nil { + return err + } + case "delivery": + m.Delivery = string(attr.Value) + case "type": + m.MediaType = string(attr.Value) + case "codec": + m.Codec = string(attr.Value) + } + } + if token.WasCDATA { + m.Text = string(token.Data) + } else { + m.Text = string(xmlStringToString(token.Data)) + } + c.Linear.MediaFiles = append(c.Linear.MediaFiles, m) + } + } +} + +func (ext *Extension) UnmarshalToken(tok *xmltokenizer.Tokenizer, se *xmltokenizer.Token) error { + for i := range se.Attrs { + attr := &se.Attrs[i] + switch string(attr.Name.Local) { + case "type": + ext.ExtensionType = string(attr.Value) + } + } + for { + token, err := tok.Token() + if err != nil { + return err + } + if token.IsEndElementOf(se) { // Reach desired EndElement + return nil + } + if token.IsEndElement { // Ignore child's EndElements + continue + } + + switch string(token.Name.Local) { + case "CreativeParameter": + var par CreativeParameter + for i := range token.Attrs { + attr := &token.Attrs[i] + switch string(attr.Name.Local) { + case "creativeId": + par.CreativeId = string(attr.Value) + case "name": + par.Name = string(attr.Value) + case "type": + par.CreativeParameterType = string(attr.Value) + } + } + if token.WasCDATA { + par.Value = string(token.Data) + } else { + par.Value = string(xmlStringToString(token.Data)) + } + ext.CreativeParameters = append(ext.CreativeParameters, par) + } + } +} + +func xmlStringToString(input []byte) []byte { + o := 0 + for i := 0; i < len(input); i++ { + b := input[i] + + switch b { + //If we see a '&' we have a special character that needs decoding + case '&': + cb := make([]byte, 0, 4) + specialCharLoop: + for { + i++ + if i >= len(input) { + break + } + + c := input[i] + switch c { + case '#', 'x': + case ';': + break specialCharLoop + default: + cb = append(cb, c) + } + } + ch := decodeSpecialCharacterFromHexCode(cb) + for _, l := range []byte(string(ch)) { + input[o] = l + o++ + } + //This is just a normal byte, just output it + default: + input[o] = b + o++ + } + } + return input[0:o] +} + +func decodeSpecialCharacterFromHexCode(input []byte) rune { + // Handle & < > ' " + switch string(input) { + case "amp": + return '&' + case "lt": + return '<' + case "gt": + return '>' + case "apos": + return '\'' + case "quot": + return '"' + } + codePoint, _ := strconv.ParseInt(string(input), 16, 32) + return rune(codePoint) +} diff --git a/vmap/sample-vmap/testVast2.xml b/vmap/sample-vmap/testVast2.xml new file mode 100644 index 0000000..7a250ba --- /dev/null +++ b/vmap/sample-vmap/testVast2.xml @@ -0,0 +1,377 @@ + + + + + + FreeWheel + Blommande körsbärsträd + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=&r=524918&adid=84694610&reid=589423750&arid=0&async=0&iw=&uxnw=&uxss=&uxct=&et=e&cn=[ERRORCODE] + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=&r=524918&adid=84694610&reid=589423750&arid=0&auid=&cn=defaultImpression&et=i&_cc=84694610,589423750,,,1746536264,1&tpos=919&async=0&iw=&uxnw=&uxss=&uxct=&metr=7&init=1&vcid2=794e2760-28a4-4b07-bf80-96e963a6020e&pingids=5991 + https://730721846599843.tv4mms.a2d.tv/tracker.png + + + 145507734 + + 00:00:03 + + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=&r=524918&adid=84694610&reid=589423750&arid=0&auid=&cn=complete&et=i&_cc=&tpos=919&async=0&init=1&iw=&uxnw=&uxss=&uxct=&metr=7 + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=&r=524918&adid=84694610&reid=589423750&arid=0&auid=&cn=firstQuartile&et=i&_cc=&tpos=919&async=0&init=1&iw=&uxnw=&uxss=&uxct=&metr=7 + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=&r=524918&adid=84694610&reid=589423750&arid=0&auid=&cn=midPoint&et=i&_cc=&tpos=919&async=0&init=1&iw=&uxnw=&uxss=&uxct=&metr=7 + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=&r=524918&adid=84694610&reid=589423750&arid=0&auid=&cn=thirdQuartile&et=i&_cc=&tpos=919&async=0&init=1&iw=&uxnw=&uxss=&uxct=&metr=7 + + + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=&r=524918&adid=84694610&reid=589423750&arid=0&auid=&cn=defaultClick&et=c&_cc=&tpos=919&async=0 + + + https://prism-ads-cdn.a2d.tv/abr/start_4_RV3_K_RSB_RSBLOMMOR_2023_P8_mp4_1712561245_11776886_981/f65c0f0e-9da5-49da-9ba7-3eced8070fe8/index.m3u8 + + + + + + + 145507734 + + bumper + + + + + + + + FreeWheel + Alla 20-49 + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918&adid=84892990&reid=890417550&arid=0&async=0&iw=&uxnw=&uxss=&uxct=&et=e&cn=[ERRORCODE] + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918&adid=84892990&reid=890417550&arid=0&auid=&cn=defaultImpression&et=i&_cc=84892990,890417550,,,1746536264,1&tpos=919&async=0&iw=&uxnw=&uxss=&uxct=&metr=7&init=1&vcid2=794e2760-28a4-4b07-bf80-96e963a6020e&asid=434600981&ssid=-1&pingids=5991 + https://visitanalytics.userreport.com/hit?t=FFTdaf53103&event=impression&gdpr=1&gdpr_consent=&camp_id=84302701&camp_name=%5BWOO%5D%20Tre%20Lansering%20v.%2017-21%20%2B%2029-31%20%C3%85lder%3AK%C3%B6n&io_id=84892987&pl_id=84892989&pl_name=Alla%2020-49&cr_id=235953821&cr_name=E1H32W3000&deal_id=&d=dpid&ip=193.45.52.147&rnd=1477005635 + https://730721846599843.tv4mms.a2d.tv/tracker.png + + + E1H32W3000 + + 00:00:30 + + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918&adid=84892990&reid=890417550&arid=0&auid=&cn=complete&et=i&_cc=&tpos=919&async=0&init=1&iw=&uxnw=&uxss=&uxct=&metr=7 + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918&adid=84892990&reid=890417550&arid=0&auid=&cn=firstQuartile&et=i&_cc=&tpos=919&async=0&init=1&iw=&uxnw=&uxss=&uxct=&metr=7 + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918&adid=84892990&reid=890417550&arid=0&auid=&cn=midPoint&et=i&_cc=&tpos=919&async=0&init=1&iw=&uxnw=&uxss=&uxct=&metr=7 + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918&adid=84892990&reid=890417550&arid=0&auid=&cn=thirdQuartile&et=i&_cc=&tpos=919&async=0&init=1&iw=&uxnw=&uxss=&uxct=&metr=7 + + + https://www.tre.se/handla/mobilabonnemang?utm_medium=paid_video&utm_source=tv4play&utm_content=p3_25_mammis_30s&utm_campaign=3b2c_2025_p3_konceptlansering_seefeel + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918&adid=84892990&reid=890417550&arid=0&auid=&cn=defaultClick&et=c&_cc=&tpos=919&async=0 + + + https://prism-ads-cdn.a2d.tv/abr/5ZcbCx_16848953_981/c1315035-4031-4456-b9a6-d4bdef88f99b/index.m3u8 + + + + + + + E1H32W3000 + + + + + + + FreeWheel + ProgMod_Placeholder_AD + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918%3B518308&adid=53236803&reid=831875215&arid=1441792&async=0&iw=&uxnw=&uxss=&uxct=&et=e&cn=[ERRORCODE] + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918%3B518308&adid=53236803&reid=831875215&arid=1441792&auid=&cn=defaultImpression&et=i&_cc=53236803,831875215,m264360;0;0.,,1746536264,1&tpos=919&async=0&iw=&uxnw=&uxss=&uxct=&metr=7&init=1&vcid2=794e2760-28a4-4b07-bf80-96e963a6020e&asid=434600981&ssid=-1&dealid=256113&pingids=5991 + https://googleads4.g.doubleclick.net/pcs/view?xai=AKAOjstp9uelNrSZC68oNLIgV2he0ylYJDYaKlopaoVYuYFESNM7cf_VT0kq1NnQoyjXTZVxZ7_yM2P-4VAu82UAL2GfJbCowpm8BenEimOm0YT-Rve67rP7ImM2khFbPwWeYkA-DpLHTdrrLhRONHCsBN2E&sai=AMfl-YRDzoKACRaiphAvDnpKd3K8kMVaN4x09rlMMfMkPbK995ZvlOpfy4dhHW_u1ba0ZKCiTFyPyH7UHyR2EtBtTHP-4YQ&sig=Cg0ArKJSzNzYXzpKt0RNEAE&uach_m=%5BUACH%5D&cry=1&fbs_aeid=%5Bgw_fbsaeid%5D&urlfix=1&adurl= + https://aax-eu.amazon-adsystem.com/e/is/6c383505a97d267185466970b0d4b7ad/imp?b=JKKypjcLNiORtYxJV9S6rBwAAAGWpaxx-AMAAAe_BAAzcHhfdHhuX2JpZDEgICAzcHhfdHhuX2ltcDEgICBlzhah&w=AAAAAAAAAAAAAAAAAAAAALb8ggZpo6MjtfHDBg&bi=KCP-6wKQZWQbzM6Bjw-vPnNr4NOkuY2wHZ80D8M2hOKQIZDKYysbM2oqHXDgtvb8NdzIMUkEh1vq6bARbUkOYeY53P0nF.7qEu888Buw9MqkoVWmGSo6v-fTnwWqqTESIVDlCX4tRgc9c4-IeDAFyqePG5n1LPFROJzbTnev5AlpRwXeECX3uyQcmYlMQyVff9LmIOeUj8ZVqmRSHiAAVGMsUTej2XAWnskwGgdul7gJHtqJgV90BiG2Nhvln--RcV0ch5PxQeEkTxThtfmQwJ6s1UMHMBVqIB1wZGmiuY8LMbje-bH63M5E4G0gBBsfZTD7y6Sxb.hAyTk2vscuhItU7Wd4ZsLJEcBnGiPi7Z5Lb9bcPbREIzsxBb6HkbYiav2KOr3mNZSo4y2kgz2Jp3KYigHuChO1UDe9jY1fyb0oX5pnUrRzRL1d7WcGUU-e4PD-4bbPEDcw9TYjVDbziLW964bgS7at0gOUxgLIwvblziFbibCiDPolC.1j01z9tDxmhQ2rYrDuLi5hAQhXVdxcUkzTQUASCdqOcvkKe4pPls9kIacOYTGpWKMdXocJpF3y3a0ScsLjbcO.oVJBGC7w125yA62UZzrNbi-jrbYeniYvZ-MPqDKL4.DhaXhmjgbFcKcVcnYQ4Z5ZG4xYwihw2-VgoTMJb2yeM8lf-5hDfrQFhI-j5Zi6cIO2WQTy + https://aax-eu.amazon-adsystem.com/s/iui3?ex-fch=416719&d=forester-did&cb=4956751576937641&gdpr_pd=1&gdpr_consent=CQNEqlgQNEqlgAcABBSVBdFsAP_gAAAAAChQK1wNAAEQAKAAsACAAFQALgAZAA8ACAAGQANAAiQBNAE4ALYAXwAxABuADmAICAQQBBgCFAEYANEAfoBCACIgEWAI6ATgArIBcwDFAG2AO2AmQBSYCwwF5gMZAZYA4QBy4E9IKRgpXBS0FMgKaQU2BT-CoIKiQVGBVCCqgKsQVaBV-CsIKxwVlBWsAAABISAYAAgABYAFQAPAAggBkAGgARAB-gFzAMUAvMBy44AKAAgAC4BCACIgKTHQDwAFgAVABBADIANAAiABiAGiAP0AiwBcwDFAJkAXmAywBy5AAEAAgBSZCAIAAsAMQBcwDFEoA4ACAAFgBEADEAYoBeYDLCQAIAC4CkykAwABYAFQAQQAyADQAIgAYgBogD9AIsAXMAxQC8ygAIAC4B2w.f_wAAAAAAAAA&gdpr=1&v-args=%3Ft%3D3%26d%3D15%26ct%3D%255B1014%252C1020%255D%26ca%3D%255B7%255D%26s%3D1983&ex-fargs=%3Fi%3DorKmNws2I5G1jElX1LqsHA%26e%3DvideoImpression%26a%3D577981708224706615%26c%3D577469246600535182%26s%3Dpda%26u%3DorKmNws2I5G1jElX1LqsHA&vdb=%5B1014%2C1020%5D%3A%5B7%5D%3A3%3Anull%3A15%3A1983 + https://ade.googlesyndication.com/ddm/activity/dc_oe=ChMI2fX3zPKOjQMVKZlQBh1R9xWSEAEYACD82pJvKgTBLTSTSABQOljPdWD-4_sPaM2izMcB;dc_eps=AHas8cDsBHGGsBOQIDkqH05lGBgdSs08YEQXY4sssz3gVFnR_feS7jWkm_9g9JMKSztLy0FwR_5sE8puQ1-PnTXg7ZCjCTog2wT0;met=1;ecn1=1;etm1=0;eid1=11; + https://aax-eu.amazon-adsystem.com/s/iui3?ex-fch=416719&d=forester-did&cb=8329697195822472&gdpr_pd=1&gdpr_consent=CQNEqlgQNEqlgAcABBSVBdFsAP_gAAAAAChQK1wNAAEQAKAAsACAAFQALgAZAA8ACAAGQANAAiQBNAE4ALYAXwAxABuADmAICAQQBBgCFAEYANEAfoBCACIgEWAI6ATgArIBcwDFAG2AO2AmQBSYCwwF5gMZAZYA4QBy4E9IKRgpXBS0FMgKaQU2BT-CoIKiQVGBVCCqgKsQVaBV-CsIKxwVlBWsAAABISAYAAgABYAFQAPAAggBkAGgARAB-gFzAMUAvMBy44AKAAgAC4BCACIgKTHQDwAFgAVABBADIANAAiABiAGiAP0AiwBcwDFAJkAXmAywBy5AAEAAgBSZCAIAAsAMQBcwDFEoA4ACAAFgBEADEAYoBeYDLCQAIAC4CkykAwABYAFQAQQAyADQAIgAYgBogD9AIsAXMAxQC8ygAIAC4B2w.f_wAAAAAAAAA&gdpr=1&v-args=%3Ft%3D3%26d%3D15%26ct%3D%255B1014%252C1020%255D%26ca%3D%255B7%255D%26s%3D1983&ex-fargs=%3Fi%3DorKmNws2I5G1jElX1LqsHA%26e%3DvideoStart%26a%3D577981708224706615%26c%3D577469246600535182%26s%3Dpda%26u%3DorKmNws2I5G1jElX1LqsHA&vdb=%5B1014%2C1020%5D%3A%5B7%5D%3A3%3Anull%3A15%3A1983 + https://aax-eu.amazon-adsystem.com/x/px/JKKypjcLNiORtYxJV9S6rBwAAAGWpaxx-AMAAAe_BAAzcHhfdHhuX2JpZDEgICAzcHhfdHhuX2ltcDEgICBlzhah/%7B%22c%22%3A%22video%22%2C%22src%22%3A1983%2C%22start%22%3A1%7D + https://730721846599843.tv4mms.a2d.tv/tracker.png + + + 233090428-1 + + 00:00:15 + + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918%3B518308&adid=53236803&reid=831875215&arid=1441792&auid=&cn=firstQuartile&et=i&_cc=&tpos=919&async=0&iw=&uxnw=&uxss=&uxct=&metr=7&init=1 + https://ade.googlesyndication.com/ddm/activity/dc_oe=ChMI2fX3zPKOjQMVKZlQBh1R9xWSEAEYACD82pJvKgTBLTSTSABQOljPdWD-4_sPaM2izMcB;dc_eps=AHas8cDsBHGGsBOQIDkqH05lGBgdSs08YEQXY4sssz3gVFnR_feS7jWkm_9g9JMKSztLy0FwR_5sE8puQ1-PnTXg7ZCjCTog2wT0;met=1;ecn1=1;etm1=0;eid1=960584; + https://aax-eu.amazon-adsystem.com/s/iui3?ex-fch=416719&d=forester-did&cb=346710249895589&gdpr_pd=1&gdpr_consent=CQNEqlgQNEqlgAcABBSVBdFsAP_gAAAAAChQK1wNAAEQAKAAsACAAFQALgAZAA8ACAAGQANAAiQBNAE4ALYAXwAxABuADmAICAQQBBgCFAEYANEAfoBCACIgEWAI6ATgArIBcwDFAG2AO2AmQBSYCwwF5gMZAZYA4QBy4E9IKRgpXBS0FMgKaQU2BT-CoIKiQVGBVCCqgKsQVaBV-CsIKxwVlBWsAAABISAYAAgABYAFQAPAAggBkAGgARAB-gFzAMUAvMBy44AKAAgAC4BCACIgKTHQDwAFgAVABBADIANAAiABiAGiAP0AiwBcwDFAJkAXmAywBy5AAEAAgBSZCAIAAsAMQBcwDFEoA4ACAAFgBEADEAYoBeYDLCQAIAC4CkykAwABYAFQAQQAyADQAIgAYgBogD9AIsAXMAxQC8ygAIAC4B2w.f_wAAAAAAAAA&gdpr=1&v-args=%3Ft%3D3%26d%3D15%26ct%3D%255B1014%252C1020%255D%26ca%3D%255B7%255D%26s%3D1983&ex-fargs=%3Fi%3DorKmNws2I5G1jElX1LqsHA%26e%3DvideoFirstQuartile%26a%3D577981708224706615%26c%3D577469246600535182%26s%3Dpda%26u%3DorKmNws2I5G1jElX1LqsHA&vdb=%5B1014%2C1020%5D%3A%5B7%5D%3A3%3Anull%3A15%3A1983 + https://aax-eu.amazon-adsystem.com/x/px/JKKypjcLNiORtYxJV9S6rBwAAAGWpaxx-AMAAAe_BAAzcHhfdHhuX2JpZDEgICAzcHhfdHhuX2ltcDEgICBlzhah/%7B%221q%22%3A1%2C%22c%22%3A%22video%22%2C%22src%22%3A1983%7D + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918%3B518308&adid=53236803&reid=831875215&arid=1441792&auid=&cn=midPoint&et=i&_cc=&tpos=919&async=0&iw=&uxnw=&uxss=&uxct=&metr=7&init=1 + https://ade.googlesyndication.com/ddm/activity/dc_oe=ChMI2fX3zPKOjQMVKZlQBh1R9xWSEAEYACD82pJvKgTBLTSTSABQOljPdWD-4_sPaM2izMcB;dc_eps=AHas8cDsBHGGsBOQIDkqH05lGBgdSs08YEQXY4sssz3gVFnR_feS7jWkm_9g9JMKSztLy0FwR_5sE8puQ1-PnTXg7ZCjCTog2wT0;met=1;ecn1=1;etm1=0;eid1=18; + https://aax-eu.amazon-adsystem.com/s/iui3?ex-fch=416719&d=forester-did&cb=1019708848455052&gdpr_pd=1&gdpr_consent=CQNEqlgQNEqlgAcABBSVBdFsAP_gAAAAAChQK1wNAAEQAKAAsACAAFQALgAZAA8ACAAGQANAAiQBNAE4ALYAXwAxABuADmAICAQQBBgCFAEYANEAfoBCACIgEWAI6ATgArIBcwDFAG2AO2AmQBSYCwwF5gMZAZYA4QBy4E9IKRgpXBS0FMgKaQU2BT-CoIKiQVGBVCCqgKsQVaBV-CsIKxwVlBWsAAABISAYAAgABYAFQAPAAggBkAGgARAB-gFzAMUAvMBy44AKAAgAC4BCACIgKTHQDwAFgAVABBADIANAAiABiAGiAP0AiwBcwDFAJkAXmAywBy5AAEAAgBSZCAIAAsAMQBcwDFEoA4ACAAFgBEADEAYoBeYDLCQAIAC4CkykAwABYAFQAQQAyADQAIgAYgBogD9AIsAXMAxQC8ygAIAC4B2w.f_wAAAAAAAAA&gdpr=1&v-args=%3Ft%3D3%26d%3D15%26ct%3D%255B1014%252C1020%255D%26ca%3D%255B7%255D%26s%3D1983&ex-fargs=%3Fi%3DorKmNws2I5G1jElX1LqsHA%26e%3DvideoMidpoint%26a%3D577981708224706615%26c%3D577469246600535182%26s%3Dpda%26u%3DorKmNws2I5G1jElX1LqsHA&vdb=%5B1014%2C1020%5D%3A%5B7%5D%3A3%3Anull%3A15%3A1983 + https://aax-eu.amazon-adsystem.com/x/px/JKKypjcLNiORtYxJV9S6rBwAAAGWpaxx-AMAAAe_BAAzcHhfdHhuX2JpZDEgICAzcHhfdHhuX2ltcDEgICBlzhah/%7B%22c%22%3A%22video%22%2C%22src%22%3A1983%2C%222q%22%3A1%7D + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918%3B518308&adid=53236803&reid=831875215&arid=1441792&auid=&cn=thirdQuartile&et=i&_cc=&tpos=919&async=0&iw=&uxnw=&uxss=&uxct=&metr=7&init=1 + https://ade.googlesyndication.com/ddm/activity/dc_oe=ChMI2fX3zPKOjQMVKZlQBh1R9xWSEAEYACD82pJvKgTBLTSTSABQOljPdWD-4_sPaM2izMcB;dc_eps=AHas8cDsBHGGsBOQIDkqH05lGBgdSs08YEQXY4sssz3gVFnR_feS7jWkm_9g9JMKSztLy0FwR_5sE8puQ1-PnTXg7ZCjCTog2wT0;met=1;ecn1=1;etm1=0;eid1=960585; + https://aax-eu.amazon-adsystem.com/s/iui3?ex-fch=416719&d=forester-did&cb=4738944078196523&gdpr_pd=1&gdpr_consent=CQNEqlgQNEqlgAcABBSVBdFsAP_gAAAAAChQK1wNAAEQAKAAsACAAFQALgAZAA8ACAAGQANAAiQBNAE4ALYAXwAxABuADmAICAQQBBgCFAEYANEAfoBCACIgEWAI6ATgArIBcwDFAG2AO2AmQBSYCwwF5gMZAZYA4QBy4E9IKRgpXBS0FMgKaQU2BT-CoIKiQVGBVCCqgKsQVaBV-CsIKxwVlBWsAAABISAYAAgABYAFQAPAAggBkAGgARAB-gFzAMUAvMBy44AKAAgAC4BCACIgKTHQDwAFgAVABBADIANAAiABiAGiAP0AiwBcwDFAJkAXmAywBy5AAEAAgBSZCAIAAsAMQBcwDFEoA4ACAAFgBEADEAYoBeYDLCQAIAC4CkykAwABYAFQAQQAyADQAIgAYgBogD9AIsAXMAxQC8ygAIAC4B2w.f_wAAAAAAAAA&gdpr=1&v-args=%3Ft%3D3%26d%3D15%26ct%3D%255B1014%252C1020%255D%26ca%3D%255B7%255D%26s%3D1983&ex-fargs=%3Fi%3DorKmNws2I5G1jElX1LqsHA%26e%3DvideoThirdQuartile%26a%3D577981708224706615%26c%3D577469246600535182%26s%3Dpda%26u%3DorKmNws2I5G1jElX1LqsHA&vdb=%5B1014%2C1020%5D%3A%5B7%5D%3A3%3Anull%3A15%3A1983 + https://aax-eu.amazon-adsystem.com/x/px/JKKypjcLNiORtYxJV9S6rBwAAAGWpaxx-AMAAAe_BAAzcHhfdHhuX2JpZDEgICAzcHhfdHhuX2ltcDEgICBlzhah/%7B%22c%22%3A%22video%22%2C%22src%22%3A1983%2C%223q%22%3A1%7D + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918%3B518308&adid=53236803&reid=831875215&arid=1441792&auid=&cn=complete&et=i&_cc=&tpos=919&async=0&iw=&uxnw=&uxss=&uxct=&metr=7&init=1 + https://ade.googlesyndication.com/ddm/activity/dc_oe=ChMI2fX3zPKOjQMVKZlQBh1R9xWSEAEYACD82pJvKgTBLTSTSABQOljPdWD-4_sPaM2izMcB;dc_eps=AHas8cDsBHGGsBOQIDkqH05lGBgdSs08YEQXY4sssz3gVFnR_feS7jWkm_9g9JMKSztLy0FwR_5sE8puQ1-PnTXg7ZCjCTog2wT0;met=1;ecn1=1;etm1=0;eid1=13; + https://aax-eu.amazon-adsystem.com/s/iui3?ex-fch=416719&d=forester-did&cb=9041028638469008&gdpr_pd=1&gdpr_consent=CQNEqlgQNEqlgAcABBSVBdFsAP_gAAAAAChQK1wNAAEQAKAAsACAAFQALgAZAA8ACAAGQANAAiQBNAE4ALYAXwAxABuADmAICAQQBBgCFAEYANEAfoBCACIgEWAI6ATgArIBcwDFAG2AO2AmQBSYCwwF5gMZAZYA4QBy4E9IKRgpXBS0FMgKaQU2BT-CoIKiQVGBVCCqgKsQVaBV-CsIKxwVlBWsAAABISAYAAgABYAFQAPAAggBkAGgARAB-gFzAMUAvMBy44AKAAgAC4BCACIgKTHQDwAFgAVABBADIANAAiABiAGiAP0AiwBcwDFAJkAXmAywBy5AAEAAgBSZCAIAAsAMQBcwDFEoA4ACAAFgBEADEAYoBeYDLCQAIAC4CkykAwABYAFQAQQAyADQAIgAYgBogD9AIsAXMAxQC8ygAIAC4B2w.f_wAAAAAAAAA&gdpr=1&v-args=%3Ft%3D3%26d%3D15%26ct%3D%255B1014%252C1020%255D%26ca%3D%255B7%255D%26s%3D1983&ex-fargs=%3Fi%3DorKmNws2I5G1jElX1LqsHA%26e%3DvideoComplete%26a%3D577981708224706615%26c%3D577469246600535182%26s%3Dpda%26u%3DorKmNws2I5G1jElX1LqsHA&vdb=%5B1014%2C1020%5D%3A%5B7%5D%3A3%3Anull%3A15%3A1983 + https://aax-eu.amazon-adsystem.com/x/px/JKKypjcLNiORtYxJV9S6rBwAAAGWpaxx-AMAAAe_BAAzcHhfdHhuX2JpZDEgICAzcHhfdHhuX2ltcDEgICBlzhah/%7B%22c%22%3A%22video%22%2C%22src%22%3A1983%2C%22cpl%22%3A1%7D + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918%3B518308&adid=53236803&reid=831875215&arid=1441792&auid=&cn=_mute&et=s&_cc=&tpos=919&async=0 + https://ade.googlesyndication.com/ddm/activity/dc_oe=ChMI2fX3zPKOjQMVKZlQBh1R9xWSEAEYACD82pJvKgTBLTSTSABQOljPdWD-4_sPaM2izMcB;dc_eps=AHas8cDsBHGGsBOQIDkqH05lGBgdSs08YEQXY4sssz3gVFnR_feS7jWkm_9g9JMKSztLy0FwR_5sE8puQ1-PnTXg7ZCjCTog2wT0;met=1;ecn1=1;etm1=0;eid1=16; + https://aax-eu.amazon-adsystem.com/s/iui3?ex-fch=416719&d=forester-did&cb=8089363953119340&gdpr_pd=1&gdpr_consent=CQNEqlgQNEqlgAcABBSVBdFsAP_gAAAAAChQK1wNAAEQAKAAsACAAFQALgAZAA8ACAAGQANAAiQBNAE4ALYAXwAxABuADmAICAQQBBgCFAEYANEAfoBCACIgEWAI6ATgArIBcwDFAG2AO2AmQBSYCwwF5gMZAZYA4QBy4E9IKRgpXBS0FMgKaQU2BT-CoIKiQVGBVCCqgKsQVaBV-CsIKxwVlBWsAAABISAYAAgABYAFQAPAAggBkAGgARAB-gFzAMUAvMBy44AKAAgAC4BCACIgKTHQDwAFgAVABBADIANAAiABiAGiAP0AiwBcwDFAJkAXmAywBy5AAEAAgBSZCAIAAsAMQBcwDFEoA4ACAAFgBEADEAYoBeYDLCQAIAC4CkykAwABYAFQAQQAyADQAIgAYgBogD9AIsAXMAxQC8ygAIAC4B2w.f_wAAAAAAAAA&gdpr=1&v-args=%3Ft%3D3%26d%3D15%26ct%3D%255B1014%252C1020%255D%26ca%3D%255B7%255D%26s%3D1983&ex-fargs=%3Fi%3DorKmNws2I5G1jElX1LqsHA%26e%3DvideoMute%26a%3D577981708224706615%26c%3D577469246600535182%26s%3Dpda%26u%3DorKmNws2I5G1jElX1LqsHA&vdb=%5B1014%2C1020%5D%3A%5B7%5D%3A3%3Anull%3A15%3A1983 + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918%3B518308&adid=53236803&reid=831875215&arid=1441792&auid=&cn=_un-mute&et=s&_cc=&tpos=919&async=0 + https://ade.googlesyndication.com/ddm/activity/dc_oe=ChMI2fX3zPKOjQMVKZlQBh1R9xWSEAEYACD82pJvKgTBLTSTSABQOljPdWD-4_sPaM2izMcB;dc_eps=AHas8cDsBHGGsBOQIDkqH05lGBgdSs08YEQXY4sssz3gVFnR_feS7jWkm_9g9JMKSztLy0FwR_5sE8puQ1-PnTXg7ZCjCTog2wT0;met=1;ecn1=1;etm1=0;eid1=149645; + https://aax-eu.amazon-adsystem.com/s/iui3?ex-fch=416719&d=forester-did&cb=66912368996410&gdpr_pd=1&gdpr_consent=CQNEqlgQNEqlgAcABBSVBdFsAP_gAAAAAChQK1wNAAEQAKAAsACAAFQALgAZAA8ACAAGQANAAiQBNAE4ALYAXwAxABuADmAICAQQBBgCFAEYANEAfoBCACIgEWAI6ATgArIBcwDFAG2AO2AmQBSYCwwF5gMZAZYA4QBy4E9IKRgpXBS0FMgKaQU2BT-CoIKiQVGBVCCqgKsQVaBV-CsIKxwVlBWsAAABISAYAAgABYAFQAPAAggBkAGgARAB-gFzAMUAvMBy44AKAAgAC4BCACIgKTHQDwAFgAVABBADIANAAiABiAGiAP0AiwBcwDFAJkAXmAywBy5AAEAAgBSZCAIAAsAMQBcwDFEoA4ACAAFgBEADEAYoBeYDLCQAIAC4CkykAwABYAFQAQQAyADQAIgAYgBogD9AIsAXMAxQC8ygAIAC4B2w.f_wAAAAAAAAA&gdpr=1&v-args=%3Ft%3D3%26d%3D15%26ct%3D%255B1014%252C1020%255D%26ca%3D%255B7%255D%26s%3D1983&ex-fargs=%3Fi%3DorKmNws2I5G1jElX1LqsHA%26e%3DvideoUnmute%26a%3D577981708224706615%26c%3D577469246600535182%26s%3Dpda%26u%3DorKmNws2I5G1jElX1LqsHA&vdb=%5B1014%2C1020%5D%3A%5B7%5D%3A3%3Anull%3A15%3A1983 + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918%3B518308&adid=53236803&reid=831875215&arid=1441792&auid=&cn=_pause&et=s&_cc=&tpos=919&async=0 + https://ade.googlesyndication.com/ddm/activity/dc_oe=ChMI2fX3zPKOjQMVKZlQBh1R9xWSEAEYACD82pJvKgTBLTSTSABQOljPdWD-4_sPaM2izMcB;dc_eps=AHas8cDsBHGGsBOQIDkqH05lGBgdSs08YEQXY4sssz3gVFnR_feS7jWkm_9g9JMKSztLy0FwR_5sE8puQ1-PnTXg7ZCjCTog2wT0;met=1;ecn1=1;etm1=0;eid1=15; + https://aax-eu.amazon-adsystem.com/s/iui3?ex-fch=416719&d=forester-did&cb=6910527340519603&gdpr_pd=1&gdpr_consent=CQNEqlgQNEqlgAcABBSVBdFsAP_gAAAAAChQK1wNAAEQAKAAsACAAFQALgAZAA8ACAAGQANAAiQBNAE4ALYAXwAxABuADmAICAQQBBgCFAEYANEAfoBCACIgEWAI6ATgArIBcwDFAG2AO2AmQBSYCwwF5gMZAZYA4QBy4E9IKRgpXBS0FMgKaQU2BT-CoIKiQVGBVCCqgKsQVaBV-CsIKxwVlBWsAAABISAYAAgABYAFQAPAAggBkAGgARAB-gFzAMUAvMBy44AKAAgAC4BCACIgKTHQDwAFgAVABBADIANAAiABiAGiAP0AiwBcwDFAJkAXmAywBy5AAEAAgBSZCAIAAsAMQBcwDFEoA4ACAAFgBEADEAYoBeYDLCQAIAC4CkykAwABYAFQAQQAyADQAIgAYgBogD9AIsAXMAxQC8ygAIAC4B2w.f_wAAAAAAAAA&gdpr=1&v-args=%3Ft%3D3%26d%3D15%26ct%3D%255B1014%252C1020%255D%26ca%3D%255B7%255D%26s%3D1983&ex-fargs=%3Fi%3DorKmNws2I5G1jElX1LqsHA%26e%3DvideoPause%26a%3D577981708224706615%26c%3D577469246600535182%26s%3Dpda%26u%3DorKmNws2I5G1jElX1LqsHA&vdb=%5B1014%2C1020%5D%3A%5B7%5D%3A3%3Anull%3A15%3A1983 + https://aax-eu.amazon-adsystem.com/x/px/JKKypjcLNiORtYxJV9S6rBwAAAGWpaxx-AMAAAe_BAAzcHhfdHhuX2JpZDEgICAzcHhfdHhuX2ltcDEgICBlzhah/%7B%22p%22%3A1%2C%22c%22%3A%22video%22%2C%22src%22%3A1983%7D + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918%3B518308&adid=53236803&reid=831875215&arid=1441792&auid=&cn=_expand&et=s&_cc=&tpos=919&async=0 + https://ade.googlesyndication.com/ddm/activity/dc_oe=ChMI2fX3zPKOjQMVKZlQBh1R9xWSEAEYACD82pJvKgTBLTSTSABQOljPdWD-4_sPaM2izMcB;dc_eps=AHas8cDsBHGGsBOQIDkqH05lGBgdSs08YEQXY4sssz3gVFnR_feS7jWkm_9g9JMKSztLy0FwR_5sE8puQ1-PnTXg7ZCjCTog2wT0;met=1;ecn1=1;etm1=0;eid1=19; + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918%3B518308&adid=53236803&reid=831875215&arid=1441792&auid=&cn=_resume&et=s&_cc=&tpos=919&async=0 + https://aax-eu.amazon-adsystem.com/s/iui3?ex-fch=416719&d=forester-did&cb=2320475029873650&gdpr_pd=1&gdpr_consent=CQNEqlgQNEqlgAcABBSVBdFsAP_gAAAAAChQK1wNAAEQAKAAsACAAFQALgAZAA8ACAAGQANAAiQBNAE4ALYAXwAxABuADmAICAQQBBgCFAEYANEAfoBCACIgEWAI6ATgArIBcwDFAG2AO2AmQBSYCwwF5gMZAZYA4QBy4E9IKRgpXBS0FMgKaQU2BT-CoIKiQVGBVCCqgKsQVaBV-CsIKxwVlBWsAAABISAYAAgABYAFQAPAAggBkAGgARAB-gFzAMUAvMBy44AKAAgAC4BCACIgKTHQDwAFgAVABBADIANAAiABiAGiAP0AiwBcwDFAJkAXmAywBy5AAEAAgBSZCAIAAsAMQBcwDFEoA4ACAAFgBEADEAYoBeYDLCQAIAC4CkykAwABYAFQAQQAyADQAIgAYgBogD9AIsAXMAxQC8ygAIAC4B2w.f_wAAAAAAAAA&gdpr=1&v-args=%3Ft%3D3%26d%3D15%26ct%3D%255B1014%252C1020%255D%26ca%3D%255B7%255D%26s%3D1983&ex-fargs=%3Fi%3DorKmNws2I5G1jElX1LqsHA%26e%3DvideoResume%26a%3D577981708224706615%26c%3D577469246600535182%26s%3Dpda%26u%3DorKmNws2I5G1jElX1LqsHA&vdb=%5B1014%2C1020%5D%3A%5B7%5D%3A3%3Anull%3A15%3A1983 + https://aax-eu.amazon-adsystem.com/x/px/JKKypjcLNiORtYxJV9S6rBwAAAGWpaxx-AMAAAe_BAAzcHhfdHhuX2JpZDEgICAzcHhfdHhuX2ltcDEgICBlzhah/%7B%22r%22%3A1%2C%22c%22%3A%22video%22%2C%22src%22%3A1983%7D + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918%3B518308&adid=53236803&reid=831875215&arid=1441792&auid=&cn=_rewind&et=s&_cc=&tpos=919&async=0 + https://aax-eu.amazon-adsystem.com/s/iui3?ex-fch=416719&d=forester-did&cb=8817632692896136&gdpr_pd=1&gdpr_consent=CQNEqlgQNEqlgAcABBSVBdFsAP_gAAAAAChQK1wNAAEQAKAAsACAAFQALgAZAA8ACAAGQANAAiQBNAE4ALYAXwAxABuADmAICAQQBBgCFAEYANEAfoBCACIgEWAI6ATgArIBcwDFAG2AO2AmQBSYCwwF5gMZAZYA4QBy4E9IKRgpXBS0FMgKaQU2BT-CoIKiQVGBVCCqgKsQVaBV-CsIKxwVlBWsAAABISAYAAgABYAFQAPAAggBkAGgARAB-gFzAMUAvMBy44AKAAgAC4BCACIgKTHQDwAFgAVABBADIANAAiABiAGiAP0AiwBcwDFAJkAXmAywBy5AAEAAgBSZCAIAAsAMQBcwDFEoA4ACAAFgBEADEAYoBeYDLCQAIAC4CkykAwABYAFQAQQAyADQAIgAYgBogD9AIsAXMAxQC8ygAIAC4B2w.f_wAAAAAAAAA&gdpr=1&v-args=%3Ft%3D3%26d%3D15%26ct%3D%255B1014%252C1020%255D%26ca%3D%255B7%255D%26s%3D1983&ex-fargs=%3Fi%3DorKmNws2I5G1jElX1LqsHA%26e%3DvideoSkipBackward%26a%3D577981708224706615%26c%3D577469246600535182%26s%3Dpda%26u%3DorKmNws2I5G1jElX1LqsHA&vdb=%5B1014%2C1020%5D%3A%5B7%5D%3A3%3Anull%3A15%3A1983 + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918%3B518308&adid=53236803&reid=831875215&arid=1441792&auid=&cn=_accept-invitation&et=s&_cc=&tpos=919&async=0 + https://aax-eu.amazon-adsystem.com/s/iui3?ex-fch=416719&d=forester-did&cb=4633332100950709&gdpr_pd=1&gdpr_consent=CQNEqlgQNEqlgAcABBSVBdFsAP_gAAAAAChQK1wNAAEQAKAAsACAAFQALgAZAA8ACAAGQANAAiQBNAE4ALYAXwAxABuADmAICAQQBBgCFAEYANEAfoBCACIgEWAI6ATgArIBcwDFAG2AO2AmQBSYCwwF5gMZAZYA4QBy4E9IKRgpXBS0FMgKaQU2BT-CoIKiQVGBVCCqgKsQVaBV-CsIKxwVlBWsAAABISAYAAgABYAFQAPAAggBkAGgARAB-gFzAMUAvMBy44AKAAgAC4BCACIgKTHQDwAFgAVABBADIANAAiABiAGiAP0AiwBcwDFAJkAXmAywBy5AAEAAgBSZCAIAAsAMQBcwDFEoA4ACAAFgBEADEAYoBeYDLCQAIAC4CkykAwABYAFQAQQAyADQAIgAYgBogD9AIsAXMAxQC8ygAIAC4B2w.f_wAAAAAAAAA&gdpr=1&v-args=%3Ft%3D3%26d%3D15%26ct%3D%255B1014%252C1020%255D%26ca%3D%255B7%255D%26s%3D1983&ex-fargs=%3Fi%3DorKmNws2I5G1jElX1LqsHA%26e%3DvideoCreativeView%26a%3D577981708224706615%26c%3D577469246600535182%26s%3Dpda%26u%3DorKmNws2I5G1jElX1LqsHA&vdb=%5B1014%2C1020%5D%3A%5B7%5D%3A3%3Anull%3A15%3A1983 + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918%3B518308&adid=53236803&reid=831875215&arid=1441792&auid=&cn=_close&et=s&_cc=&tpos=919&async=0 + https://aax-eu.amazon-adsystem.com/s/iui3?ex-fch=416719&d=forester-did&cb=3154322839807089&gdpr_pd=1&gdpr_consent=CQNEqlgQNEqlgAcABBSVBdFsAP_gAAAAAChQK1wNAAEQAKAAsACAAFQALgAZAA8ACAAGQANAAiQBNAE4ALYAXwAxABuADmAICAQQBBgCFAEYANEAfoBCACIgEWAI6ATgArIBcwDFAG2AO2AmQBSYCwwF5gMZAZYA4QBy4E9IKRgpXBS0FMgKaQU2BT-CoIKiQVGBVCCqgKsQVaBV-CsIKxwVlBWsAAABISAYAAgABYAFQAPAAggBkAGgARAB-gFzAMUAvMBy44AKAAgAC4BCACIgKTHQDwAFgAVABBADIANAAiABiAGiAP0AiwBcwDFAJkAXmAywBy5AAEAAgBSZCAIAAsAMQBcwDFEoA4ACAAFgBEADEAYoBeYDLCQAIAC4CkykAwABYAFQAQQAyADQAIgAYgBogD9AIsAXMAxQC8ygAIAC4B2w.f_wAAAAAAAAA&gdpr=1&v-args=%3Ft%3D3%26d%3D15%26ct%3D%255B1014%252C1020%255D%26ca%3D%255B7%255D%26s%3D1983&ex-fargs=%3Fi%3DorKmNws2I5G1jElX1LqsHA%26e%3DvideoClose%26a%3D577981708224706615%26c%3D577469246600535182%26s%3Dpda%26u%3DorKmNws2I5G1jElX1LqsHA&vdb=%5B1014%2C1020%5D%3A%5B7%5D%3A3%3Anull%3A15%3A1983 + + + https://adclick.g.doubleclick.net/pcs/click?xai=AKAOjsum1W1Iy3YtJhj6KxB4cQE8j0x3vR-aT9pbzCJFJwlT_aSQlO72J1OkcMP-pmUl9jbhyhyJ_VVLDg-DBX_AN1dsS25rtBDWHLhhebk6bOS7RBc1YPwHwy7UkE-jEuW_gdG6lP52wdBgMuhvzizAEyNRar7_RcDqHfih1uckQw&sai=AMfl-YT2IVdJu9wtGQmBhak4mnTnBS1yvC_FqT07BD2y9M0PT-z7r7ChnMHiR7iW_xm6cMrFtclLJ8ZTWVql&sig=Cg0ArKJSzETz0QfuvdjFEAE&cry=1&fbs_aeid=%5Bgw_fbsaeid%5D&urlfix=1&adurl=https://havrefras.se%3Futm_source%3Dtv4play%26utm_medium%3Dpaid_video%26utm_campaign%3D2024_of_havrefras_v.16-19%26dclid%3D%25edclid!%26gad_source%3D7 + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=201326592&r=524918%3B518308&adid=53236803&reid=831875215&arid=1441792&auid=&cn=defaultClick&et=c&_cc=&tpos=919&async=0 + https://aax-eu.amazon-adsystem.com/s/iui3?aref=%5B%5BCS_MADS_TOKEN%5D%5D&ex-fch=416719&d=forester-did&cb=9599340948680342&gdpr_pd=1&gdpr_consent=CQNEqlgQNEqlgAcABBSVBdFsAP_gAAAAAChQK1wNAAEQAKAAsACAAFQALgAZAA8ACAAGQANAAiQBNAE4ALYAXwAxABuADmAICAQQBBgCFAEYANEAfoBCACIgEWAI6ATgArIBcwDFAG2AO2AmQBSYCwwF5gMZAZYA4QBy4E9IKRgpXBS0FMgKaQU2BT-CoIKiQVGBVCCqgKsQVaBV-CsIKxwVlBWsAAABISAYAAgABYAFQAPAAggBkAGgARAB-gFzAMUAvMBy44AKAAgAC4BCACIgKTHQDwAFgAVABBADIANAAiABiAGiAP0AiwBcwDFAJkAXmAywBy5AAEAAgBSZCAIAAsAMQBcwDFEoA4ACAAFgBEADEAYoBeYDLCQAIAC4CkykAwABYAFQAQQAyADQAIgAYgBogD9AIsAXMAxQC8ygAIAC4B2w.f_wAAAAAAAAA&gdpr=1&v-args=%3Ft%3D3%26d%3D15%26ct%3D%255B1014%252C1020%255D%26ca%3D%255B7%255D%26s%3D1983&ex-fargs=%3Fi%3DorKmNws2I5G1jElX1LqsHA%26e%3DvideoClick%26a%3D577981708224706615%26c%3D577469246600535182%26s%3Dpda%26u%3DorKmNws2I5G1jElX1LqsHA&vdb=%5B1014%2C1020%5D%3A%5B7%5D%3A3%3Anull%3A15%3A1983 + https://aax-eu.amazon-adsystem.com/x/c/JKKypjcLNiORtYxJV9S6rBwAAAGWpaxx-AMAAAe_BAAzcHhfdHhuX2JpZDEgICAzcHhfdHhuX2ltcDEgICBlzhah/clv1_CEuOPUxokZB9hHrVXf0OxgTPWypaD5VstroDxWWu7vhV4X8Vny6df_w18HHE8xvbmq442pNCnbWmP0nDHwJkJE1FKDrLN5fcSkMgYnEob0K6PK8EYFEE89IliATjLfm-EsjLNhP9s9RSA0MhlFD1a1sJOldNDd93Q1piUbI6XVeDyfnNkkszRVC7PITyOqgoCHBLaoni0btJbVVKcNtLSeMVuNWYuPIKWEnyFLRwyQktuYpotp68QXYWqm2D_gB6OEbQaNz-5Cdf6WE0GPkvayyIflibqg3lG824rp_VY68nBn358p-A_TCvIzb9JaONztpB2s9TF_7mrt7QUMiXFJtPjsB1eDwP94g1_49OPLtAz1BBHHuPthaW0bYbWX6dEztAj1qksWQIlgrWCZaPfz99drPqmfxJP707El00ogpoBehRnSxrQDww0rRSZBEWDJO3SP2xo9maXyNNYSyilL0ScWuSxFjtOjFMe41-mtP9ATU_KUI2G9A556MhdlUrXTDscLCse8OVQ7orPeVFLF2X07byr52koNLnmlHwRPT1bWb-Y2olmD9HVxOtLjmvRuV7r9JhbHLAy7xSrdsaGwSGMRd_mjd42vts-I6hjVoHppCt3e9Jfke55QqHa_bgTHgnTt57LAcL7OVeKNUS8Ucx15XPMQ9hWWBGnsJC6d47GwkYUAjkrXUerOmwLS81r3Rc_1dL1u0q1_MKCVu5AEJEo53SYK0ws9SKS0EQGg2J-UmrHY0tnmAB0uIOdvW5vo5Anjt3X5v221ooGiuuHcrqfQ/ + https://aax-eu.amazon-adsystem.com/x/px/JKKypjcLNiORtYxJV9S6rBwAAAGWpaxx-AMAAAe_BAAzcHhfdHhuX2JpZDEgICAzcHhfdHhuX2ltcDEgICBlzhah/%7B%22c%22%3A%22video%22%2C%22clk%22%3A1%2C%22src%22%3A1983%7D + + + https://prism-ads-cdn.a2d.tv/abr/file_16659921_981/3ebcf19f-016d-4be2-abd7-1f746d106c37/index.m3u8 + + + + + + + 233090428-1 + + + + + + + FreeWheel + Blommande körsbärsträd + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=&r=524918&adid=84694611&reid=589423891&arid=0&async=0&iw=&uxnw=&uxss=&uxct=&et=e&cn=[ERRORCODE] + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=&r=524918&adid=84694611&reid=589423891&arid=0&auid=&cn=defaultImpression&et=i&_cc=84694611,589423891,,,1746536264,1&tpos=919&async=0&iw=&uxnw=&uxss=&uxct=&metr=7&init=1&vcid2=794e2760-28a4-4b07-bf80-96e963a6020e&pingids=5991 + https://730721846599843.tv4mms.a2d.tv/tracker.png + + + 145507756 + + 00:00:03 + + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=&r=524918&adid=84694611&reid=589423891&arid=0&auid=&cn=complete&et=i&_cc=&tpos=919&async=0&init=1&iw=&uxnw=&uxss=&uxct=&metr=7 + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=&r=524918&adid=84694611&reid=589423891&arid=0&auid=&cn=firstQuartile&et=i&_cc=&tpos=919&async=0&init=1&iw=&uxnw=&uxss=&uxct=&metr=7 + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=&r=524918&adid=84694611&reid=589423891&arid=0&auid=&cn=midPoint&et=i&_cc=&tpos=919&async=0&init=1&iw=&uxnw=&uxss=&uxct=&metr=7 + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=&r=524918&adid=84694611&reid=589423891&arid=0&auid=&cn=thirdQuartile&et=i&_cc=&tpos=919&async=0&init=1&iw=&uxnw=&uxss=&uxct=&metr=7 + + + https://80276.v.fwmrm.net/ad/l/1?s=v2c22&n=524918%3B524918%3B512166%3B512167%3B512188%3B516869%3B517250%3B517327%3B517424%3B518308%3B529333&t=1746536263310556650&f=&r=524918&adid=84694611&reid=589423891&arid=0&auid=&cn=defaultClick&et=c&_cc=&tpos=919&async=0 + + + https://prism-ads-cdn.a2d.tv/abr/end_4_HV3_K_RSB_RSBLOMMOR_2023_P8_mp4_1712561286_11776887_981/41c8dc1f-da69-48f2-9883-814a65404fe2/index.m3u8 + + + + + + + 145507756 + + bumper + + + + + + diff --git a/vmap/sample-vmap/testVastSpecialChars.xml b/vmap/sample-vmap/testVastSpecialChars.xml new file mode 100644 index 0000000..4430067 --- /dev/null +++ b/vmap/sample-vmap/testVastSpecialChars.xml @@ -0,0 +1,8 @@ + + + + + Hej&ö <>" + + + diff --git a/vmap/structure_test.go b/vmap/structure_test.go index ddde73f..c656142 100644 --- a/vmap/structure_test.go +++ b/vmap/structure_test.go @@ -3,8 +3,11 @@ package vmap import ( "encoding/json" "encoding/xml" + "fmt" "io" "os" + "strings" + "sync" "testing" "time" @@ -20,6 +23,7 @@ func TestUnmarshalVMAP(t *testing.T) { var vmap VMAP xmlBytes, err := io.ReadAll(f) is.NoErr(err) + err = xml.Unmarshal(xmlBytes, &vmap) is.NoErr(err) @@ -47,6 +51,43 @@ func TestUnmarshalVMAP(t *testing.T) { is.Equal(len(thirdBreak.TrackingEvents), 1) } +func TestDecodeVmap(t *testing.T) { + is := is.New(t) + f, err := os.Open("sample-vmap/testVmap.xml") + is.NoErr(err) + defer f.Close() + + var vmap VMAP + xmlBytes, err := io.ReadAll(f) + is.NoErr(err) + + vmap, err = DecodeVmap(xmlBytes) + is.NoErr(err) + + is.Equal(len(vmap.AdBreaks), 3) + firstBreak := vmap.AdBreaks[0] + is.Equal(firstBreak.Id, "midroll.ad-1") + is.Equal(firstBreak.BreakType, "linear") + is.True(firstBreak.TimeOffset.Duration == nil) + is.Equal(firstBreak.TimeOffset.Position, OffsetStart) + is.True(firstBreak.AdSource.VASTData.VAST != nil) + is.Equal(len(firstBreak.TrackingEvents), 1) + + secondBreak := vmap.AdBreaks[1] + is.Equal(secondBreak.Id, "midroll.ad-2") + is.Equal(secondBreak.BreakType, "linear") + is.Equal(*secondBreak.TimeOffset.Duration, Duration{5 * time.Minute}) + is.True(firstBreak.AdSource.VASTData.VAST != nil) + is.Equal(len(secondBreak.TrackingEvents), 1) + + thirdBreak := vmap.AdBreaks[2] + is.Equal(thirdBreak.Id, "midroll.ad-3") + is.Equal(thirdBreak.BreakType, "linear") + is.Equal(*thirdBreak.TimeOffset.Duration, Duration{7 * time.Minute}) + is.True(thirdBreak.AdSource.VASTData.VAST != nil) + is.Equal(len(thirdBreak.TrackingEvents), 1) +} + func TestUnmarshalVast(t *testing.T) { is := is.New(t) f, err := os.Open("sample-vmap/testVast.xml") @@ -105,6 +146,64 @@ func TestUnmarshalVast(t *testing.T) { is.Equal(mediaFile.Codec, "H.264") } +func TestDecodeVast(t *testing.T) { + is := is.New(t) + f, err := os.Open("sample-vmap/testVast.xml") + is.NoErr(err) + defer f.Close() + + var vast VAST + xmlBytes, err := io.ReadAll(f) + is.NoErr(err) + vast, err = DecodeVast(xmlBytes) + is.NoErr(err) + + is.Equal(len(vast.Ad), 2) + firstAd := vast.Ad[0] + is.Equal(firstAd.Id, "POD_AD-ID_001") + firstAdInLine := firstAd.InLine + is.Equal(firstAdInLine.AdSystem, "Test Adserver") + is.Equal(firstAdInLine.AdTitle, "Ad That Test-Adserver Wants Player To See #1") + + // Error validation + firstAdError := firstAdInLine.Error + is.True(firstAdError != nil) + is.Equal(firstAdError.Value, "https://error-url/code") + // Extension validation + firstAdExtensions := firstAdInLine.Extensions + is.Equal(len(firstAdExtensions), 1) + firstAdExtension := firstAdExtensions[0] + is.Equal(firstAdExtension.ExtensionType, "FreeWheel") + firstAdExtensionCParams := firstAdExtension.CreativeParameters[0] + is.Equal(firstAdExtensionCParams.CreativeId, "132285420") + is.Equal(firstAdExtensionCParams.Name, "AdType") + is.Equal(firstAdExtensionCParams.Value, "bumper") + is.Equal(firstAdExtensionCParams.CreativeParameterType, "Linear") + // Impression validation + firstAdImpression := firstAdInLine.Impression + is.Equal(len(firstAdImpression), 1) + // Creatives validation + firstAdCreatives := firstAdInLine.Creatives + is.Equal(len(firstAdCreatives), 1) + firstCreative := firstAdCreatives[0] + is.Equal(firstCreative.Id, "CRETIVE-ID_001") + is.Equal(firstCreative.AdId, "alvedon-10s") + is.Equal(len(firstCreative.Linear.TrackingEvents), 5) + is.Equal(firstCreative.Linear.Duration, Duration{10 * time.Second}) + is.Equal(len(firstCreative.Linear.MediaFiles), 1) + is.True(firstCreative.Linear.ClickThrough != nil) + is.Equal(len(firstCreative.Linear.ClickTracking), 0) + is.Equal(len(firstCreative.Linear.CustomClick), 0) + // MediaFile validation + mediaFile := firstCreative.Linear.MediaFiles[0] + is.Equal(mediaFile.Width, 718) + is.Equal(mediaFile.Height, 404) + is.Equal(mediaFile.MediaType, "video/mp4") + is.Equal(mediaFile.Delivery, "progressive") + is.Equal(mediaFile.Bitrate, 1300) + is.Equal(mediaFile.Codec, "H.264") +} + func TestUnmarshalDuration(t *testing.T) { is := is.New(t) d := Duration{} @@ -145,3 +244,135 @@ func TestMarshalJson(t *testing.T) { is.NoErr(err) is.Equal(vmap, vmap2) } + +func BenchmarkUnmarshal(b *testing.B) { + doc, err := os.ReadFile("sample-vmap/testVmap.xml") + if err != nil { + panic(err) + } + + var vmap VMAP + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = xml.Unmarshal(doc, &vmap) + } +} + +func BenchmarkFasterDecode(b *testing.B) { + doc, err := os.ReadFile("sample-vmap/testVmap.xml") + if err != nil { + panic(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = DecodeVmap(doc) + } +} + +func TestSpecialCharacters(t *testing.T) { + is := is.New(t) + doc, err := os.ReadFile("sample-vmap/testVastSpecialChars.xml") + if err != nil { + panic(err) + } + var vastUnmarshal VAST + _ = xml.Unmarshal(doc, &vastUnmarshal) + vastDecoded, _ := DecodeVast(doc) + + is.Equal(vastUnmarshal.Ad[0].InLine.AdTitle, vastDecoded.Ad[0].InLine.AdTitle) + is.Equal(vastDecoded.Ad[0].InLine.AdTitle, "Hej&รถ\n<>\"") +} + +func TestDecodeCompliance(t *testing.T) { + wg := sync.WaitGroup{} + //Check for race conditions + for range 1000 { + wg.Add(1) + go func(wg *sync.WaitGroup, t *testing.T) { + defer wg.Done() + is := is.New(t) + doc, err := os.ReadFile("sample-vmap/testVmap.xml") + is.NoErr(err) + + var vmap1 VMAP + err = xml.Unmarshal(doc, &vmap1) + is.NoErr(err) + + vmap2, err := DecodeVmap(doc) + is.NoErr(err) + + is.Equal(vmap1.Version, vmap2.Version) + is.Equal(vmap1.Vmap, vmap2.Vmap) + is.Equal(vmap1.XMLName.Local, vmap2.XMLName.Local) + is.Equal(vmap1.XMLName.Space, vmap2.XMLName.Space) + + is.Equal(len(vmap1.AdBreaks), len(vmap2.AdBreaks)) + for i := range vmap1.AdBreaks { + adb1 := vmap1.AdBreaks[i] + adb2 := vmap2.AdBreaks[i] + is.Equal(adb1.BreakType, adb2.BreakType) + is.Equal(adb1.Id, adb2.Id) + is.Equal(adb1.TimeOffset, adb2.TimeOffset) + is.Equal(adb1.TimeOffset.Duration, adb2.TimeOffset.Duration) + is.Equal(adb1.TimeOffset.Position, adb2.TimeOffset.Position) + + if adb1.TrackingEvents != nil { + te1 := adb1.TrackingEvents + te2 := adb2.TrackingEvents + + for j := range te1 { + abt1 := te1[j] + abt2 := te2[j] + is.Equal(abt1.Event, abt2.Event) + //Decode trims spaces, so not checking whitespace + is.Equal(strings.TrimSpace(abt1.Text), strings.TrimSpace(abt2.Text)) + } + } + + is.True(adb1.AdSource.VASTData.VAST != nil) + is.True(adb2.AdSource.VASTData.VAST != nil) + v1 := *adb1.AdSource.VASTData.VAST + v2 := *adb2.AdSource.VASTData.VAST + is.Equal(v1.Version, v2.Version) + is.Equal(v1.NoNamespaceSchemaLocation, v2.NoNamespaceSchemaLocation) + is.Equal(v1.Xsi, v2.Xsi) + + for j := range v1.Ad { + ad1 := v1.Ad[j] + ad2 := v2.Ad[j] + is.Equal(ad1.Id, ad2.Id) + is.Equal(ad1.Sequence, ad2.Sequence) + if ad1.InLine != nil { + is.Equal(strings.TrimSpace(ad1.InLine.AdSystem), strings.TrimSpace(ad2.InLine.AdSystem)) + is.Equal(strings.TrimSpace(ad1.InLine.AdTitle), strings.TrimSpace(ad2.InLine.AdTitle)) + is.Equal(ad1.InLine.Error, ad2.InLine.Error) + if ad1.InLine.Error != nil { + is.Equal(ad1.InLine.Error.Value, ad2.InLine.Error.Value) + } + if ad1.InLine.Creatives != nil { + for i := range ad1.InLine.Creatives { + for j := range ad1.InLine.Creatives[i].Linear.TrackingEvents { + is.Equal( + strings.TrimSpace(ad1.InLine.Creatives[i].Linear.TrackingEvents[j].Text), + strings.TrimSpace(ad2.InLine.Creatives[i].Linear.TrackingEvents[j].Text), + ) + } + for j := range ad1.InLine.Creatives[i].Linear.ClickTracking { + fmt.Println(strings.TrimSpace(ad1.InLine.Creatives[i].Linear.ClickTracking[j].Text)) + is.Equal( + strings.TrimSpace(ad1.InLine.Creatives[i].Linear.ClickTracking[j].Text), + strings.TrimSpace(ad2.InLine.Creatives[i].Linear.ClickTracking[j].Text), + ) + } + } + + } + } + } + } + }(&wg, t) + } + wg.Wait() +}