├── .travis.yml ├── LICENSE ├── README.md ├── duration.go ├── duration_test.go ├── extension.go ├── extension_test.go ├── offset.go ├── offset_test.go ├── testdata ├── creative_extensions.xml ├── extraspaces_vpaid.xml ├── inline_extensions.xml ├── liverail-vast2-linear-companion.xml ├── liverail-vast2-nonlinear.xml ├── spotx_adparameters.txt ├── spotx_html_resource.html ├── spotx_vpaid.xml ├── vast4_universal_ad_id.xml ├── vast_adaptv_attempt_attr.xml ├── vast_inline_linear-duration_undefined.xml ├── vast_inline_linear.xml ├── vast_inline_nonlinear.xml ├── vast_wrapper_linear_1.xml ├── vast_wrapper_linear_2.xml ├── vast_wrapper_nonlinear_1.xml └── vast_wrapper_nonlinear_2.xml ├── vast.go └── vast_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.7 4 | - 1.8 5 | - tip 6 | matrix: 7 | allow_failures: 8 | - go: tip 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Olivier Poitrey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VAST Golang Library 2 | 3 | [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/rs/vast) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/rs/vast/master/LICENSE) [![Build Status](https://travis-ci.org/rs/vast.svg?branch=master)](https://travis-ci.org/rs/vast) [![Coverage](http://gocover.io/_badge/github.com/rs/vast)](http://gocover.io/github.com/rs/vast) 4 | -------------------------------------------------------------------------------- /duration.go: -------------------------------------------------------------------------------- 1 | package vast 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // Duration is a VAST duration expressed a hh:mm:ss 11 | type Duration time.Duration 12 | 13 | // MarshalText implements the encoding.TextMarshaler interface. 14 | func (dur Duration) MarshalText() ([]byte, error) { 15 | h := dur / Duration(time.Hour) 16 | m := dur % Duration(time.Hour) / Duration(time.Minute) 17 | s := dur % Duration(time.Minute) / Duration(time.Second) 18 | ms := dur % Duration(time.Second) / Duration(time.Millisecond) 19 | if ms == 0 { 20 | return []byte(fmt.Sprintf("%02d:%02d:%02d", h, m, s)), nil 21 | } 22 | return []byte(fmt.Sprintf("%02d:%02d:%02d.%03d", h, m, s, ms)), nil 23 | } 24 | 25 | // UnmarshalText implements the encoding.TextUnmarshaler interface. 26 | func (dur *Duration) UnmarshalText(data []byte) (err error) { 27 | s := string(data) 28 | s = strings.TrimSpace(s) 29 | if s == "" || strings.ToLower(s) == "undefined" { 30 | *dur = 0 31 | return nil 32 | } 33 | parts := strings.SplitN(s, ":", 3) 34 | if len(parts) != 3 { 35 | return fmt.Errorf("invalid duration: %s", data) 36 | } 37 | if i := strings.IndexByte(parts[2], '.'); i > 0 { 38 | ms, err := strconv.ParseInt(parts[2][i+1:], 10, 32) 39 | if err != nil || ms < 0 || ms > 999 { 40 | return fmt.Errorf("invalid duration: %s", data) 41 | } 42 | parts[2] = parts[2][:i] 43 | *dur += Duration(ms) * Duration(time.Millisecond) 44 | } 45 | f := Duration(time.Second) 46 | for i := 2; i >= 0; i-- { 47 | n, err := strconv.ParseInt(parts[i], 10, 32) 48 | if err != nil || n < 0 || n > 59 { 49 | return fmt.Errorf("invalid duration: %s", data) 50 | } 51 | *dur += Duration(n) * f 52 | f *= 60 53 | } 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /duration_test.go: -------------------------------------------------------------------------------- 1 | package vast 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDurationMarshaler(t *testing.T) { 11 | b, err := Duration(0).MarshalText() 12 | if assert.NoError(t, err) { 13 | assert.Equal(t, "00:00:00", string(b)) 14 | } 15 | b, err = Duration(2 * time.Millisecond).MarshalText() 16 | if assert.NoError(t, err) { 17 | assert.Equal(t, "00:00:00.002", string(b)) 18 | } 19 | b, err = Duration(2 * time.Second).MarshalText() 20 | if assert.NoError(t, err) { 21 | assert.Equal(t, "00:00:02", string(b)) 22 | } 23 | b, err = Duration(2 * time.Minute).MarshalText() 24 | if assert.NoError(t, err) { 25 | assert.Equal(t, "00:02:00", string(b)) 26 | } 27 | b, err = Duration(2 * time.Hour).MarshalText() 28 | if assert.NoError(t, err) { 29 | assert.Equal(t, "02:00:00", string(b)) 30 | } 31 | } 32 | 33 | func TestDurationUnmarshal(t *testing.T) { 34 | var d Duration 35 | if assert.NoError(t, d.UnmarshalText([]byte("00:00:00"))) { 36 | assert.Equal(t, Duration(0), d) 37 | } 38 | d = 0 39 | if assert.NoError(t, d.UnmarshalText([]byte("00:00:02"))) { 40 | assert.Equal(t, Duration(2*time.Second), d) 41 | } 42 | d = 0 43 | if assert.NoError(t, d.UnmarshalText([]byte(" 00:00:02 "))) { 44 | assert.Equal(t, Duration(2*time.Second), d) 45 | } 46 | d = 0 47 | if assert.NoError(t, d.UnmarshalText([]byte("00:02:00"))) { 48 | assert.Equal(t, Duration(2*time.Minute), d) 49 | } 50 | d = 0 51 | if assert.NoError(t, d.UnmarshalText([]byte("02:00:00"))) { 52 | assert.Equal(t, Duration(2*time.Hour), d) 53 | } 54 | d = 0 55 | if assert.NoError(t, d.UnmarshalText([]byte("00:00:00.123"))) { 56 | assert.Equal(t, Duration(123*time.Millisecond), d) 57 | } 58 | d = 0 59 | if assert.NoError(t, d.UnmarshalText([]byte("undefined"))) { 60 | assert.Equal(t, Duration(0), d) 61 | } 62 | d = 0 63 | if assert.NoError(t, d.UnmarshalText([]byte(""))) { 64 | assert.Equal(t, Duration(0), d) 65 | } 66 | assert.EqualError(t, d.UnmarshalText([]byte("00:00:60")), "invalid duration: 00:00:60") 67 | assert.EqualError(t, d.UnmarshalText([]byte("00:60:00")), "invalid duration: 00:60:00") 68 | assert.EqualError(t, d.UnmarshalText([]byte("00:00:00.-1")), "invalid duration: 00:00:00.-1") 69 | assert.EqualError(t, d.UnmarshalText([]byte("00:00:00.1000")), "invalid duration: 00:00:00.1000") 70 | assert.EqualError(t, d.UnmarshalText([]byte("00h01m")), "invalid duration: 00h01m") 71 | } 72 | -------------------------------------------------------------------------------- /extension.go: -------------------------------------------------------------------------------- 1 | package vast 2 | 3 | import "encoding/xml" 4 | 5 | // Extension represent arbitrary XML provided by the platform to extend the 6 | // VAST response or by custom trackers. 7 | type Extension struct { 8 | Type string `xml:"type,attr,omitempty"` 9 | CustomTracking []Tracking `xml:"CustomTracking>Tracking,omitempty"` 10 | Data []byte `xml:",innerxml"` 11 | } 12 | 13 | // the extension type as a middleware in the encoding process. 14 | type extension Extension 15 | 16 | type extensionNoCT struct { 17 | Type string `xml:"type,attr,omitempty"` 18 | Data []byte `xml:",innerxml"` 19 | } 20 | 21 | // MarshalXML implements xml.Marshaler interface. 22 | func (e Extension) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { 23 | // create a temporary element from a wrapper Extension, copy what we need to 24 | // it and return it's encoding. 25 | var e2 interface{} 26 | // if we have custom trackers, we should ignore the data, if not, then we 27 | // should consider only the data. 28 | if len(e.CustomTracking) > 0 { 29 | e2 = extension{Type: e.Type, CustomTracking: e.CustomTracking} 30 | } else { 31 | e2 = extensionNoCT{Type: e.Type, Data: e.Data} 32 | } 33 | 34 | return enc.EncodeElement(e2, start) 35 | } 36 | 37 | // UnmarshalXML implements xml.Unmarshaler interface. 38 | func (e *Extension) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error { 39 | // decode the extension into a temporary element from a wrapper Extension, 40 | // copy what we need over. 41 | var e2 extension 42 | if err := dec.DecodeElement(&e2, &start); err != nil { 43 | return err 44 | } 45 | // copy the type and the customTracking 46 | e.Type = e2.Type 47 | e.CustomTracking = e2.CustomTracking 48 | // copy the data only of customTracking is empty 49 | if len(e.CustomTracking) == 0 { 50 | e.Data = e2.Data 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /extension_test.go: -------------------------------------------------------------------------------- 1 | package vast 2 | 3 | import ( 4 | "encoding/xml" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var ( 11 | extensionCustomTracking = []byte(``) 12 | extensionData = []byte(`Generic`) 13 | ) 14 | 15 | func TestExtensionCustomTrackingMarshal(t *testing.T) { 16 | e := Extension{ 17 | Type: "testCustomTracking", 18 | CustomTracking: []Tracking{ 19 | { 20 | Event: "event.1", 21 | URI: "http://event.1", 22 | }, 23 | { 24 | Event: "event.2", 25 | URI: "http://event.2", 26 | }, 27 | }, 28 | } 29 | 30 | // marshal the extension 31 | xmlExtensionOutput, err := xml.Marshal(e) 32 | assert.NoError(t, err) 33 | 34 | // assert the resulting marshaled extension 35 | assert.Equal(t, string(extensionCustomTracking), string(xmlExtensionOutput)) 36 | } 37 | 38 | func TestExtensionCustomTracking(t *testing.T) { 39 | // unmarshal the Extension 40 | var e Extension 41 | assert.NoError(t, xml.Unmarshal(extensionCustomTracking, &e)) 42 | 43 | // assert the resulting extension 44 | assert.Equal(t, "testCustomTracking", e.Type) 45 | assert.Empty(t, string(e.Data)) 46 | if assert.Len(t, e.CustomTracking, 2) { 47 | // first event 48 | assert.Equal(t, "event.1", e.CustomTracking[0].Event) 49 | assert.Equal(t, "http://event.1", e.CustomTracking[0].URI) 50 | // second event 51 | assert.Equal(t, "event.2", e.CustomTracking[1].Event) 52 | assert.Equal(t, "http://event.2", e.CustomTracking[1].URI) 53 | } 54 | 55 | // marshal the extension 56 | xmlExtensionOutput, err := xml.Marshal(e) 57 | assert.NoError(t, err) 58 | 59 | // assert the resulting marshaled extension 60 | assert.Equal(t, string(extensionCustomTracking), string(xmlExtensionOutput)) 61 | } 62 | 63 | func TestExtensionGeneric(t *testing.T) { 64 | // unmarshal the Extension 65 | var e Extension 66 | assert.NoError(t, xml.Unmarshal(extensionData, &e)) 67 | 68 | // assert the resulting extension 69 | assert.Equal(t, "testCustomTracking", e.Type) 70 | assert.Equal(t, "Generic", string(e.Data)) 71 | assert.Empty(t, e.CustomTracking) 72 | 73 | // marshal the extension 74 | xmlExtensionOutput, err := xml.Marshal(e) 75 | assert.NoError(t, err) 76 | 77 | // assert the resulting marshaled extension 78 | assert.Equal(t, string(extensionData), string(xmlExtensionOutput)) 79 | } 80 | -------------------------------------------------------------------------------- /offset.go: -------------------------------------------------------------------------------- 1 | package vast 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // Offset represents either a vast.Duration or a percentage of the video duration. 10 | type Offset struct { 11 | // If not nil, the Offset is duration based 12 | Duration *Duration 13 | // If Duration is nil, the Offset is percent based 14 | Percent float32 15 | } 16 | 17 | // MarshalText implements the encoding.TextMarshaler interface. 18 | func (o Offset) MarshalText() ([]byte, error) { 19 | if o.Duration != nil { 20 | return o.Duration.MarshalText() 21 | } 22 | return []byte(fmt.Sprintf("%d%%", int(o.Percent*100))), nil 23 | } 24 | 25 | // UnmarshalText implements the encoding.TextUnmarshaler interface. 26 | func (o *Offset) UnmarshalText(data []byte) error { 27 | if strings.HasSuffix(string(data), "%") { 28 | p, err := strconv.ParseInt(string(data[:len(data)-1]), 10, 8) 29 | if err != nil { 30 | return fmt.Errorf("invalid offset: %s", data) 31 | } 32 | o.Percent = float32(p) / 100 33 | return nil 34 | } 35 | var d Duration 36 | o.Duration = &d 37 | return o.Duration.UnmarshalText(data) 38 | } 39 | -------------------------------------------------------------------------------- /offset_test.go: -------------------------------------------------------------------------------- 1 | package vast 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestOffsetMarshaler(t *testing.T) { 10 | b, err := Offset{}.MarshalText() 11 | if assert.NoError(t, err) { 12 | assert.Equal(t, "0%", string(b)) 13 | } 14 | b, err = Offset{Percent: .1}.MarshalText() 15 | if assert.NoError(t, err) { 16 | assert.Equal(t, "10%", string(b)) 17 | } 18 | d := Duration(0) 19 | b, err = Offset{Duration: &d}.MarshalText() 20 | if assert.NoError(t, err) { 21 | assert.Equal(t, "00:00:00", string(b)) 22 | } 23 | } 24 | 25 | func TestOffsetUnmarshaler(t *testing.T) { 26 | var o Offset 27 | if assert.NoError(t, o.UnmarshalText([]byte("0%"))) { 28 | assert.Nil(t, o.Duration) 29 | assert.Equal(t, float32(0.0), o.Percent) 30 | } 31 | o = Offset{} 32 | if assert.NoError(t, o.UnmarshalText([]byte("10%"))) { 33 | assert.Nil(t, o.Duration) 34 | assert.Equal(t, float32(0.1), o.Percent) 35 | } 36 | o = Offset{} 37 | if assert.NoError(t, o.UnmarshalText([]byte("00:00:00"))) { 38 | if assert.NotNil(t, o.Duration) { 39 | assert.Equal(t, Duration(0), *o.Duration) 40 | } 41 | assert.Equal(t, float32(0), o.Percent) 42 | } 43 | o = Offset{} 44 | assert.EqualError(t, o.UnmarshalText([]byte("abc%")), "invalid offset: abc%") 45 | } 46 | -------------------------------------------------------------------------------- /testdata/creative_extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | US 10 | 3 11 | 1680 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Generic 21 | 22 | 23 | MubmWKCWLs_tiQPYiYrwBw 24 | CIGpsPCTkdMCFdN-Ygod-xkCKQ 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /testdata/extraspaces_vpaid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SpotXchange 6 | 7 | 8 | 9 | 10 | 11 | 00:00:16 12 | 13 | ]]> 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /testdata/inline_extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | US 8 | 3 9 | 1680 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Generic 19 | 20 | 21 | MubmWKCWLs_tiQPYiYrwBw 22 | CIGpsPCTkdMCFdN-Ygod-xkCKQ 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /testdata/liverail-vast2-linear-companion.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | LiveRail 5 | 6 | 7 | 8 | 9 | 10 | 00:00:11 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /testdata/liverail-vast2-nonlinear.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | LiveRail 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /testdata/spotx_adparameters.txt: -------------------------------------------------------------------------------- 1 | {"ad_id":"1130507-1818483","title":"IntegralAds_VAST_2_0_Ad_Wrapper","page_url":"","media":{"ad_source":"rtb","syn":{"video_uri_required":1},"tracking":{"video_valid_first_frame":1,"beacon":[{"type":"skip","beacon_url":"https:\\/\\/search.spotxchange.com\\/beacon?_a=137530&_p=spotx&_z=1&_m=eNpVz99vgjAQB%2FD%2BLTyP0rM%2FLCZ7WOKWbFF5GJr4RAotUBUwgkHZ9rdvVB8W%2B3BJv3eXT45hQhBCwAnhEkKQKA2lZCHLhJYSwExTSgSkqcpBCEIIIKBTTglaf6JQ4glleAISAxOo7LpjOwuCvu9xrip7uJaq1tdK1ThrqgB9eefWnJLibLU385hRoTQ8FylPnSNzMgUtc6EpdY73dB%2B3bVKbflwYg6xp9tYkuqmUrceoNeqUlbg9Nt0lG7HCOGkcVGdtTZ2Z5IHyneU7zHea%2F8D93A6bcPR7e2i1XizuBW4R4oJcKPDx%2B99y5TuKD%2BU23rPlrhiW849dFG%2FstnqrVsPLJYrfYTm88mi%2BZ6uheP4DQppvcA%3D%3D&_l=eNplj01rwkAYhN%2FfsleLvJv9yEbooUXoxU1BDKW5yGazNUk1SZMNtVr%2Fe9VgofQyh5mHGYYqqrhiEEYykIyDABWgAi4wlAAUKKdcRIpD3zZ%2BD5MJSBpB3dQOEI6EkdmRZGVOZgSniEySO9Kar2bwNwcvVt%2B6%2BpeJrtCQbUu7dntbmHrj1m%2FOXXOKiOfYDp3xLl%2FvTPfufLs11v1n8cKeThAni8UoCDhVAparR%2FDdsGuLxrttP7XNDlBxRjORW5pxhRYjIYULc8nDyDLm8vH3%2BRxy%2BBhM7a3p%2FdhKWSgC8XcGvuNK8%2FQp2b%2BuiiKdb1AfNNfVw6eukkBXcfk8T4L0ZVnqQ3L%2FAyjAYKo%3D&_t=eNotjFFvwiAUhfkt99ksQDtQjA9Llix7qMsSF1NfGkpRmW0hhS1W538fFe%2FLuefLOafWUtkeLfaYS8wb1sznhGheZ5iTupZ7whjGmCB1lKZHdjjI3ihUp9YV0lOF0WkQ4E%2FGwQxcK8dguokQFr3XclDHyjutQFzByUF2OujBT67Rv0bp6dPnMIl3NpyrhCvp3GOc3GbQ2Ua3IOgMbOzSWzyEyBPGSAourl4wAYfW1rKFpRF4eQfO%2BpAAWRAaWSbAdHICWc45ziPKBSgTxkeIpOKgD8b2E2NkERGPIfvTh%2BGeozRLW03aYpQsb%2Biv2KzN7q3M19%2Flc7H9xFEv5eZrLLbv9GNzGnev5VhcXki5LVf%2F4KN2Bg%3D%3D&_b=eNozY%2FAL9fFBI2r8qiINo8IjDaJyA038wqOy%2FIz8Mv1CQqt83T3L%2FV0iDXyrUrJ9jbwyokJCbQF2oxP%2F&beacon_type=skip"},{"type":"exception","beacon_url":"https:\\/\\/search.spotxchange.com\\/exception?_a=137530&_p=spotx&_z=1&_m=eNpVz99vgjAQB%2FD%2BLTyP0rM%2FLCZ7WOKWbFF5GJr4RAotUBUwgkHZ9rdvVB8W%2B3BJv3eXT45hQhBCwAnhEkKQKA2lZCHLhJYSwExTSgSkqcpBCEIIIKBTTglaf6JQ4glleAISAxOo7LpjOwuCvu9xrip7uJaq1tdK1ThrqgB9eefWnJLibLU385hRoTQ8FylPnSNzMgUtc6EpdY73dB%2B3bVKbflwYg6xp9tYkuqmUrceoNeqUlbg9Nt0lG7HCOGkcVGdtTZ2Z5IHyneU7zHea%2F8D93A6bcPR7e2i1XizuBW4R4oJcKPDx%2B99y5TuKD%2BU23rPlrhiW849dFG%2FstnqrVsPLJYrfYTm88mi%2BZ6uheP4DQppvcA%3D%3D&_l=eNplj01rwkAYhN%2FfsleLvJv9yEbooUXoxU1BDKW5yGazNUk1SZMNtVr%2Fe9VgofQyh5mHGYYqqrhiEEYykIyDABWgAi4wlAAUKKdcRIpD3zZ%2BD5MJSBpB3dQOEI6EkdmRZGVOZgSniEySO9Kar2bwNwcvVt%2B6%2BpeJrtCQbUu7dntbmHrj1m%2FOXXOKiOfYDp3xLl%2FvTPfufLs11v1n8cKeThAni8UoCDhVAparR%2FDdsGuLxrttP7XNDlBxRjORW5pxhRYjIYULc8nDyDLm8vH3%2BRxy%2BBhM7a3p%2FdhKWSgC8XcGvuNK8%2FQp2b%2BuiiKdb1AfNNfVw6eukkBXcfk8T4L0ZVnqQ3L%2FAyjAYKo%3D&_t=eNpdUGFr2zAQ9W%2FR58SRXEgyQxkZXT%2BMOqVsI2QEhCydY2WypEly5iztf985CR3svhz33r17dweDBJ%2B0s9mHhi4EXai5Wi4Zg0V9RxesrkXD5nNKKctkK7TNXNgLq2UG78IzqUFIZ3k6eSAleWfIhHgjTkl3I8zmWB%2B1AsePwmjFGx1i4k0QVxpZoXg8xQQdKc8kpHpM0miwiUcUkpJOSB8hYJeVvA8GdW1KPpa72W7m9QAm%2F9ULm7DnCLl03Q3FNJ2uNqtPqmi%2Fto%2BPXb7XzUetOpFke0%2FRugFAc9cHOS5jdEza7hH%2FZ3Lx6OP0N8SUR%2B%2FSkOOK%2F%2FmVS7qku9mFRnWULVyucx4sdvOCF%2BTtDQkQQbY8epDjkV6MX0gQ4lgpOOpxjTO%2BMo3pMo5fYS68v33a9sbgrM4pMNdqQly84RhZxnJKs%2FX3p6fstSqq4XmzZT82L3%2Bqw%2Br0%2FFDR7UF11beXu3VRserweai6Lz%2FXD9v7vx8btes%3D&_b=eNozZfAL9fFBI2r8QzxNolwiK%2FxDkkF0VZS7W5ZfladJZJavkV94VK6%2Fi6exX4hTpr97qC0AcfcToA%3D%3D&beacon_type=exception&exception%5Bid%5D=%24EXCEPTION_ID&exception%5Bdata%5D=%24EXCEPTION_DATA&exception%5Btrace%5D=%24EXCEPTION_TRACE&syn%5Btiming%5D=%24TIMING_DATA"},{"type":"thirdQuartile","beacon_url":"https:\\/\\/search.spotxchange.com\\/beacon?_a=137530&_p=spotx&_z=1&_m=eNpFizsOwjAQBfdEaBd7HbugIFKgiY0QThF3mI9CTPoQcXbAgYIpRho9PblABABiRNZkSEM0WksjT%2BqsNdGliAIVxXi8klKISECiYIHQHGA%2B5lgyvL6Aa%2Br6J4J3BljhKIhz%2FqdZTzeVXfAVu6EVdtok65MIvry1fZdCvx9323C3fv2wQ7X6ANPiL68%3D&_l=eNpljl1rwjAUhs9vya1DTpomTYXdDMcQtKWgSHtTYsycH02rTZnV%2Bd%2FXKhuMnYv34nmfFw6VVPqSQRAKTzAfOEgPJfgcAwFAAaGuSneGwQAEDcGW1nTsShgZXclquyYjgkNEJsgTqVRbNu6HYI%2FqythfJ7xLzeqw1bk56w9lNyZ%2FN%2BbeU0Tsat2clDPrvFCnvXHVQWnz38Xevd0gWkynj0DAoeTQHfZ%2Fog%2FHRlmnVe0eAmUB9%2FjfBXzNdulntNvQeP6K2Vu2j8cvRVSkl6yYtelywuJxwrJl0sbz5PkbhgZPGQ%3D%3D&_t=eNpdUO9rwjAQ7d%2BSz1rTytQVxnBsftKOMUUcQkiTqw1LkyxJ%2FTHn%2F77UCoPly%2BXdu3fv7gqgTKvovsRjisd8xCeTJIFxMcTjpChomYxGGOMkYhUVKtJ2R5VgUdGpzqj7EH8ygDJkgTXWCrVDPWQkPXlRt%2BlkFPBecNBkT6XgpBTWeVJa2tGBpZy4k%2FNQo%2ByMrC%2FawKQA5YkLQpThHmoc2FClGGmsDLrKe%2BOy7WA7MOIIMv5qqPKhZg8x0%2FUtG0K%2FP11Pn3havVezWR3vRPkoeE09qx5wsC4BgrluLGuHkcL5boE%2Fk6tH4%2FoHcD52RvtjHEb855dN8ARvB1c6qB2r4LqdNqBCNUlJii6XQAC1rCLOAGuXNLS9ggfrWsRhL9oxzgiOvg3XdqRLE2rM7dKqkTL0qjUH2aEe0u6WDy%2BKkhjjKF%2FN59HPon5JFsvNYbOc3uXp4jt%2F3g039cdnvn45vT6%2FDfN0dfhYrg4BP%2FwCbHyzFQ%3D%3D&_b=eNozYfAL9fFBI2r8w12NI3N9TfyyHE39QzKyorIcDfxcko19qxxNfF2CsiOznDL9XPyyfatCbQFnrxOe&beacon_type=recurring&view_percent=75"},{"type":"midpoint","beacon_url":"https:\\/\\/search.spotxchange.com\\/beacon?_a=137530&_p=spotx&_z=1&_m=eNpFizsOwjAQBfdEaBd7HbugIFKgiY0QThF3mI9CTPoQcXbAgYIpRho9PblABABiRNZkSEM0WksjT%2BqsNdGliAIVxXi8klKISECiYIHQHGA%2B5lgyvL6Aa%2Br6J4J3BljhKIhz%2FqdZTzeVXfAVu6EVdtok65MIvry1fZdCvx9323C3fv2wQ7X6ANPiL68%3D&_l=eNpljl1rwjAUhs9vya1DTpomTYXdDMcQtKWgSHtTYsycH02rTZnV%2Bd%2FXKhuMnYv34nmfFw6VVPqSQRAKTzAfOEgPJfgcAwFAAaGuSneGwQAEDcGW1nTsShgZXclquyYjgkNEJsgTqVRbNu6HYI%2FqythfJ7xLzeqw1bk56w9lNyZ%2FN%2BbeU0Tsat2clDPrvFCnvXHVQWnz38Xevd0gWkynj0DAoeTQHfZ%2Fog%2FHRlmnVe0eAmUB9%2FjfBXzNdulntNvQeP6K2Vu2j8cvRVSkl6yYtelywuJxwrJl0sbz5PkbhgZPGQ%3D%3D&_t=eNpdUO9rwjAQ7d%2BSz1rTytQVxnBsftKOMUUcQkiTqw1LkyxJ%2FTHn%2F77UCoPly%2BXdu3fv7gqgTKvovsRjisd8xCeTJIFxMcTjpChomYxGGOMkYhUVKtJ2R5VgUdGpzqj7EH8ygDJkgTXWCrVDPWQkPXlRt%2BlkFPBecNBkT6XgpBTWeVJa2tGBpZy4k%2FNQo%2ByMrC%2FawKQA5YkLQpThHmoc2FClGGmsDLrKe%2BOy7WA7MOIIMv5qqPKhZg8x0%2FUtG0K%2FP11Pn3havVezWR3vRPkoeE09qx5wsC4BgrluLGuHkcL5boE%2Fk6tH4%2FoHcD52RvtjHEb855dN8ARvB1c6qB2r4LqdNqBCNUlJii6XQAC1rCLOAGuXNLS9ggfrWsRhL9oxzgiOvg3XdqRLE2rM7dKqkTL0qjUH2aEe0u6WDy%2BKkhjjKF%2FN59HPon5JFsvNYbOc3uXp4jt%2F3g039cdnvn45vT6%2FDfN0dfhYrg4BP%2FwCbHyzFQ%3D%3D&_b=eNozZvAL9fFBI2r8srJN%2FENcjf1cnHIjQ6Jyo3J9q6LC%2FbJ9XVwrfKu8cn1dQg0ic%2F2AMNQWAHwnFGs%3D&beacon_type=recurring&view_percent=50"},{"type":"firstQuartile","beacon_url":"https:\\/\\/search.spotxchange.com\\/beacon?_a=137530&_p=spotx&_z=1&_m=eNpFizsOwjAQBfdEaBd7HbugIFKgiY0QThF3mI9CTPoQcXbAgYIpRho9PblABABiRNZkSEM0WksjT%2BqsNdGliAIVxXi8klKISECiYIHQHGA%2B5lgyvL6Aa%2Br6J4J3BljhKIhz%2FqdZTzeVXfAVu6EVdtok65MIvry1fZdCvx9323C3fv2wQ7X6ANPiL68%3D&_l=eNpljl1rwjAUhs9vya1DTpomTYXdDMcQtKWgSHtTYsycH02rTZnV%2Bd%2FXKhuMnYv34nmfFw6VVPqSQRAKTzAfOEgPJfgcAwFAAaGuSneGwQAEDcGW1nTsShgZXclquyYjgkNEJsgTqVRbNu6HYI%2FqythfJ7xLzeqw1bk56w9lNyZ%2FN%2BbeU0Tsat2clDPrvFCnvXHVQWnz38Xevd0gWkynj0DAoeTQHfZ%2Fog%2FHRlmnVe0eAmUB9%2FjfBXzNdulntNvQeP6K2Vu2j8cvRVSkl6yYtelywuJxwrJl0sbz5PkbhgZPGQ%3D%3D&_t=eNpdUO9rwjAQ7d%2BSz1rTytQVxnBsftKOMUUcQkiTqw1LkyxJ%2FTHn%2F77UCoPly%2BXdu3fv7gqgTKvovsRjisd8xCeTJIFxMcTjpChomYxGGOMkYhUVKtJ2R5VgUdGpzqj7EH8ygDJkgTXWCrVDPWQkPXlRt%2BlkFPBecNBkT6XgpBTWeVJa2tGBpZy4k%2FNQo%2ByMrC%2FawKQA5YkLQpThHmoc2FClGGmsDLrKe%2BOy7WA7MOIIMv5qqPKhZg8x0%2FUtG0K%2FP11Pn3havVezWR3vRPkoeE09qx5wsC4BgrluLGuHkcL5boE%2Fk6tH4%2FoHcD52RvtjHEb855dN8ARvB1c6qB2r4LqdNqBCNUlJii6XQAC1rCLOAGuXNLS9ggfrWsRhL9oxzgiOvg3XdqRLE2rM7dKqkTL0qjUH2aEe0u6WDy%2BKkhjjKF%2FN59HPon5JFsvNYbOc3uXp4jt%2F3g039cdnvn45vT6%2FDfN0dfhYrg4BP%2FwCbHyzFQ%3D%3D&_b=eNozYvAL9fFBI2p8qxwNIrOyK6JCUnL8XUIr%2FEIyMiOz0iv83X0rfLOyTX2rko38XVwrIqsibQGT7hUz&beacon_type=recurring&view_percent=25"},{"type":"complete","beacon_url":"https:\\/\\/search.spotxchange.com\\/beacon?_a=137530&_p=spotx&_z=1&_m=eNpFizsOwjAQBfdEaBd7HbugIFKgiY0QThF3mI9CTPoQcXbAgYIpRho9PblABABiRNZkSEM0WksjT%2BqsNdGliAIVxXi8klKISECiYIHQHGA%2B5lgyvL6Aa%2Br6J4J3BljhKIhz%2FqdZTzeVXfAVu6EVdtok65MIvry1fZdCvx9323C3fv2wQ7X6ANPiL68%3D&_l=eNpljl1rwjAUhs9vya1DTpomTYXdDMcQtKWgSHtTYsycH02rTZnV%2Bd%2FXKhuMnYv34nmfFw6VVPqSQRAKTzAfOEgPJfgcAwFAAaGuSneGwQAEDcGW1nTsShgZXclquyYjgkNEJsgTqVRbNu6HYI%2FqythfJ7xLzeqw1bk56w9lNyZ%2FN%2BbeU0Tsat2clDPrvFCnvXHVQWnz38Xevd0gWkynj0DAoeTQHfZ%2Fog%2FHRlmnVe0eAmUB9%2FjfBXzNdulntNvQeP6K2Vu2j8cvRVSkl6yYtelywuJxwrJl0sbz5PkbhgZPGQ%3D%3D&_t=eNpdUNuK2zAQ9bfoOXHktLnUsJSUdEtg66U0y7IhIGRpHKvVrZKcJpvuv3ccBwrVy2jOzJlzZmrgwtnsQ0MXnC7kXC6XRQGL%2Bh1dFHXNm2I%2Bp5QWmWi5spkLB26VyOqBdSHDh6WzB1IS4YzXkICMiNf8nJTp0WKO%2BVFJcOzItZKsUSEm1gQ%2BlLHKJYvnmMCQ8kJCqvsgtAKbWEQiKemIdBECdlnBuqCR16bkY7mf7CdenUDnvzpuE%2FYcIUcfNxTDeLx6Xn2S0%2FZ7e39v8oNqPippeBLtHUXpBgDFXRdEb0armJQ9IP5P5KrRxfFviCmP3qVTjhb%2F0yuXdEn3k2sZ2VG0cN3OebDYzaZsSt7esAA8iJZFD6Jf0vP%2BCglC7DMJR9XbuBA4pT5cx7EBZtz726FtpzXOMk6CHrIRcfGG48uyIqc0q54eHrI%2F1fOGvmzFbGcqszOb8%2B7Hz%2FfVl11bbVevX9eb2eP62%2FRx%2FXlWbTd3fwGJWrJT&_b=eNozZPAL9fFBI2ois8Iy%2FN2DcvyqPCsjq7KN%2FKqSjaNy3TKjXMIy%2FLKyTfyyXMsjszyNfLN8bQGSHxTj&beacon_type=complete"},{"type":"impression","beacon_url":"https:\\/\\/search.spotxchange.com\\/beacon?_a=137530&_p=spotx&_z=1&_m=eNpVz99vgjAQB%2FD%2BLTyP0rM%2FLCZ7WOKWbFF5GJr4RAotUBUwgkHZ9rdvVB8W%2B3BJv3eXT45hQhBCwAnhEkKQKA2lZCHLhJYSwExTSgSkqcpBCEIIIKBTTglaf6JQ4glleAISAxOo7LpjOwuCvu9xrip7uJaq1tdK1ThrqgB9eefWnJLibLU385hRoTQ8FylPnSNzMgUtc6EpdY73dB%2B3bVKbflwYg6xp9tYkuqmUrceoNeqUlbg9Nt0lG7HCOGkcVGdtTZ2Z5IHyneU7zHea%2F8D93A6bcPR7e2i1XizuBW4R4oJcKPDx%2B99y5TuKD%2BU23rPlrhiW849dFG%2FstnqrVsPLJYrfYTm88mi%2BZ6uheP4DQppvcA%3D%3D&_l=eNplj01rwkAYhN%2FfsleLvJv9yEbooUXoxU1BDKW5yGazNUk1SZMNtVr%2Fe9VgofQyh5mHGYYqqrhiEEYykIyDABWgAi4wlAAUKKdcRIpD3zZ%2BD5MJSBpB3dQOEI6EkdmRZGVOZgSniEySO9Kar2bwNwcvVt%2B6%2BpeJrtCQbUu7dntbmHrj1m%2FOXXOKiOfYDp3xLl%2FvTPfufLs11v1n8cKeThAni8UoCDhVAparR%2FDdsGuLxrttP7XNDlBxRjORW5pxhRYjIYULc8nDyDLm8vH3%2BRxy%2BBhM7a3p%2FdhKWSgC8XcGvuNK8%2FQp2b%2BuiiKdb1AfNNfVw6eukkBXcfk8T4L0ZVnqQ3L%2FAyjAYKo%3D&_t=eNpdkVGP4iAQx%2FtZeNYKrWkVs7l42fim%2B3Cb22hMCKW0xWsLB9S15%2Fndd9qaXHK8DPzm%2F58ZIJNc6DZYFzjlOM2TfLUiRKZZjFOSZbwgSYIxJoGouGoDbUveKhFkk%2BuOpg3zvZGIIue59WiGTM17r5oBkQTOV5VLza68VjkrlHWeFZZPacjynLneedkgekfWZ0MQtZKtZw6MiOIZ6py0oGoF62wNvsp74%2Bh5cV4YdZN1%2BLvjrQfNVYZCN08KYT7ffmy%2F51H1o9rtmrBUxTeVN9yL6gVD60JKaK47K4ZhauW8akvg%2F5qMPTo3%2F5TOh85ofwthxP%2F60RVe4fNiTIPbiUqOt9NGtqBmEYvQ4wEJya2omDNSDJc0fHgFL60bTrm8qmGMO5I3P4SxHJsw48Y8X5lAoUbnEiaMZkiDN3rACgISYhxwmtK7owlFZa0zXqONongzAqPhf0ZA1iQCFlOkGj6AeJmmeAloSZFQvn%2BKyGS0slS6HVhC1oBSEOmu9XbURVE81cqnWklENo%2Fg7%2BlSxoePn5dTc%2BwP7%2FvPw%2BVQnS7H%2Fu11V%2B%2BbIzm9%2F%2Frz9rqN95f9yxciMdN%2F&_b=eNozYPAL9fFBI2r8qiINfbNCDSOr%2FHJ8s7IrI41CTXyNIk0jQ5LLI7MCjf3cA6v8w8OyfXMDbQF9gxRO&beacon_type=start&aid=4ea98e5f-6b5b-11e7-8f07-1d8f6d330001&syn%5Btiming%5D=%24TIMING_DATA"},{"type":"click","beacon_url":"https:\\/\\/search.spotxchange.com\\/click?_a=137530&_p=spotx&_z=1&_m=eNpVz99vgjAQB%2FD%2BLTyP0rM%2FLCZ7WOKWbFF5GJr4RAotUBUwgkHZ9rdvVB8W%2B3BJv3eXT45hQhBCwAnhEkKQKA2lZCHLhJYSwExTSgSkqcpBCEIIIKBTTglaf6JQ4glleAISAxOo7LpjOwuCvu9xrip7uJaq1tdK1ThrqgB9eefWnJLibLU385hRoTQ8FylPnSNzMgUtc6EpdY73dB%2B3bVKbflwYg6xp9tYkuqmUrceoNeqUlbg9Nt0lG7HCOGkcVGdtTZ2Z5IHyneU7zHea%2F8D93A6bcPR7e2i1XizuBW4R4oJcKPDx%2B99y5TuKD%2BU23rPlrhiW849dFG%2FstnqrVsPLJYrfYTm88mi%2BZ6uheP4DQppvcA%3D%3D&_l=eNplj01rwkAYhN%2FfsleLvJv9yEbooUXoxU1BDKW5yGazNUk1SZMNtVr%2Fe9VgofQyh5mHGYYqqrhiEEYykIyDABWgAi4wlAAUKKdcRIpD3zZ%2BD5MJSBpB3dQOEI6EkdmRZGVOZgSniEySO9Kar2bwNwcvVt%2B6%2BpeJrtCQbUu7dntbmHrj1m%2FOXXOKiOfYDp3xLl%2FvTPfufLs11v1n8cKeThAni8UoCDhVAparR%2FDdsGuLxrttP7XNDlBxRjORW5pxhRYjIYULc8nDyDLm8vH3%2BRxy%2BBhM7a3p%2FdhKWSgC8XcGvuNK8%2FQp2b%2BuiiKdb1AfNNfVw6eukkBXcfk8T4L0ZVnqQ3L%2FAyjAYKo%3D&_t=eNotkFtv3CAQhf1beI5awBs7y6oPlaq%2BRdVKvchpK2sMrD2JMRaw9%2B5%2F77AOL8z5dM7MgB5RvxXrHa%2BB16YyT09C2LoreS26DnaiqjjnotAD4FT40MOEujigsb7V9%2BiVOWsQWjRMMfbAdj44SEudAkxRe4NTz9RvNvojwWzfOyoG7Af2lwA626bzbCkE80xdIaGf%2Fnx8hQNEHXBO5J5HOCdykklUeQ6ONuLFLpM6pFkpC07qiCYNVJc8q8HSoLyRfMwSYxtTsODafRiXdLQQ9NDG2WqmrmyGAM4mG2JWxh5Q21zZU8pXnH06tQtuaeH33cWNnuKNpZ7ygXnKyhudohAfOC9A1eoaVaVYP%2FoORrZBxTd3MPuYFiDWQhIrFUMHGZSruuYrQivFNKbzu0kswWB7%2BqfMKrEmVJPJ76cU7j4py6WXWXpVUmxuxb9GbmXzvZeN2x5fXj%2Bfnn99fXtx28dnt119%2B%2FLTNZembNyPC7FP%2FwHQgKlo&_b=eNozYGDwC%2FXxYbBMMzQyMrQ0MEuxsDA0TDVPMjYwN0xKSkwzNDNzAwKwqhrfkOzyyPCwXN9cV1PfXL8sX5eMXD%2F3wErfKk%2FTKBe%2FrMiQlIyokGRTPxdPWwBB6Ro5"},{"beacon_url":"https:\\/\\/exch.quantserve.com\\/pixel\\/p-e_yCqWnB93CQF.gif?media=ad&p=0.978&r=&rand=86985&labels=_qc.imp,_imp.adserver.rtb,_qc.vast,_imp.qccampaign.556888,_imp.flight.409580,_imp.lineitem.354317&rtbip=192.184.68.197&rtbdata2=EBYaHlRoZV9UcnVtcF9Pcmdhbml6YXRpb25fUTNfMjAxNyDY_iEojdAVMIuARToeaHR0cHM6Ly93d3cuZmFtaWx5aGFuZHltYW4uY29tWig3UE4yak9feUl0RDByaUhkN0t4dDN1SHlkZHIwcTNiZjY2OGgyQnd4dQlqrD-AAanX4-0PoAEBqAHxrsQDugEkNGVhOThlNWYtNmI1Yi0xMWU3LThmMDctMWQ4ZjZkMzMwMDAxwAGMkfcByAHQ7puN1ivaASBiOTg4NDk0YzZkODgxMWU3YjMwNjFiYmFmMTY2MDAwMeUBBjH5O-gBZJgC7P8YqAIGqAIFsAIIugIEwLhExcACAsgCANAC4Ojb04yY0-2ZAeACAA","type":"impression"},{"beacon_url":"https:\\/\\/exch.quantserve.com\\/pixel\\/p-e_yCqWnB93CQF.gif?media=ad&r=&rand=86985&labels=_qc.vast,_qc.event.start&rtbip=192.184.68.197&rtbdata2=EBYaHlRoZV9UcnVtcF9Pcmdhbml6YXRpb25fUTNfMjAxNyDY_iEojdAVMIuARToeaHR0cHM6Ly93d3cuZmFtaWx5aGFuZHltYW4uY29tWig3UE4yak9feUl0RDByaUhkN0t4dDN1SHlkZHIwcTNiZjY2OGgyQnd4dQlqrD-AAanX4-0PoAEBqAHxrsQDugEkNGVhOThlNWYtNmI1Yi0xMWU3LThmMDctMWQ4ZjZkMzMwMDAxwAGMkfcByAHQ7puN1ivaASBiOTg4NDk0YzZkODgxMWU3YjMwNjFiYmFmMTY2MDAwMeUBBjH5O-gBZJgC7P8YqAIGqAIFsAIIugIEwLhExcACAsgCANAC4Ojb04yY0-2ZAeACAA","type":"start"},{"beacon_url":"https:\\/\\/exch.quantserve.com\\/pixel\\/p-e_yCqWnB93CQF.gif?media=ad&r=&rand=86985&labels=_qc.vast,_qc.event.firstQuartile&rtbip=192.184.68.197&rtbdata2=EBYaHlRoZV9UcnVtcF9Pcmdhbml6YXRpb25fUTNfMjAxNyDY_iEojdAVMIuARToeaHR0cHM6Ly93d3cuZmFtaWx5aGFuZHltYW4uY29tWig3UE4yak9feUl0RDByaUhkN0t4dDN1SHlkZHIwcTNiZjY2OGgyQnd4dQlqrD-AAanX4-0PoAEBqAHxrsQDugEkNGVhOThlNWYtNmI1Yi0xMWU3LThmMDctMWQ4ZjZkMzMwMDAxwAGMkfcByAHQ7puN1ivaASBiOTg4NDk0YzZkODgxMWU3YjMwNjFiYmFmMTY2MDAwMeUBBjH5O-gBZJgC7P8YqAIGqAIFsAIIugIEwLhExcACAsgCANAC4Ojb04yY0-2ZAeACAA","type":"firstQuartile"},{"beacon_url":"https:\\/\\/exch.quantserve.com\\/pixel\\/p-e_yCqWnB93CQF.gif?media=ad&r=&rand=86985&labels=_qc.vast,_qc.event.midpoint&rtbip=192.184.68.197&rtbdata2=EBYaHlRoZV9UcnVtcF9Pcmdhbml6YXRpb25fUTNfMjAxNyDY_iEojdAVMIuARToeaHR0cHM6Ly93d3cuZmFtaWx5aGFuZHltYW4uY29tWig3UE4yak9feUl0RDByaUhkN0t4dDN1SHlkZHIwcTNiZjY2OGgyQnd4dQlqrD-AAanX4-0PoAEBqAHxrsQDugEkNGVhOThlNWYtNmI1Yi0xMWU3LThmMDctMWQ4ZjZkMzMwMDAxwAGMkfcByAHQ7puN1ivaASBiOTg4NDk0YzZkODgxMWU3YjMwNjFiYmFmMTY2MDAwMeUBBjH5O-gBZJgC7P8YqAIGqAIFsAIIugIEwLhExcACAsgCANAC4Ojb04yY0-2ZAeACAA","type":"midpoint"},{"beacon_url":"https:\\/\\/exch.quantserve.com\\/pixel\\/p-e_yCqWnB93CQF.gif?media=ad&r=&rand=86985&labels=_qc.vast,_qc.event.thirdQuartile&rtbip=192.184.68.197&rtbdata2=EBYaHlRoZV9UcnVtcF9Pcmdhbml6YXRpb25fUTNfMjAxNyDY_iEojdAVMIuARToeaHR0cHM6Ly93d3cuZmFtaWx5aGFuZHltYW4uY29tWig3UE4yak9feUl0RDByaUhkN0t4dDN1SHlkZHIwcTNiZjY2OGgyQnd4dQlqrD-AAanX4-0PoAEBqAHxrsQDugEkNGVhOThlNWYtNmI1Yi0xMWU3LThmMDctMWQ4ZjZkMzMwMDAxwAGMkfcByAHQ7puN1ivaASBiOTg4NDk0YzZkODgxMWU3YjMwNjFiYmFmMTY2MDAwMeUBBjH5O-gBZJgC7P8YqAIGqAIFsAIIugIEwLhExcACAsgCANAC4Ojb04yY0-2ZAeACAA","type":"thirdQuartile"},{"beacon_url":"https:\\/\\/exch.quantserve.com\\/pixel\\/p-e_yCqWnB93CQF.gif?media=ad&r=&rand=86985&labels=_qc.vast,_qc.event.complete&rtbip=192.184.68.197&rtbdata2=EBYaHlRoZV9UcnVtcF9Pcmdhbml6YXRpb25fUTNfMjAxNyDY_iEojdAVMIuARToeaHR0cHM6Ly93d3cuZmFtaWx5aGFuZHltYW4uY29tWig3UE4yak9feUl0RDByaUhkN0t4dDN1SHlkZHIwcTNiZjY2OGgyQnd4dQlqrD-AAanX4-0PoAEBqAHxrsQDugEkNGVhOThlNWYtNmI1Yi0xMWU3LThmMDctMWQ4ZjZkMzMwMDAxwAGMkfcByAHQ7puN1ivaASBiOTg4NDk0YzZkODgxMWU3YjMwNjFiYmFmMTY2MDAwMeUBBjH5O-gBZJgC7P8YqAIGqAIFsAIIugIEwLhExcACAsgCANAC4Ojb04yY0-2ZAeACAA","type":"complete"},{"beacon_url":"https:\\/\\/exch.quantserve.com\\/pixel\\/p-e_yCqWnB93CQF.gif?media=ad&r=&rand=86985&labels=_qc.vast,_qc.event.close&rtbip=192.184.68.197&rtbdata2=EBYaHlRoZV9UcnVtcF9Pcmdhbml6YXRpb25fUTNfMjAxNyDY_iEojdAVMIuARToeaHR0cHM6Ly93d3cuZmFtaWx5aGFuZHltYW4uY29tWig3UE4yak9feUl0RDByaUhkN0t4dDN1SHlkZHIwcTNiZjY2OGgyQnd4dQlqrD-AAanX4-0PoAEBqAHxrsQDugEkNGVhOThlNWYtNmI1Yi0xMWU3LThmMDctMWQ4ZjZkMzMwMDAxwAGMkfcByAHQ7puN1ivaASBiOTg4NDk0YzZkODgxMWU3YjMwNjFiYmFmMTY2MDAwMeUBBjH5O-gBZJgC7P8YqAIGqAIFsAIIugIEwLhExcACAsgCANAC4Ojb04yY0-2ZAeACAA","type":"close"},{"beacon_url":"https:\\/\\/exch.quantserve.com\\/pixel\\/p-e_yCqWnB93CQF.gif?media=ad&r=&rand=86985&labels=_qc.clk,_click.adserver.rtb,_qc.vast&rtbip=192.184.68.197&rtbdata2=EBYaHlRoZV9UcnVtcF9Pcmdhbml6YXRpb25fUTNfMjAxNyDY_iEojdAVMIuARToeaHR0cHM6Ly93d3cuZmFtaWx5aGFuZHltYW4uY29tWig3UE4yak9feUl0RDByaUhkN0t4dDN1SHlkZHIwcTNiZjY2OGgyQnd4dQlqrD-AAanX4-0PoAEBqAHxrsQDugEkNGVhOThlNWYtNmI1Yi0xMWU3LThmMDctMWQ4ZjZkMzMwMDAxwAGMkfcByAHQ7puN1ivaASBiOTg4NDk0YzZkODgxMWU3YjMwNjFiYmFmMTY2MDAwMeUBBjH5O-gBZJgC7P8YqAIGqAIFsAIIugIEwLhExcACAsgCANAC4Ojb04yY0-2ZAeACAA","type":"click"},{"type":"initialization","beacon_url":"https:\\/\\/pr-bh.ybp.yahoo.com\\/sync\\/spotx\\/4ea98e5f-6b5b-11e7-8f07-1d8f6d330001"},{"type":"initialization","beacon_url":"https:\\/\\/spotxbidder-east.extend.tv\\/r.gif"},{"type":"impression","beacon_url":"https:\\/\\/event.spotxchange.com\\/vast\\/impression?_x=WzE1MDA1ODE5MTgsImI5ODg0OTRjNmQ4ODExZTdiMzA2MWJiYWYxNjYwMDAxIiwiMTM3NTMwIiwic3BvdHgiLCI4MjA4IiwiMTQxNDU5ODQiLCIxMTMwNTA3IiwiNGVhOThlNWYtNmI1Yi0xMWU3LThmMDctMWQ4ZjZkMzMwMDAxIl0mc2V0ZWM9TkRCaFpUY3lPRFF6WVRSa016WTNOVFl3WWpZeFl6QmpZV1kxWkRkalpqYz0~","with_creds":false},{"type":"complete","beacon_url":"https:\\/\\/event.spotxchange.com\\/vast\\/complete?_x=WzE1MDA1ODE5MTgsImI5ODg0OTRjNmQ4ODExZTdiMzA2MWJiYWYxNjYwMDAxIiwiMTM3NTMwIiwic3BvdHgiLCI4MjA4IiwiMTQxNDU5ODQiLCIxMTMwNTA3Il0mc2V0ZWM9T0RNNU9UWmpNVE01Wm1RMU1URmhPVFU1Tm1FM016a3pPR1U1TURGbU9UVT0~","with_creds":false},{"type":"firstQuartile","beacon_url":"https:\\/\\/event.spotxchange.com\\/vast\\/25?_x=WzE1MDA1ODE5MTgsImI5ODg0OTRjNmQ4ODExZTdiMzA2MWJiYWYxNjYwMDAxIiwiMTM3NTMwIiwic3BvdHgiLCI4MjA4IiwiMTQxNDU5ODQiLCIxMTMwNTA3Il0mc2V0ZWM9T0RNNU9UWmpNVE01Wm1RMU1URmhPVFU1Tm1FM016a3pPR1U1TURGbU9UVT0~","with_creds":false},{"type":"midpoint","beacon_url":"https:\\/\\/event.spotxchange.com\\/vast\\/50?_x=WzE1MDA1ODE5MTgsImI5ODg0OTRjNmQ4ODExZTdiMzA2MWJiYWYxNjYwMDAxIiwiMTM3NTMwIiwic3BvdHgiLCI4MjA4IiwiMTQxNDU5ODQiLCIxMTMwNTA3Il0mc2V0ZWM9T0RNNU9UWmpNVE01Wm1RMU1URmhPVFU1Tm1FM016a3pPR1U1TURGbU9UVT0~","with_creds":false},{"type":"thirdQuartile","beacon_url":"https:\\/\\/event.spotxchange.com\\/vast\\/75?_x=WzE1MDA1ODE5MTgsImI5ODg0OTRjNmQ4ODExZTdiMzA2MWJiYWYxNjYwMDAxIiwiMTM3NTMwIiwic3BvdHgiLCI4MjA4IiwiMTQxNDU5ODQiLCIxMTMwNTA3Il0mc2V0ZWM9T0RNNU9UWmpNVE01Wm1RMU1URmhPVFU1Tm1FM016a3pPR1U1TURGbU9UVT0~","with_creds":false},{"type":"skip","beacon_url":"https:\\/\\/event.spotxchange.com\\/vast\\/skip?_x=WzE1MDA1ODE5MTgsImI5ODg0OTRjNmQ4ODExZTdiMzA2MWJiYWYxNjYwMDAxIiwiMTM3NTMwIiwic3BvdHgiLCI4MjA4IiwiMTQxNDU5ODQiLCIxMTMwNTA3Il0mc2V0ZWM9T0RNNU9UWmpNVE01Wm1RMU1URmhPVFU1Tm1FM016a3pPR1U1TURGbU9UVT0~","with_creds":false},{"type":"event","beacon_url":"https:\\/\\/event.spotxchange.com\\/event\\/d?_x=WzE1MDA1ODE5MTgsImI5ODg0OTRjNmQ4ODExZTdiMzA2MWJiYWYxNjYwMDAxIiwiMTM3NTMwIiwic3BvdHgiLCI4MjA4IiwiMTQxNDU5ODQiLCIxMTMwNTA3Il0mc2V0ZWM9T0RNNU9UWmpNVE01Wm1RMU1URmhPVFU1Tm1FM016a3pPR1U1TURGbU9UVT0~&es=$EVENT_SOURCE&ts=$TS&eid=$EVENT_ID","with_creds":false},{"type":"initialization","beacon_url":"https:\\/\\/sb.scorecardresearch.com\\/b?c1=1&c2=6272977&c3=137530&cv=1.3&cj=1"},{"type":"click","beacon_url":""}]},"video":[{"playtime":16,"vpi":"VPAID_JS","transcoding":["low","medium","high"],"maintain_aspect_ratio":"","height":"250","source_uri":"https:\\/\\/svastx.moatads.com\\/quantcastvpaid04786010\\/moatwrapper.js#vast=https%3a%2f%2ffw.adsafeprotected.com%2fvast%2ffwjsvid%2fst%2f97316%2f16816497%2fskeleton.js%3fincludeFlash%3dfalse%26originalVast%3dhttps%3a%2f%2fad.doubleclick.net%2fddm%2fpfadx%2fN3727.Quantcast%2fB10712985.201400089%3bsz%3d0x0%3bord%3d489477956%3bdc_lat%3d%3bdc_rdid%3d%3btag_for_child_directed_treatment%3d%3bdcmt%3dtext%2fxml&level1=undefined&level2=undefined&level3=undefined&level4=undefined&slicer1=undefined&slicer2=undefined&pcode=quantcastvpaid04786010&spvb=1&zMoatAccount=p-e_yCqWnB93CQF&zMoatCreate=1130507&zMoatuid=b988494c6d8811e7b3061bbaf1660001&zMoatCamp=556888&zMoatLine=354317","bitrate":0,"width":"300","mime_type":"application\\/javascript","source_uri_external":true,"api_framework":"VPAID","media_id":"","scalable":"","page_url":null,"media_url":"https:\\/\\/svastx.moatads.com\\/quantcastvpaid04786010\\/moatwrapper.js#vast=https%3a%2f%2ffw.adsafeprotected.com%2fvast%2ffwjsvid%2fst%2f97316%2f16816497%2fskeleton.js%3fincludeFlash%3dfalse%26originalVast%3dhttps%3a%2f%2fad.doubleclick.net%2fddm%2fpfadx%2fN3727.Quantcast%2fB10712985.201400089%3bsz%3d0x0%3bord%3d489477956%3bdc_lat%3d%3bdc_rdid%3d%3btag_for_child_directed_treatment%3d%3bdcmt%3dtext%2fxml&level1=undefined&level2=undefined&level3=undefined&level4=undefined&slicer1=undefined&slicer2=undefined&pcode=quantcastvpaid04786010&spvb=1&zMoatAccount=p-e_yCqWnB93CQF&zMoatCreate=1130507&zMoatuid=b988494c6d8811e7b3061bbaf1660001&zMoatCamp=556888&zMoatLine=354317"},{"playtime":16,"vpi":"VPAID_JS","transcoding":["low","medium","high"],"maintain_aspect_ratio":"","height":"250","source_uri":"https:\\/\\/svastx.moatads.com\\/quantcastvpaid04786010\\/moatwrapper.js#vast=https%3a%2f%2ffw.adsafeprotected.com%2fvast%2ffwjsvid%2fst%2f97316%2f16816497%2fskeleton.js%3fincludeFlash%3dfalse%26originalVast%3dhttps%3a%2f%2fad.doubleclick.net%2fddm%2fpfadx%2fN3727.Quantcast%2fB10712985.201400089%3bsz%3d0x0%3bord%3d489477956%3bdc_lat%3d%3bdc_rdid%3d%3btag_for_child_directed_treatment%3d%3bdcmt%3dtext%2fxml&level1=undefined&level2=undefined&level3=undefined&level4=undefined&slicer1=undefined&slicer2=undefined&pcode=quantcastvpaid04786010&spvb=1&zMoatAccount=p-e_yCqWnB93CQF&zMoatCreate=1130507&zMoatuid=b988494c6d8811e7b3061bbaf1660001&zMoatCamp=556888&zMoatLine=354317","bitrate":0,"width":"300","mime_type":"application\\/javascript","source_uri_external":true,"api_framework":"VPAID","media_id":"","scalable":"","media_url":"https:\\/\\/svastx.moatads.com\\/quantcastvpaid04786010\\/moatwrapper.js#vast=https%3a%2f%2ffw.adsafeprotected.com%2fvast%2ffwjsvid%2fst%2f97316%2f16816497%2fskeleton.js%3fincludeFlash%3dfalse%26originalVast%3dhttps%3a%2f%2fad.doubleclick.net%2fddm%2fpfadx%2fN3727.Quantcast%2fB10712985.201400089%3bsz%3d0x0%3bord%3d489477956%3bdc_lat%3d%3bdc_rdid%3d%3btag_for_child_directed_treatment%3d%3bdcmt%3dtext%2fxml&level1=undefined&level2=undefined&level3=undefined&level4=undefined&slicer1=undefined&slicer2=undefined&pcode=quantcastvpaid04786010&spvb=1&zMoatAccount=p-e_yCqWnB93CQF&zMoatCreate=1130507&zMoatuid=b988494c6d8811e7b3061bbaf1660001&zMoatCamp=556888&zMoatLine=354317"},{"playtime":16,"vpi":"VPAID_JS","transcoding":["low","medium","high"],"maintain_aspect_ratio":"","height":"250","source_uri":"https:\\/\\/svastx.moatads.com\\/quantcastvpaid04786010\\/moatwrapper.js#vast=https%3a%2f%2ffw.adsafeprotected.com%2fvast%2ffwjsvid%2fst%2f97316%2f16816497%2fskeleton.js%3fincludeFlash%3dfalse%26originalVast%3dhttps%3a%2f%2fad.doubleclick.net%2fddm%2fpfadx%2fN3727.Quantcast%2fB10712985.201400089%3bsz%3d0x0%3bord%3d489477956%3bdc_lat%3d%3bdc_rdid%3d%3btag_for_child_directed_treatment%3d%3bdcmt%3dtext%2fxml&level1=undefined&level2=undefined&level3=undefined&level4=undefined&slicer1=undefined&slicer2=undefined&pcode=quantcastvpaid04786010&spvb=1&zMoatAccount=p-e_yCqWnB93CQF&zMoatCreate=1130507&zMoatuid=b988494c6d8811e7b3061bbaf1660001&zMoatCamp=556888&zMoatLine=354317","bitrate":0,"width":"300","mime_type":"application\\/javascript","source_uri_external":true,"api_framework":"VPAID","media_id":"","scalable":"","media_url":"https:\\/\\/svastx.moatads.com\\/quantcastvpaid04786010\\/moatwrapper.js#vast=https%3a%2f%2ffw.adsafeprotected.com%2fvast%2ffwjsvid%2fst%2f97316%2f16816497%2fskeleton.js%3fincludeFlash%3dfalse%26originalVast%3dhttps%3a%2f%2fad.doubleclick.net%2fddm%2fpfadx%2fN3727.Quantcast%2fB10712985.201400089%3bsz%3d0x0%3bord%3d489477956%3bdc_lat%3d%3bdc_rdid%3d%3btag_for_child_directed_treatment%3d%3bdcmt%3dtext%2fxml&level1=undefined&level2=undefined&level3=undefined&level4=undefined&slicer1=undefined&slicer2=undefined&pcode=quantcastvpaid04786010&spvb=1&zMoatAccount=p-e_yCqWnB93CQF&zMoatCreate=1130507&zMoatuid=b988494c6d8811e7b3061bbaf1660001&zMoatCamp=556888&zMoatLine=354317"}],"banners":{"medium_rectangle":{"banner_type":"auto","mime_type":"image\\/gif","iab_imu":"medium_rectangle","width":300,"height":250,"page_url":"https:\\/\\/search.spotxchange.com\\/click?_a=137530&_p=spotx&_z=1&_m=eNpVz99vgjAQB%2FD%2BLTyP0rM%2FLCZ7WOKWbFF5GJr4RAotUBUwgkHZ9rdvVB8W%2B3BJv3eXT45hQhBCwAnhEkKQKA2lZCHLhJYSwExTSgSkqcpBCEIIIKBTTglaf6JQ4glleAISAxOo7LpjOwuCvu9xrip7uJaq1tdK1ThrqgB9eefWnJLibLU385hRoTQ8FylPnSNzMgUtc6EpdY73dB%2B3bVKbflwYg6xp9tYkuqmUrceoNeqUlbg9Nt0lG7HCOGkcVGdtTZ2Z5IHyneU7zHea%2F8D93A6bcPR7e2i1XizuBW4R4oJcKPDx%2B99y5TuKD%2BU23rPlrhiW849dFG%2FstnqrVsPLJYrfYTm88mi%2BZ6uheP4DQppvcA%3D%3D&_l=eNplj01rwkAYhN%2FfsleLvJv9yEbooUXoxU1BDKW5yGazNUk1SZMNtVr%2Fe9VgofQyh5mHGYYqqrhiEEYykIyDABWgAi4wlAAUKKdcRIpD3zZ%2BD5MJSBpB3dQOEI6EkdmRZGVOZgSniEySO9Kar2bwNwcvVt%2B6%2BpeJrtCQbUu7dntbmHrj1m%2FOXXOKiOfYDp3xLl%2FvTPfufLs11v1n8cKeThAni8UoCDhVAparR%2FDdsGuLxrttP7XNDlBxRjORW5pxhRYjIYULc8nDyDLm8vH3%2BRxy%2BBhM7a3p%2FdhKWSgC8XcGvuNK8%2FQp2b%2BuiiKdb1AfNNfVw6eukkBXcfk8T4L0ZVnqQ3L%2FAyjAYKo%3D&_t=eNotj0tv3CAUhf1bWEcJYMeOPeomqhplMVOp8iwcVUI87tgkYCzMNPPo%2FPeAHRZwz8c5l4s0Wn5k9QFXHFeqVE9PhEAlclwRIfiBlCXGmGRy4HrMnO%2F5qGUm%2BDiCZ3LJXpEFpTnTCjVIKSFKQRUhdfGYxx1ULQtVQ81LVVNAd%2BjgvOUhel9ef0VptQUWzhNEoi3v4e9Drw%2Fx4lOrMESYYxzVALofUoo%2BLjJYw6SzkwELY%2BIRai6Ytsco0kRHyzzIwMfepGdn4F4ObJ5AouaKJu65hQB%2BTkrBPy0hVXAK6ZgnF05sxYxP0%2FeE5BYHdgoMaugdcjFLb3FlGbnHOONN1VznpmxQb5zgBm10gzcLmNwcVkBqQiPLl98mkBdVhYuIigZJHc7fJrIGPfTajYmVpI6oiiZ3HINffJTmay%2B19iop2dyy%2F93lj%2Bnandm1%2B3P3PrzvXl4%2Fu8v2tG3fzNbuL7tW6a59Hn7%2F3P%2F4Ap5ioKM%3D&_b=eNozYMgoKSkottLXLy7IL6lIzkjMS0%2FVS87PZfAL9fFhsEwzsLQ0NzQ0S7GwMDRMNU8yNjA3TEpKTDM0M3MDArCqmih3V5PIrGxDX5d0g8gqr0x%2Fl0hDfxe3bN8sTwO%2FrLDMyKycDD8XEJ1uCwCmYiLM","media_id":"ddbb6b2d119453119ed9c4d9e9a6d92e","filesize":420,"type":"image","format":"GIF","html_tag":"&lt;a href=&quot;https:\\/\\/search.spotxchange.com\\/click?_a=137530&_p=spotx&_z=1&_m=eNpVz99vgjAQB%2FD%2BLTyP0rM%2FLCZ7WOKWbFF5GJr4RAotUBUwgkHZ9rdvVB8W%2B3BJv3eXT45hQhBCwAnhEkKQKA2lZCHLhJYSwExTSgSkqcpBCEIIIKBTTglaf6JQ4glleAISAxOo7LpjOwuCvu9xrip7uJaq1tdK1ThrqgB9eefWnJLibLU385hRoTQ8FylPnSNzMgUtc6EpdY73dB%2B3bVKbflwYg6xp9tYkuqmUrceoNeqUlbg9Nt0lG7HCOGkcVGdtTZ2Z5IHyneU7zHea%2F8D93A6bcPR7e2i1XizuBW4R4oJcKPDx%2B99y5TuKD%2BU23rPlrhiW849dFG%2FstnqrVsPLJYrfYTm88mi%2BZ6uheP4DQppvcA%3D%3D&_l=eNplj01rwkAYhN%2FfsleLvJv9yEbooUXoxU1BDKW5yGazNUk1SZMNtVr%2Fe9VgofQyh5mHGYYqqrhiEEYykIyDABWgAi4wlAAUKKdcRIpD3zZ%2BD5MJSBpB3dQOEI6EkdmRZGVOZgSniEySO9Kar2bwNwcvVt%2B6%2BpeJrtCQbUu7dntbmHrj1m%2FOXXOKiOfYDp3xLl%2FvTPfufLs11v1n8cKeThAni8UoCDhVAparR%2FDdsGuLxrttP7XNDlBxRjORW5pxhRYjIYULc8nDyDLm8vH3%2BRxy%2BBhM7a3p%2FdhKWSgC8XcGvuNK8%2FQp2b%2BuiiKdb1AfNNfVw6eukkBXcfk8T4L0ZVnqQ3L%2FAyjAYKo%3D&_t=eNotj0tv3CAUhf1bWEcJYMeOPeomqhplMVOp8iwcVUI87tgkYCzMNPPo%2FPeAHRZwz8c5l4s0Wn5k9QFXHFeqVE9PhEAlclwRIfiBlCXGmGRy4HrMnO%2F5qGUm%2BDiCZ3LJXpEFpTnTCjVIKSFKQRUhdfGYxx1ULQtVQ81LVVNAd%2BjgvOUhel9ef0VptQUWzhNEoi3v4e9Drw%2Fx4lOrMESYYxzVALofUoo%2BLjJYw6SzkwELY%2BIRai6Ytsco0kRHyzzIwMfepGdn4F4ObJ5AouaKJu65hQB%2BTkrBPy0hVXAK6ZgnF05sxYxP0%2FeE5BYHdgoMaugdcjFLb3FlGbnHOONN1VznpmxQb5zgBm10gzcLmNwcVkBqQiPLl98mkBdVhYuIigZJHc7fJrIGPfTajYmVpI6oiiZ3HINffJTmay%2B19iop2dyy%2F93lj%2Bnandm1%2B3P3PrzvXl4%2Fu8v2tG3fzNbuL7tW6a59Hn7%2F3P%2F4Ap5ioKM%3D&_b=eNozYMgoKSkottLXLy7IL6lIzkjMS0%2FVS87PZfAL9fFhsEwzsLQ0NzQ0S7GwMDRMNU8yNjA3TEpKTDM0M3MDArCqmih3V5PIrGxDX5d0g8gqr0x%2Fl0hDfxe3bN8sTwO%2FrLDMyKycDD8XEJ1uCwCmYiLM&quot; border=&quot;0&quot; target=&quot;_blank&quot; title=&quot;IntegralAds_VAST_2_0_Ad_Wrapper&quot;&gt;&lt;img style=&quot;border:0; width:300px; height:250px;&quot; src=&quot;https:\\/\\/search.spotxchange.com\\/banner?_a=137530&_p=spotx&_z=1&_m=eNpVz99vgjAQB%2FD%2BLTyP0rM%2FLCZ7WOKWbFF5GJr4RAotUBUwgkHZ9rdvVB8W%2B3BJv3eXT45hQhBCwAnhEkKQKA2lZCHLhJYSwExTSgSkqcpBCEIIIKBTTglaf6JQ4glleAISAxOo7LpjOwuCvu9xrip7uJaq1tdK1ThrqgB9eefWnJLibLU385hRoTQ8FylPnSNzMgUtc6EpdY73dB%2B3bVKbflwYg6xp9tYkuqmUrceoNeqUlbg9Nt0lG7HCOGkcVGdtTZ2Z5IHyneU7zHea%2F8D93A6bcPR7e2i1XizuBW4R4oJcKPDx%2B99y5TuKD%2BU23rPlrhiW849dFG%2FstnqrVsPLJYrfYTm88mi%2BZ6uheP4DQppvcA%3D%3D&_l=eNplj01rwkAYhN%2FfsleLvJv9yEbooUXoxU1BDKW5yGazNUk1SZMNtVr%2Fe9VgofQyh5mHGYYqqrhiEEYykIyDABWgAi4wlAAUKKdcRIpD3zZ%2BD5MJSBpB3dQOEI6EkdmRZGVOZgSniEySO9Kar2bwNwcvVt%2B6%2BpeJrtCQbUu7dntbmHrj1m%2FOXXOKiOfYDp3xLl%2FvTPfufLs11v1n8cKeThAni8UoCDhVAparR%2FDdsGuLxrttP7XNDlBxRjORW5pxhRYjIYULc8nDyDLm8vH3%2BRxy%2BBhM7a3p%2FdhKWSgC8XcGvuNK8%2FQp2b%2BuiiKdb1AfNNfVw6eukkBXcfk8T4L0ZVnqQ3L%2FAyjAYKo%3D&_t=eNotj9Fu4yAQRf0tPFdbwK5dE%2B1b1aiVnGpXlVaJKlkDTGxaYyxMtsmm%2BfeFuDzA3MO9wyBhHNFn9Z5WQCtd6vt7xrCSOa2YlLBnZUkpZZnqwYyZ8x2MRmVySZ2JRW2gNZoIorWUpeSasbq4y%2BOOulaFrrGGUtccyQ3ZO28hRO%2F66TFKayy24TRhJMZCh2%2B3ndnHi0%2BjQx9hTmlUPZquTyl%2Bd5XBDq1ydhrQ4ph4hAZka%2BwhijTRwbYeVYCxG9KzM4JXfTtPqIg4kwk8WAzo56Q0%2FjUKU4XHkI55cuHYLriFafqekF3iwE7jQAS%2FIS5m%2BSWuLGM%2FKM1AVOI8i1KQbnASBrIygq6uYHJzWACrGY8sv%2F42gbyoKlpEVAiiTDh9m9gS9NgZNyZWsjqiKprcYQz%2B6uM8X3rppVfJ2eqSfW3Wv05b%2Fvy%2Be2j4br352NqN3f55Ys2%2Fj8%2FNQ3d8WTfHhv8eXl6bn%2F8BtbOdiA%3D%3D&_b=eNpFxl0LgjAUgGF%2FkSc%2FEhZ0EUje6AFFibxznuXmsklbGNKPL6%2FihYd350nnZnsA6Onh29m49za9mWASpDpw8jVxC%2BapBqAt4jzhIQUBi%2FfRT0Gsj4kJ1iXEQuEP6uZhk%2Bd%2FPsVaKUxPK6Z3haOO26xZ2gvKoi6Xa30eMas0rmWEtT5%2BAZ5CLzA%3D&quot; alt=&quot;IntegralAds_VAST_2_0_Ad_Wrapper&quot; \\/&gt;&lt;\\/a&gt;","source_uri":null,"html_source":null,"iframe_source":null,"banner_url":"https:\\/\\/search.spotxchange.com\\/banner?_a=137530&_p=spotx&_z=1&_m=eNpVz99vgjAQB%2FD%2BLTyP0rM%2FLCZ7WOKWbFF5GJr4RAotUBUwgkHZ9rdvVB8W%2B3BJv3eXT45hQhBCwAnhEkKQKA2lZCHLhJYSwExTSgSkqcpBCEIIIKBTTglaf6JQ4glleAISAxOo7LpjOwuCvu9xrip7uJaq1tdK1ThrqgB9eefWnJLibLU385hRoTQ8FylPnSNzMgUtc6EpdY73dB%2B3bVKbflwYg6xp9tYkuqmUrceoNeqUlbg9Nt0lG7HCOGkcVGdtTZ2Z5IHyneU7zHea%2F8D93A6bcPR7e2i1XizuBW4R4oJcKPDx%2B99y5TuKD%2BU23rPlrhiW849dFG%2FstnqrVsPLJYrfYTm88mi%2BZ6uheP4DQppvcA%3D%3D&_l=eNplj01rwkAYhN%2FfsleLvJv9yEbooUXoxU1BDKW5yGazNUk1SZMNtVr%2Fe9VgofQyh5mHGYYqqrhiEEYykIyDABWgAi4wlAAUKKdcRIpD3zZ%2BD5MJSBpB3dQOEI6EkdmRZGVOZgSniEySO9Kar2bwNwcvVt%2B6%2BpeJrtCQbUu7dntbmHrj1m%2FOXXOKiOfYDp3xLl%2FvTPfufLs11v1n8cKeThAni8UoCDhVAparR%2FDdsGuLxrttP7XNDlBxRjORW5pxhRYjIYULc8nDyDLm8vH3%2BRxy%2BBhM7a3p%2FdhKWSgC8XcGvuNK8%2FQp2b%2BuiiKdb1AfNNfVw6eukkBXcfk8T4L0ZVnqQ3L%2FAyjAYKo%3D&_t=eNotj9Fu4yAQRf0tPFdbwK5dE%2B1b1aiVnGpXlVaJKlkDTGxaYyxMtsmm%2BfeFuDzA3MO9wyBhHNFn9Z5WQCtd6vt7xrCSOa2YlLBnZUkpZZnqwYyZ8x2MRmVySZ2JRW2gNZoIorWUpeSasbq4y%2BOOulaFrrGGUtccyQ3ZO28hRO%2F66TFKayy24TRhJMZCh2%2B3ndnHi0%2BjQx9hTmlUPZquTyl%2Bd5XBDq1ydhrQ4ph4hAZka%2BwhijTRwbYeVYCxG9KzM4JXfTtPqIg4kwk8WAzo56Q0%2FjUKU4XHkI55cuHYLriFafqekF3iwE7jQAS%2FIS5m%2BSWuLGM%2FKM1AVOI8i1KQbnASBrIygq6uYHJzWACrGY8sv%2F42gbyoKlpEVAiiTDh9m9gS9NgZNyZWsjqiKprcYQz%2B6uM8X3rppVfJ2eqSfW3Wv05b%2Fvy%2Be2j4br352NqN3f55Ys2%2Fj8%2FNQ3d8WTfHhv8eXl6bn%2F8BtbOdiA%3D%3D&_b=eNpFxl0LgjAUgGF%2FkSc%2FEhZ0EUje6AFFibxznuXmsklbGNKPL6%2FihYd350nnZnsA6Onh29m49za9mWASpDpw8jVxC%2BapBqAt4jzhIQUBi%2FfRT0Gsj4kJ1iXEQuEP6uZhk%2Bd%2FPsVaKUxPK6Z3haOO26xZ2gvKoi6Xa30eMas0rmWEtT5%2BAZ5CLzA%3D","html_banner_url":"https:\\/\\/search.spotxchange.com\\/banner?_a=137530&_p=spotx&_z=1&_m=eNpVz99vgjAQB%2FD%2BLTyP0rM%2FLCZ7WOKWbFF5GJr4RAotUBUwgkHZ9rdvVB8W%2B3BJv3eXT45hQhBCwAnhEkKQKA2lZCHLhJYSwExTSgSkqcpBCEIIIKBTTglaf6JQ4glleAISAxOo7LpjOwuCvu9xrip7uJaq1tdK1ThrqgB9eefWnJLibLU385hRoTQ8FylPnSNzMgUtc6EpdY73dB%2B3bVKbflwYg6xp9tYkuqmUrceoNeqUlbg9Nt0lG7HCOGkcVGdtTZ2Z5IHyneU7zHea%2F8D93A6bcPR7e2i1XizuBW4R4oJcKPDx%2B99y5TuKD%2BU23rPlrhiW849dFG%2FstnqrVsPLJYrfYTm88mi%2BZ6uheP4DQppvcA%3D%3D&_l=eNplj01rwkAYhN%2FfsleLvJv9yEbooUXoxU1BDKW5yGazNUk1SZMNtVr%2Fe9VgofQyh5mHGYYqqrhiEEYykIyDABWgAi4wlAAUKKdcRIpD3zZ%2BD5MJSBpB3dQOEI6EkdmRZGVOZgSniEySO9Kar2bwNwcvVt%2B6%2BpeJrtCQbUu7dntbmHrj1m%2FOXXOKiOfYDp3xLl%2FvTPfufLs11v1n8cKeThAni8UoCDhVAparR%2FDdsGuLxrttP7XNDlBxRjORW5pxhRYjIYULc8nDyDLm8vH3%2BRxy%2BBhM7a3p%2FdhKWSgC8XcGvuNK8%2FQp2b%2BuiiKdb1AfNNfVw6eukkBXcfk8T4L0ZVnqQ3L%2FAyjAYKo%3D&_t=eNotj9Fu4yAQRf0tPFdbwK5dE%2B1b1aiVnGpXlVaJKlkDTGxaYyxMtsmm%2BfeFuDzA3MO9wyBhHNFn9Z5WQCtd6vt7xrCSOa2YlLBnZUkpZZnqwYyZ8x2MRmVySZ2JRW2gNZoIorWUpeSasbq4y%2BOOulaFrrGGUtccyQ3ZO28hRO%2F66TFKayy24TRhJMZCh2%2B3ndnHi0%2BjQx9hTmlUPZquTyl%2Bd5XBDq1ydhrQ4ph4hAZka%2BwhijTRwbYeVYCxG9KzM4JXfTtPqIg4kwk8WAzo56Q0%2FjUKU4XHkI55cuHYLriFafqekF3iwE7jQAS%2FIS5m%2BSWuLGM%2FKM1AVOI8i1KQbnASBrIygq6uYHJzWACrGY8sv%2F42gbyoKlpEVAiiTDh9m9gS9NgZNyZWsjqiKprcYQz%2B6uM8X3rppVfJ2eqSfW3Wv05b%2Fvy%2Be2j4br352NqN3f55Ys2%2Fj8%2FNQ3d8WTfHhv8eXl6bn%2F8BtbOdiA%3D%3D&_b=eNozYEjRB8KUpCSzJKMUQ0NLE1NjIJmaYplskmKZaplolmJplKqXUZKbw%2BAX6uODIGp83T0NI7MysiOzHCv8wj0rI3MDq3yNwnKAtEFkuKtBVDiQrvI1jqxKtwUAEJMfUw%3D%3D","html_tag_internal":true}},"ad_parameters":""},"ad_system":[],"referrer":"https:\\/\\/www.familyhandyman.com\\/","inventory_class":null,"safety":null,"client_playback":{"ados":{"client_side":{"feed_timeout":2000,"load_timeout":120000,"total_timeout":15000}},"ad_broker":{"idod_always_monetize":false,"idod_disable_max":false,"idod_max_count":5,"idod_detect":false,"idod_kill":false},"channel_name":"Familyhandyman.com TMBI","playback_conf":{"third_party_tracking":{"double_verify":true,"double_verify_noscript":false,"integral":false,"moat":true,"whiteops":false},"instream":[],"min_volume":"0","release_track":"beta"},"ad_unit":{"outstream":{"retry_timeout":10000,"retry_interval":850}},"publisher_name":"Trusted Media Brands, Inc","publisher_id":"137525","custom_skin":0},"skipit":{"enabled":1},"display":{"ad_marker":{"evidon":{"enabled":0}}},"third_party_tracking":{"providers":{"moat":{"id":"moat","parameters":{"level1":"137525","level2":"137530","level3":"8208","level4":"45076","slicer1":"www.familyhandyman.com","partnerCode":"spotxchangejsvideo759622536126","zMoatImpressionId":"9f07a07d6d8811e7b3071bbaf1660001","zMoatAD":["trumphotels.com"],"zMoatFD":null}},"double_verify":{"id":"double_verify","type":"impression","mime_type":"text\\/javascript","unique":"No","beacon_url":"https:\\/\\/cdn.doubleverify.com\\/dvtp_src.js?ctx=484047&cmp=2228897&sid=593588&plc=22288971&num=&adid=&advid=484048&adsrv=15&btreg=&btadsrv=&crt=&crtname=&chnl=&unit=&pid=&uid=&dvtagver=6.1.src&DVP_CHANNELID=137530&DVP_DEALID=spotx&DVP_CMPID=45076&turl=https:\\/\\/www.familyhandyman.com\\/&DVPX_SX_UID=4ea98e5f-6b5b-11e7-8f07-1d8f6d330001&DVPX_SX_MID=b988494c6d8811e7b3061bbaf1660001&DVPX_IP=98.234.218.146&DVPX_UA=Mozilla\\/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit\\/537.36 (KHTML, like Gecko) Chrome\\/59.0.3071.115 Safari\\/537.36","parameters":[]}}},"bundle_id":null,"channel_id":"137530"} -------------------------------------------------------------------------------- /testdata/spotx_html_resource.html: -------------------------------------------------------------------------------- 1 | IntegralAds_VAST_2_0_Ad_Wrapper -------------------------------------------------------------------------------- /testdata/vast4_universal_ad_id.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | iabtechlab 7 | http://example.com/error 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | http://example.com/track/impression 16 | 17 | 18 | 19 | iabtechlab video ad 20 | AD CONTENT description category 21 | 22 | 23 | 8465 24 | 25 | 26 | http://example.com/tracking/start 27 | http://example.com/tracking/firstQuartile 28 | http://example.com/tracking/midpoint 29 | http://example.com/tracking/thirdQuartile 30 | http://example.com/tracking/complete 31 | 32 | 00:00:16 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /testdata/vast_adaptv_attempt_attr.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Adap.tv 7 | 8 | 9 | 10 | &p.vw.viewableOpportunity=1&p.vw.viewableHistorical=61.49&p.vw.psize=3&a.sdk=o2unit&a.sdkType=js&pi.sdk=&pi.sdkType=&a.appReq=0&a.platformDevice=ONLINE_VIDEO&ipAddressOverride=98.234.218.146&a.platformOs=Mac+OS+X&p.vw.active=&a.rtbexch=&pi.sideview=&pi.flashonpage=&pi.mvoa=100&pi.avoa=&pi.sound=&pi.autoInitiation=&esVr=67.2897&esMvmr=61.4865&sadVr=67.2897&sadMvmr=61.4865&p.vw.geometric=&p.vw.framerate=&a.pub_id=&device_id_status=&a.ts=0&a.adSeq={adSeq}&isHttps=1&pubSettingId=5&eov=28963053&alephdEnabled=1&isFailoverAd=0&doubleAuction=&errorCode=%5BERRORCODE%5D]]> 11 | 12 | &f0=1&162=61.49&68=3&d7=o2unit&c0=js&c4=0&91=ONLINE_VIDEO&45=98.234.218.146&ee=Mac+OS+X&b5=&146=100&14c=67.2897&14d=61.4865&153=67.2897&154=61.4865&120=0&100={adSeq}&112=1&134=5&33=28963053&135=2&137=1&13d=0&14f=9zYHE4kOIKIZc1EDPjx1hw__&colo=NA&a.afv=-1&a.dfv=-2&168=}&16e=0a61ed6&16f=gayoncMgHis_&a.cv=1]]> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | &p.vw.viewableOpportunity=1&p.vw.viewableHistorical=61.49&p.vw.psize=3&a.sdk=o2unit&a.sdkType=js&pi.sdk=&pi.sdkType=&a.appReq=0&a.platformDevice=ONLINE_VIDEO&ipAddressOverride=98.234.218.146&a.platformOs=Mac+OS+X&p.vw.active=&a.rtbexch=&pi.sideview=&pi.flashonpage=&pi.mvoa=100&pi.avoa=&pi.sound=&pi.autoInitiation=&esVr=67.2897&esMvmr=61.4865&sadVr=67.2897&sadMvmr=61.4865&p.vw.geometric=&p.vw.framerate=&a.pub_id=&device_id_status=&a.ts=0&a.adSeq={adSeq}&isHttps=1&pubSettingId=5&eov=28963053&doubleAuction=&nextSeatbid=&buyerSeatId=&isMatchedUser=]]> 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | &f0=1&162=61.49&68=3&d7=o2unit&c0=js&c4=0&91=ONLINE_VIDEO&45=98.234.218.146&ee=Mac+OS+X&b5=&146=100&14c=67.2897&14d=61.4865&153=67.2897&154=61.4865&120=0&100={adSeq}&112=1&134=5&33=28963053&a.cv=1]]> 45 | 46 | &f0=1&162=61.49&68=3&d7=o2unit&c0=js&c4=0&91=ONLINE_VIDEO&45=98.234.218.146&ee=Mac+OS+X&b5=&146=100&14c=67.2897&14d=61.4865&153=67.2897&154=61.4865&120=0&100={adSeq}&112=1&134=5&33=28963053&a.cv=1]]> 47 | 48 | &f0=1&162=61.49&68=3&d7=o2unit&c0=js&c4=0&91=ONLINE_VIDEO&45=98.234.218.146&ee=Mac+OS+X&b5=&146=100&14c=67.2897&14d=61.4865&153=67.2897&154=61.4865&120=0&100={adSeq}&112=1&134=5&33=28963053&a.cv=1]]> 49 | 50 | &f0=1&162=61.49&68=3&d7=o2unit&c0=js&c4=0&91=ONLINE_VIDEO&45=98.234.218.146&ee=Mac+OS+X&b5=&146=100&14c=67.2897&14d=61.4865&153=67.2897&154=61.4865&120=0&100={adSeq}&112=1&134=5&33=28963053&a.cv=1]]> 51 | 52 | &f0=1&162=61.49&68=3&d7=o2unit&c0=js&c4=0&91=ONLINE_VIDEO&45=98.234.218.146&ee=Mac+OS+X&b5=&146=100&14c=67.2897&14d=61.4865&153=67.2897&154=61.4865&120=0&100={adSeq}&112=1&134=5&33=28963053&91=ONLINE_VIDEO&f6=iCSArrpvL%2FM_&12e=-1.0000&12f=-1.0000&130=-1.0000&131=-1.0000&15c=-1&163=NFnJzCvmQNo7ZequqMcKyQ__&164=CNCzRkR4JL4_&a.cv=1]]> 53 | 54 | &p.vw.viewableOpportunity=1&p.vw.viewableHistorical=61.49&p.vw.psize=3&a.sdk=o2unit&a.sdkType=js&pi.sdk=&pi.sdkType=&a.appReq=0&a.platformDevice=ONLINE_VIDEO&ipAddressOverride=98.234.218.146&a.platformOs=Mac+OS+X&p.vw.active=&a.rtbexch=&pi.sideview=&pi.flashonpage=&pi.mvoa=100&pi.avoa=&pi.sound=&pi.autoInitiation=&esVr=67.2897&esMvmr=61.4865&sadVr=67.2897&sadMvmr=61.4865&p.vw.geometric=&p.vw.framerate=&a.pub_id=&device_id_status=&a.ts=0&a.adSeq={adSeq}&isHttps=1&pubSettingId=5&eov=28963053&doubleAuction=]]> 55 | 56 | &p.vw.viewableOpportunity=1&p.vw.viewableHistorical=61.49&p.vw.psize=3&a.sdk=o2unit&a.sdkType=js&pi.sdk=&pi.sdkType=&a.appReq=0&a.platformDevice=ONLINE_VIDEO&ipAddressOverride=98.234.218.146&a.platformOs=Mac+OS+X&p.vw.active=&a.rtbexch=&pi.sideview=&pi.flashonpage=&pi.mvoa=100&pi.avoa=&pi.sound=&pi.autoInitiation=&esVr=67.2897&esMvmr=61.4865&sadVr=67.2897&sadMvmr=61.4865&p.vw.geometric=&p.vw.framerate=&a.pub_id=&device_id_status=&a.ts=0&a.adSeq={adSeq}&isHttps=1&pubSettingId=5&eov=28963053]]> 57 | 58 | &p.vw.viewableOpportunity=1&p.vw.viewableHistorical=61.49&p.vw.psize=3&a.sdk=o2unit&a.sdkType=js&pi.sdk=&pi.sdkType=&a.appReq=0&a.platformDevice=ONLINE_VIDEO&ipAddressOverride=98.234.218.146&a.platformOs=Mac+OS+X&p.vw.active=&a.rtbexch=&pi.sideview=&pi.flashonpage=&pi.mvoa=100&pi.avoa=&pi.sound=&pi.autoInitiation=&esVr=67.2897&esMvmr=61.4865&sadVr=67.2897&sadMvmr=61.4865&p.vw.geometric=&p.vw.framerate=&a.pub_id=&device_id_status=&a.ts=0&a.adSeq={adSeq}&isHttps=1&pubSettingId=5&eov=28963053]]> 59 | 60 | &p.vw.viewableOpportunity=1&p.vw.viewableHistorical=61.49&p.vw.psize=3&a.sdk=o2unit&a.sdkType=js&pi.sdk=&pi.sdkType=&a.appReq=0&a.platformDevice=ONLINE_VIDEO&ipAddressOverride=98.234.218.146&a.platformOs=Mac+OS+X&p.vw.active=&a.rtbexch=&pi.sideview=&pi.flashonpage=&pi.mvoa=100&pi.avoa=&pi.sound=&pi.autoInitiation=&esVr=67.2897&esMvmr=61.4865&sadVr=67.2897&sadMvmr=61.4865&p.vw.geometric=&p.vw.framerate=&a.pub_id=&device_id_status=&a.ts=0&a.adSeq={adSeq}&isHttps=1&pubSettingId=5&eov=28963053]]> 61 | 62 | &p.vw.viewableOpportunity=1&p.vw.viewableHistorical=61.49&p.vw.psize=3&a.sdk=o2unit&a.sdkType=js&pi.sdk=&pi.sdkType=&a.appReq=0&a.platformDevice=ONLINE_VIDEO&ipAddressOverride=98.234.218.146&a.platformOs=Mac+OS+X&p.vw.active=&a.rtbexch=&pi.sideview=&pi.flashonpage=&pi.mvoa=100&pi.avoa=&pi.sound=&pi.autoInitiation=&esVr=67.2897&esMvmr=61.4865&sadVr=67.2897&sadMvmr=61.4865&p.vw.geometric=&p.vw.framerate=&a.pub_id=&device_id_status=&a.ts=0&a.adSeq={adSeq}&isHttps=1&pubSettingId=5&eov=28963053]]> 63 | 64 | &p.vw.viewableOpportunity=1&p.vw.viewableHistorical=61.49&p.vw.psize=3&a.sdk=o2unit&a.sdkType=js&pi.sdk=&pi.sdkType=&a.appReq=0&a.platformDevice=ONLINE_VIDEO&ipAddressOverride=98.234.218.146&a.platformOs=Mac+OS+X&p.vw.active=&a.rtbexch=&pi.sideview=&pi.flashonpage=&pi.mvoa=100&pi.avoa=&pi.sound=&pi.autoInitiation=&esVr=67.2897&esMvmr=61.4865&sadVr=67.2897&sadMvmr=61.4865&p.vw.geometric=&p.vw.framerate=&a.pub_id=&device_id_status=&a.ts=0&a.adSeq={adSeq}&isHttps=1&pubSettingId=5&eov=28963053]]> 65 | 66 | 67 | 68 | &f0=1&162=61.49&68=3&d7=o2unit&c0=js&c4=0&91=ONLINE_VIDEO&45=98.234.218.146&ee=Mac+OS+X&b5=&146=100&14c=67.2897&14d=61.4865&153=67.2897&154=61.4865&120=0&100={adSeq}&112=1&134=5&33=28963053&f6=iCSArrpvL%2FM_&12e=-1.0000&12f=-1.0000&130=-1.0000&131=-1.0000&15c=-1&163=NFnJzCvmQNo7ZequqMcKyQ__&164=CNCzRkR4JL4_&a.cv=1&rUrl=http%3A%2F%2Fwww.adap.tv]]> 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | &p.vw.viewableOpportunity=1&p.vw.viewableHistorical=61.49&p.vw.psize=3&a.sdk=o2unit&a.sdkType=js&pi.sdk=&pi.sdkType=&a.appReq=0&a.platformDevice=ONLINE_VIDEO&ipAddressOverride=98.234.218.146&a.platformOs=Mac+OS+X&p.vw.active=&a.rtbexch=&pi.sideview=&pi.flashonpage=&pi.mvoa=100&pi.avoa=&pi.sound=&pi.autoInitiation=&esVr=67.2897&esMvmr=61.4865&sadVr=67.2897&sadMvmr=61.4865&p.vw.geometric=&p.vw.framerate=&a.pub_id=&device_id_status=&a.ts=0&a.adSeq={adSeq}&isHttps=1&pubSettingId=5&eov=28963053]]> 141 | 142 | 143 | 144 | 145 | 146 | &p.vw.viewableOpportunity=1&p.vw.viewableHistorical=61.49&p.vw.psize=3&a.sdk=o2unit&a.sdkType=js&pi.sdk=&pi.sdkType=&a.appReq=0&a.platformDevice=ONLINE_VIDEO&ipAddressOverride=98.234.218.146&a.platformOs=Mac+OS+X&p.vw.active=&a.rtbexch=&pi.sideview=&pi.flashonpage=&pi.mvoa=100&pi.avoa=&pi.sound=&pi.autoInitiation=&esVr=67.2897&esMvmr=61.4865&sadVr=67.2897&sadMvmr=61.4865&p.vw.geometric=&p.vw.framerate=&a.pub_id=&device_id_status=&a.ts=0&a.adSeq={adSeq}&isHttps=1&pubSettingId=5&eov=28963053&doubleAuction=]]> 147 | 148 | &p.vw.viewableOpportunity=1&p.vw.viewableHistorical=61.49&p.vw.psize=3&a.sdk=o2unit&a.sdkType=js&pi.sdk=&pi.sdkType=&a.appReq=0&a.platformDevice=ONLINE_VIDEO&ipAddressOverride=98.234.218.146&a.platformOs=Mac+OS+X&p.vw.active=&a.rtbexch=&pi.sideview=&pi.flashonpage=&pi.mvoa=100&pi.avoa=&pi.sound=&pi.autoInitiation=&esVr=67.2897&esMvmr=61.4865&sadVr=67.2897&sadMvmr=61.4865&p.vw.geometric=&p.vw.framerate=&a.pub_id=&device_id_status=&a.ts=0&a.adSeq={adSeq}&isHttps=1&pubSettingId=5&eov=28963053&doubleAuction=]]> 149 | 150 | &p.vw.viewableOpportunity=1&p.vw.viewableHistorical=61.49&p.vw.psize=3&a.sdk=o2unit&a.sdkType=js&pi.sdk=&pi.sdkType=&a.appReq=0&a.platformDevice=ONLINE_VIDEO&ipAddressOverride=98.234.218.146&a.platformOs=Mac+OS+X&p.vw.active=&a.rtbexch=&pi.sideview=&pi.flashonpage=&pi.mvoa=100&pi.avoa=&pi.sound=&pi.autoInitiation=&esVr=67.2897&esMvmr=61.4865&sadVr=67.2897&sadMvmr=61.4865&p.vw.geometric=&p.vw.framerate=&a.pub_id=&device_id_status=&a.ts=0&a.adSeq={adSeq}&isHttps=1&pubSettingId=5&eov=28963053&doubleAuction=]]> 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /testdata/vast_inline_linear-duration_undefined.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Acudeo Compatible 6 | VAST 2.0 Instream Test 1 7 | 8 | 9 | 10 | undefined 11 | 12 | http://cdnp.tremormedia.com/video/acudeo/Carrot_400x300_500kb.flv 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /testdata/vast_inline_linear.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Acudeo Compatible 6 | VAST 2.0 Instream Test 1 7 | VAST 2.0 Instream Test 1 8 | http://myErrorURL/error 9 | http://myErrorURL/error2 10 | http://myTrackingURL/impression 11 | http://myTrackingURL/impression2 12 | 13 | 14 | 15 | 00:00:30 16 | 17 | http://myTrackingURL/creativeView 18 | http://myTrackingURL/start 19 | http://myTrackingURL/midpoint 20 | http://myTrackingURL/firstQuartile 21 | http://myTrackingURL/thirdQuartile 22 | http://myTrackingURL/complete 23 | 24 | 25 | http://www.tremormedia.com 26 | http://myTrackingURL/click 27 | 28 | 29 | http://cdnp.tremormedia.com/video/acudeo/Carrot_400x300_500kb.flv 30 | 31 | 32 | 33 | 34 | 35 | 36 | http://demo.tremormedia.com/proddev/vast/Blistex1.jpg 37 | 38 | http://myTrackingURL/firstCompanionCreativeView 39 | 40 | http://www.tremormedia.com 41 | 42 | 43 | http://demo.tremormedia.com/proddev/vast/728x90_banner1.jpg 44 | http://www.tremormedia.com 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /testdata/vast_inline_nonlinear.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Acudeo Compatible 6 | NonLinear Test Campaign 1 7 | NonLinear Test Campaign 1 8 | http://mySurveyURL/survey 9 | http://myErrorURL/error 10 | http://myTrackingURL/impression 11 | 12 | 13 | 14 | 15 | http://myTrackingURL/nonlinear/creativeView 16 | http://myTrackingURL/nonlinear/expand 17 | http://myTrackingURL/nonlinear/collapse 18 | http://myTrackingURL/nonlinear/acceptInvitation 19 | http://myTrackingURL/nonlinear/close 20 | 21 | 22 | 23 | http://demo.tremormedia.com/proddev/vast/50x300_static.jpg 24 | 25 | http://www.tremormedia.com 26 | 27 | 28 | 29 | http://demo.tremormedia.com/proddev/vast/50x450_static.jpg 30 | 31 | http://www.tremormedia.com 32 | 33 | 34 | 35 | 36 | 37 | 38 | http://demo.tremormedia.com/proddev/vast/300x250_companion_1.swf 39 | 40 | http://www.tremormedia.com 41 | 42 | 43 | http://demo.tremormedia.com/proddev/vast/728x90_banner1.jpg 44 | 45 | http://myTrackingURL/secondCompanion 46 | 47 | http://www.tremormedia.com 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /testdata/vast_wrapper_linear_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Acudeo Compatible 6 | http://demo.tremormedia.com/proddev/vast/vast_inline_linear.xml 7 | http://myErrorURL/wrapper/error 8 | http://myTrackingURL/wrapper/impression 9 | 10 | 11 | 12 | 13 | http://myTrackingURL/wrapper/creativeView 14 | http://myTrackingURL/wrapper/start 15 | http://myTrackingURL/wrapper/midpoint 16 | http://myTrackingURL/wrapper/firstQuartile 17 | http://myTrackingURL/wrapper/thirdQuartile 18 | http://myTrackingURL/wrapper/complete 19 | http://myTrackingURL/wrapper/mute 20 | http://myTrackingURL/wrapper/unmute 21 | http://myTrackingURL/wrapper/pause 22 | http://myTrackingURL/wrapper/resume 23 | http://myTrackingURL/wrapper/fullscreen 24 | 25 | 26 | 27 | 28 | 29 | 30 | http://myTrackingURL/wrapper/click 31 | 32 | 33 | 34 | 35 | 36 | 37 | http://myTrackingURL/wrapper/creativeView 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /testdata/vast_wrapper_linear_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Acudeo Compatible 6 | http://demo.tremormedia.com/proddev/vast/vast_inline_linear.xml 7 | http://myTrackingURL/wrapper/impression 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | http://demo.tremormedia.com/proddev/vast/300x250_banner1.jpg 19 | 20 | http://myTrackingURL/wrapper/firstCompanionCreativeView 21 | 22 | http://www.tremormedia.com 23 | 24 | 25 | http://demo.tremormedia.com/proddev/vast/728x90_banner1.jpg 26 | http://www.tremormedia.com 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /testdata/vast_wrapper_nonlinear_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Acudeo Compatible 6 | http://demo.tremormedia.com/proddev/vast/vast_inline_nonlinear2.xml 7 | http://myErrorURL/wrapper/error 8 | http://myTrackingURL/wrapper/impression 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | http://myTrackingURL/wrapper/nonlinear/creativeView/creativeView 20 | http://myTrackingURL/wrapper/nonlinear/creativeView/expand 21 | http://myTrackingURL/wrapper/nonlinear/creativeView/collapse 22 | http://myTrackingURL/wrapper/nonlinear/creativeView/acceptInvitation 23 | http://myTrackingURL/wrapper/nonlinear/creativeView/close 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /testdata/vast_wrapper_nonlinear_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Acudeo Compatible 6 | http://demo.tremormedia.com/proddev/vast/vast_inline_nonlinear3.xml 7 | http://myTrackingURL/wrapper/impression 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | http://demo.tremormedia.com/proddev/vast/300x250_banner1.jpg 25 | 26 | http://myTrackingURL/wrapper/firstCompanionCreativeView 27 | 28 | http://www.tremormedia.com 29 | 30 | 31 | http://demo.tremormedia.com/proddev/vast/728x90_banner1.jpg 32 | http://www.tremormedia.com 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /vast.go: -------------------------------------------------------------------------------- 1 | // Package vast implements IAB VAST 3.0 specification http://www.iab.net/media/file/VASTv3.0.pdf 2 | package vast 3 | 4 | import "encoding/xml" 5 | 6 | // VAST is the root tag 7 | type VAST struct { 8 | // The version of the VAST spec (should be either "2.0" or "3.0") 9 | Version string `xml:"version,attr"` 10 | // One or more Ad elements. Advertisers and video content publishers may 11 | // associate an element with a line item video ad defined in contract 12 | // documentation, usually an insertion order. These line item ads typically 13 | // specify the creative to display, price, delivery schedule, targeting, 14 | // and so on. 15 | Ads []Ad `xml:"Ad"` 16 | // Contains a URI to a tracking resource that the video player should request 17 | // upon receiving a “no ad” response 18 | Errors []CDATAString `xml:"Error,omitempty"` 19 | } 20 | 21 | // Ad represent an child tag in a VAST document 22 | // 23 | // Each contains a single element or element (but never both). 24 | type Ad struct { 25 | // An ad server-defined identifier string for the ad 26 | ID string `xml:"id,attr,omitempty"` 27 | // A number greater than zero (0) that identifies the sequence in which 28 | // an ad should play; all elements with sequence values are part of 29 | // a pod and are intended to be played in sequence 30 | Sequence int `xml:"sequence,attr,omitempty"` 31 | InLine *InLine `xml:",omitempty"` 32 | Wrapper *Wrapper `xml:",omitempty"` 33 | } 34 | 35 | // CDATAString ... 36 | type CDATAString struct { 37 | CDATA string `xml:",cdata"` 38 | } 39 | 40 | // InLine is a vast ad element containing actual ad definition 41 | // 42 | // The last ad server in the ad supply chain serves an element. 43 | // Within the nested elements of an element are all the files and 44 | // URIs necessary to display the ad. 45 | type InLine struct { 46 | // The name of the ad server that returned the ad 47 | AdSystem *AdSystem 48 | // The common name of the ad 49 | AdTitle CDATAString 50 | // One or more URIs that directs the video player to a tracking resource file that the 51 | // video player should request when the first frame of the ad is displayed 52 | Impressions []Impression `xml:"Impression"` 53 | // The container for one or more elements 54 | Creatives []Creative `xml:"Creatives>Creative"` 55 | // A string value that provides a longer description of the ad. 56 | Description CDATAString `xml:",omitempty"` 57 | // The name of the advertiser as defined by the ad serving party. 58 | // This element can be used to prevent displaying ads with advertiser 59 | // competitors. Ad serving parties and publishers should identify how 60 | // to interpret values provided within this element. As with any optional 61 | // elements, the video player is not required to support it. 62 | Advertiser string `xml:",omitempty"` 63 | // A URI to a survey vendor that could be the survey, a tracking pixel, 64 | // or anything to do with the survey. Multiple survey elements can be provided. 65 | // A type attribute is available to specify the MIME type being served. 66 | // For example, the attribute might be set to type=”text/javascript”. 67 | // Surveys can be dynamically inserted into the VAST response as long as 68 | // cross-domain issues are avoided. 69 | Survey CDATAString `xml:",omitempty"` 70 | // A URI representing an error-tracking pixel; this element can occur multiple 71 | // times. 72 | Errors []CDATAString `xml:"Error,omitempty"` 73 | // Provides a value that represents a price that can be used by real-time bidding 74 | // (RTB) systems. VAST is not designed to handle RTB since other methods exist, 75 | // but this element is offered for custom solutions if needed. 76 | Pricing *Pricing `xml:",omitempty"` 77 | // XML node for custom extensions, as defined by the ad server. When used, a 78 | // custom element should be nested under to help separate custom 79 | // XML elements from VAST elements. The following example includes a custom 80 | // xml element within the Extensions element. 81 | Extensions *[]Extension `xml:"Extensions>Extension,omitempty"` 82 | } 83 | 84 | // Impression is a URI that directs the video player to a tracking resource file that 85 | // the video player should request when the first frame of the ad is displayed 86 | type Impression struct { 87 | ID string `xml:"id,attr,omitempty"` 88 | URI string `xml:",cdata"` 89 | } 90 | 91 | // Pricing provides a value that represents a price that can be used by real-time 92 | // bidding (RTB) systems. VAST is not designed to handle RTB since other methods 93 | // exist, but this element is offered for custom solutions if needed. 94 | type Pricing struct { 95 | // Identifies the pricing model as one of "cpm", "cpc", "cpe" or "cpv". 96 | Model string `xml:"model,attr"` 97 | // The 3 letter ISO-4217 currency symbol that identifies the currency of 98 | // the value provided 99 | Currency string `xml:"currency,attr"` 100 | // If the value provided is to be obfuscated/encoded, publishers and advertisers 101 | // must negotiate the appropriate mechanism to do so. When included as part of 102 | // a VAST Wrapper in a chain of Wrappers, only the value offered in the first 103 | // Wrapper need be considered. 104 | Value string `xml:",cdata"` 105 | } 106 | 107 | // Wrapper element contains a URI reference to a vendor ad server (often called 108 | // a third party ad server). The destination ad server either provides the ad 109 | // files within a VAST ad element or may provide a secondary Wrapper 110 | // ad, pointing to yet another ad server. Eventually, the final ad server in 111 | // the ad supply chain must contain all the necessary files needed to display 112 | // the ad. 113 | type Wrapper struct { 114 | // The name of the ad server that returned the ad 115 | AdSystem *AdSystem 116 | // URL of ad tag of downstream Secondary Ad Server 117 | VASTAdTagURI CDATAString 118 | // One or more URIs that directs the video player to a tracking resource file that the 119 | // video player should request when the first frame of the ad is displayed 120 | Impressions []Impression `xml:"Impression"` 121 | // A URI representing an error-tracking pixel; this element can occur multiple 122 | // times. 123 | Errors []CDATAString `xml:"Error,omitempty"` 124 | // The container for one or more elements 125 | Creatives []CreativeWrapper `xml:"Creatives>Creative"` 126 | // XML node for custom extensions, as defined by the ad server. When used, a 127 | // custom element should be nested under to help separate custom 128 | // XML elements from VAST elements. The following example includes a custom 129 | // xml element within the Extensions element. 130 | Extensions []Extension `xml:"Extensions>Extension,omitempty"` 131 | 132 | FallbackOnNoAd *bool `xml:"fallbackOnNoAd,attr,omitempty"` 133 | AllowMultipleAds *bool `xml:"allowMultipleAds,attr,omitempty"` 134 | FollowAdditionalWrappers *bool `xml:"followAdditionalWrappers,attr,omitempty"` 135 | } 136 | 137 | // AdSystem contains information about the system that returned the ad 138 | type AdSystem struct { 139 | Version string `xml:"version,attr,omitempty"` 140 | Name string `xml:",cdata"` 141 | } 142 | 143 | // Creative is a file that is part of a VAST ad. 144 | type Creative struct { 145 | // An ad server-defined identifier for the creative 146 | ID string `xml:"id,attr,omitempty"` 147 | // The preferred order in which multiple Creatives should be displayed 148 | Sequence int `xml:"sequence,attr,omitempty"` 149 | // Identifies the ad with which the creative is served 150 | AdID string `xml:"AdID,attr,omitempty"` 151 | // The technology used for any included API 152 | APIFramework string `xml:"apiFramework,attr,omitempty"` 153 | // If present, defines a linear creative 154 | Linear *Linear `xml:",omitempty"` 155 | // If defined, defins companions creatives 156 | CompanionAds *CompanionAds `xml:",omitempty"` 157 | // If defined, defines non linear creatives 158 | NonLinearAds *NonLinearAds `xml:",omitempty"` 159 | // If present, provides a VAST 4.x universal ad id 160 | UniversalAdID *UniversalAdID `xml:"UniversalAdId,omitempty"` 161 | // When an API framework is needed to execute creative, a 162 | // element can be added under the . This 163 | // extension can be used to load an executable creative with or without using 164 | // a media file. 165 | // A element is nested under the 166 | // (plural) element so that any xml extensions are separated from VAST xml. 167 | // Additionally, any xml used in this extension should identify an xml name 168 | // space (xmlns) to avoid confusing any of the xml element names with those 169 | // of VAST. 170 | // The nested includes an attribute for type, which 171 | // specifies the MIME type needed to execute the extension. 172 | CreativeExtensions *[]Extension `xml:"CreativeExtensions>CreativeExtension,omitempty"` 173 | } 174 | 175 | // CompanionAds contains companions creatives 176 | type CompanionAds struct { 177 | // Provides information about which companion creative to display. 178 | // All means that the player must attempt to display all. Any means the player 179 | // must attempt to play at least one. None means all companions are optional 180 | Required string `xml:"required,attr,omitempty"` 181 | Companions []Companion `xml:"Companion,omitempty"` 182 | } 183 | 184 | // NonLinearAds contains non linear creatives 185 | type NonLinearAds struct { 186 | TrackingEvents []Tracking `xml:"TrackingEvents>Tracking,omitempty"` 187 | // Non linear creatives 188 | NonLinears []NonLinear `xml:"NonLinear,omitempty"` 189 | } 190 | 191 | // CreativeWrapper defines wrapped creative's parent trackers 192 | type CreativeWrapper struct { 193 | // An ad server-defined identifier for the creative 194 | ID string `xml:"id,attr,omitempty"` 195 | // The preferred order in which multiple Creatives should be displayed 196 | Sequence int `xml:"sequence,attr,omitempty"` 197 | // Identifies the ad with which the creative is served 198 | AdID string `xml:"AdID,attr,omitempty"` 199 | // If present, defines a linear creative 200 | Linear *LinearWrapper `xml:",omitempty"` 201 | // If defined, defines companions creatives 202 | CompanionAds *CompanionAdsWrapper `xml:"CompanionAds,omitempty"` 203 | // If defined, defines non linear creatives 204 | NonLinearAds *NonLinearAdsWrapper `xml:"NonLinearAds,omitempty"` 205 | } 206 | 207 | // CompanionAdsWrapper contains companions creatives in a wrapper 208 | type CompanionAdsWrapper struct { 209 | // Provides information about which companion creative to display. 210 | // All means that the player must attempt to display all. Any means the player 211 | // must attempt to play at least one. None means all companions are optional 212 | Required string `xml:"required,attr,omitempty"` 213 | Companions []CompanionWrapper `xml:"Companion,omitempty"` 214 | } 215 | 216 | // NonLinearAdsWrapper contains non linear creatives in a wrapper 217 | type NonLinearAdsWrapper struct { 218 | TrackingEvents []Tracking `xml:"TrackingEvents>Tracking,omitempty"` 219 | // Non linear creatives 220 | NonLinears []NonLinearWrapper `xml:"NonLinear,omitempty"` 221 | } 222 | 223 | // Linear is the most common type of video advertisement trafficked in the 224 | // industry is a “linear ad”, which is an ad that displays in the same area 225 | // as the content but not at the same time as the content. In fact, the video 226 | // player must interrupt the content before displaying a linear ad. 227 | // Linear ads are often displayed right before the video content plays. 228 | // This ad position is called a “pre-roll” position. For this reason, a linear 229 | // ad is often called a “pre-roll.” 230 | type Linear struct { 231 | // To specify that a Linear creative can be skipped, the ad server must 232 | // include the skipoffset attribute in the element. The value 233 | // for skipoffset is a time value in the format HH:MM:SS or HH:MM:SS.mmm 234 | // or a percentage in the format n%. The .mmm value in the time offset 235 | // represents milliseconds and is optional. This skipoffset value 236 | // indicates when the skip control should be provided after the creative 237 | // begins playing. 238 | SkipOffset *Offset `xml:"skipoffset,attr,omitempty"` 239 | // Duration in standard time format, hh:mm:ss 240 | Duration Duration 241 | AdParameters *AdParameters `xml:",omitempty"` 242 | Icons *Icons 243 | TrackingEvents []Tracking `xml:"TrackingEvents>Tracking,omitempty"` 244 | VideoClicks *VideoClicks `xml:",omitempty"` 245 | MediaFiles []MediaFile `xml:"MediaFiles>MediaFile,omitempty"` 246 | } 247 | 248 | // LinearWrapper defines a wrapped linear creative 249 | type LinearWrapper struct { 250 | Icons *Icons 251 | TrackingEvents []Tracking `xml:"TrackingEvents>Tracking,omitempty"` 252 | VideoClicks *VideoClicks `xml:",omitempty"` 253 | } 254 | 255 | // Companion defines a companion ad 256 | type Companion struct { 257 | // Optional identifier 258 | ID string `xml:"id,attr,omitempty"` 259 | // Pixel dimensions of companion slot. 260 | Width int `xml:"width,attr,omitempty"` 261 | // Pixel dimensions of companion slot. 262 | Height int `xml:"height,attr,omitempty"` 263 | // Pixel dimensions of the companion asset. 264 | AssetWidth int `xml:"assetWidth,attr,omitempty"` 265 | // Pixel dimensions of the companion asset. 266 | AssetHeight int `xml:"assetHeight,attr,omitempty"` 267 | // Pixel dimensions of expanding companion ad when in expanded state. 268 | ExpandedWidth int `xml:"expandedWidth,attr,omitempty"` 269 | // Pixel dimensions of expanding companion ad when in expanded state. 270 | ExpandedHeight int `xml:"expandedHeight,attr,omitempty"` 271 | // The apiFramework defines the method to use for communication with the companion. 272 | APIFramework string `xml:"apiFramework,attr,omitempty"` 273 | // Used to match companion creative to publisher placement areas on the page. 274 | AdSlotID string `xml:"adSlotId,attr,omitempty"` 275 | // URL to open as destination page when user clicks on the the companion banner ad. 276 | CompanionClickThrough CDATAString `xml:",omitempty"` 277 | // URLs to ping when user clicks on the the companion banner ad. 278 | CompanionClickTracking []CDATAString `xml:",omitempty"` 279 | // Alt text to be displayed when companion is rendered in HTML environment. 280 | AltText string `xml:",omitempty"` 281 | // The creativeView should always be requested when present. For Companions 282 | // creativeView is the only supported event. 283 | TrackingEvents []Tracking `xml:"TrackingEvents>Tracking,omitempty"` 284 | // Data to be passed into the companion ads. The apiFramework defines the method 285 | // to use for communication (e.g. “FlashVar”) 286 | AdParameters *AdParameters `xml:",omitempty"` 287 | // URL to a static file, such as an image or SWF file 288 | StaticResource *StaticResource `xml:",omitempty"` 289 | // URL source for an IFrame to display the companion element 290 | IFrameResource CDATAString `xml:",omitempty"` 291 | // HTML to display the companion element 292 | HTMLResource *HTMLResource `xml:",omitempty"` 293 | } 294 | 295 | // CompanionWrapper defines a companion ad in a wrapper 296 | type CompanionWrapper struct { 297 | // Optional identifier 298 | ID string `xml:"id,attr,omitempty"` 299 | // Pixel dimensions of companion slot. 300 | Width int `xml:"width,attr"` 301 | // Pixel dimensions of companion slot. 302 | Height int `xml:"height,attr"` 303 | // Pixel dimensions of the companion asset. 304 | AssetWidth int `xml:"assetWidth,attr"` 305 | // Pixel dimensions of the companion asset. 306 | AssetHeight int `xml:"assetHeight,attr"` 307 | // Pixel dimensions of expanding companion ad when in expanded state. 308 | ExpandedWidth int `xml:"expandedWidth,attr"` 309 | // Pixel dimensions of expanding companion ad when in expanded state. 310 | ExpandedHeight int `xml:"expandedHeight,attr"` 311 | // The apiFramework defines the method to use for communication with the companion. 312 | APIFramework string `xml:"apiFramework,attr,omitempty"` 313 | // Used to match companion creative to publisher placement areas on the page. 314 | AdSlotID string `xml:"adSlotId,attr,omitempty"` 315 | // URL to open as destination page when user clicks on the the companion banner ad. 316 | CompanionClickThrough CDATAString `xml:",omitempty"` 317 | // URLs to ping when user clicks on the the companion banner ad. 318 | CompanionClickTracking []CDATAString `xml:",omitempty"` 319 | // Alt text to be displayed when companion is rendered in HTML environment. 320 | AltText string `xml:",omitempty"` 321 | // The creativeView should always be requested when present. For Companions 322 | // creativeView is the only supported event. 323 | TrackingEvents []Tracking `xml:"TrackingEvents>Tracking,omitempty"` 324 | // Data to be passed into the companion ads. The apiFramework defines the method 325 | // to use for communication (e.g. “FlashVar”) 326 | AdParameters *AdParameters `xml:",omitempty"` 327 | // URL to a static file, such as an image or SWF file 328 | StaticResource *StaticResource `xml:",omitempty"` 329 | // URL source for an IFrame to display the companion element 330 | IFrameResource CDATAString `xml:",omitempty"` 331 | // HTML to display the companion element 332 | HTMLResource *HTMLResource `xml:",omitempty"` 333 | } 334 | 335 | // NonLinear defines a non linear ad 336 | type NonLinear struct { 337 | // Optional identifier 338 | ID string `xml:"id,attr,omitempty"` 339 | // Pixel dimensions of companion. 340 | Width int `xml:"width,attr"` 341 | // Pixel dimensions of companion. 342 | Height int `xml:"height,attr"` 343 | // Pixel dimensions of expanding nonlinear ad when in expanded state. 344 | ExpandedWidth int `xml:"expandedWidth,attr"` 345 | // Pixel dimensions of expanding nonlinear ad when in expanded state. 346 | ExpandedHeight int `xml:"expandedHeight,attr"` 347 | // Whether it is acceptable to scale the image. 348 | Scalable bool `xml:"scalable,attr,omitempty"` 349 | // Whether the ad must have its aspect ratio maintained when scales. 350 | MaintainAspectRatio bool `xml:"maintainAspectRatio,attr,omitempty"` 351 | // Suggested duration to display non-linear ad, typically for animation to complete. 352 | // Expressed in standard time format hh:mm:ss. 353 | MinSuggestedDuration *Duration `xml:"minSuggestedDuration,attr,omitempty"` 354 | // The apiFramework defines the method to use for communication with the nonlinear element. 355 | APIFramework string `xml:"apiFramework,attr,omitempty"` 356 | // URLs to ping when user clicks on the the non-linear ad. 357 | NonLinearClickTracking []CDATAString `xml:",omitempty"` 358 | // URL to open as destination page when user clicks on the non-linear ad unit. 359 | NonLinearClickThrough CDATAString `xml:",omitempty"` 360 | // Data to be passed into the video ad. 361 | AdParameters *AdParameters `xml:",omitempty"` 362 | // URL to a static file, such as an image or SWF file 363 | StaticResource *StaticResource `xml:",omitempty"` 364 | // URL source for an IFrame to display the companion element 365 | IFrameResource CDATAString `xml:",omitempty"` 366 | // HTML to display the companion element 367 | HTMLResource *HTMLResource `xml:",omitempty"` 368 | } 369 | 370 | // NonLinearWrapper defines a non linear ad in a wrapper 371 | type NonLinearWrapper struct { 372 | // Optional identifier 373 | ID string `xml:"id,attr,omitempty"` 374 | // Pixel dimensions of companion. 375 | Width int `xml:"width,attr"` 376 | // Pixel dimensions of companion. 377 | Height int `xml:"height,attr"` 378 | // Pixel dimensions of expanding nonlinear ad when in expanded state. 379 | ExpandedWidth int `xml:"expandedWidth,attr"` 380 | // Pixel dimensions of expanding nonlinear ad when in expanded state. 381 | ExpandedHeight int `xml:"expandedHeight,attr"` 382 | // Whether it is acceptable to scale the image. 383 | Scalable bool `xml:"scalable,attr,omitempty"` 384 | // Whether the ad must have its aspect ratio maintained when scales. 385 | MaintainAspectRatio bool `xml:"maintainAspectRatio,attr,omitempty"` 386 | // Suggested duration to display non-linear ad, typically for animation to complete. 387 | // Expressed in standard time format hh:mm:ss. 388 | MinSuggestedDuration *Duration `xml:"minSuggestedDuration,attr,omitempty"` 389 | // The apiFramework defines the method to use for communication with the nonlinear element. 390 | APIFramework string `xml:"apiFramework,attr,omitempty"` 391 | // The creativeView should always be requested when present. 392 | TrackingEvents []Tracking `xml:"TrackingEvents>Tracking,omitempty"` 393 | // URLs to ping when user clicks on the the non-linear ad. 394 | NonLinearClickTracking []CDATAString `xml:",omitempty"` 395 | } 396 | 397 | type Icons struct { 398 | XMLName xml.Name `xml:"Icons,omitempty"` 399 | Icon []Icon `xml:"Icon,omitempty"` 400 | } 401 | 402 | // Icon represents advertising industry initiatives like AdChoices. 403 | type Icon struct { 404 | // Identifies the industry initiative that the icon supports. 405 | Program string `xml:"program,attr"` 406 | // Pixel dimensions of icon. 407 | Width int `xml:"width,attr"` 408 | // Pixel dimensions of icon. 409 | Height int `xml:"height,attr"` 410 | // The horizontal alignment location (in pixels) or a specific alignment. 411 | // Must match ([0-9]*|left|right) 412 | XPosition string `xml:"xPosition,attr"` 413 | // The vertical alignment location (in pixels) or a specific alignment. 414 | // Must match ([0-9]*|top|bottom) 415 | YPosition string `xml:"yPosition,attr"` 416 | // Start time at which the player should display the icon. Expressed in standard time format hh:mm:ss. 417 | Offset Offset `xml:"offset,attr"` 418 | // duration for which the player must display the icon. Expressed in standard time format hh:mm:ss. 419 | Duration Duration `xml:"duration,attr"` 420 | // The apiFramework defines the method to use for communication with the icon element 421 | APIFramework string `xml:"apiFramework,attr,omitempty"` 422 | // URL to open as destination page when user clicks on the icon. 423 | IconClickThrough CDATAString `xml:"IconClicks>IconClickThrough,omitempty"` 424 | // URLs to ping when user clicks on the the icon. 425 | IconClickTrackings []CDATAString `xml:"IconClicks>IconClickTracking,omitempty"` 426 | // URL to a static file, such as an image or SWF file 427 | StaticResource *StaticResource `xml:",omitempty"` 428 | // URL source for an IFrame to display the companion element 429 | IFrameResource CDATAString `xml:",omitempty"` 430 | // HTML to display the companion element 431 | HTMLResource *HTMLResource `xml:",omitempty"` 432 | } 433 | 434 | // Tracking defines an event tracking URL 435 | type Tracking struct { 436 | // The name of the event to track for the element. The creativeView should 437 | // always be requested when present. 438 | // 439 | // Possible values are creativeView, start, firstQuartile, midpoint, thirdQuartile, 440 | // complete, mute, unmute, pause, rewind, resume, fullscreen, exitFullscreen, expand, 441 | // collapse, acceptInvitation, close, skip, progress. 442 | Event string `xml:"event,attr"` 443 | // The time during the video at which this url should be pinged. Must be present for 444 | // progress event. Must match (\d{2}:[0-5]\d:[0-5]\d(\.\d\d\d)?|1?\d?\d(\.?\d)*%) 445 | Offset *Offset `xml:"offset,attr,omitempty"` 446 | URI string `xml:",cdata"` 447 | } 448 | 449 | // StaticResource is the URL to a static file, such as an image or SWF file 450 | type StaticResource struct { 451 | // Mime type of static resource 452 | CreativeType string `xml:"creativeType,attr,omitempty"` 453 | // URL to a static file, such as an image or SWF file 454 | URI string `xml:",cdata"` 455 | } 456 | 457 | // HTMLResource is a container for HTML data 458 | type HTMLResource struct { 459 | // Specifies whether the HTML is XML-encoded 460 | XMLEncoded bool `xml:"xmlEncoded,attr,omitempty"` 461 | HTML string `xml:",cdata"` 462 | } 463 | 464 | // AdParameters defines arbitrary ad parameters 465 | type AdParameters struct { 466 | // Specifies whether the parameters are XML-encoded 467 | XMLEncoded bool `xml:"xmlEncoded,attr,omitempty"` 468 | Parameters string `xml:",cdata"` 469 | } 470 | 471 | // VideoClicks contains types of video clicks 472 | type VideoClicks struct { 473 | ClickThroughs []VideoClick `xml:"ClickThrough,omitempty"` 474 | ClickTrackings []VideoClick `xml:"ClickTracking,omitempty"` 475 | CustomClicks []VideoClick `xml:"CustomClick,omitempty"` 476 | } 477 | 478 | // VideoClick defines a click URL for a linear creative 479 | type VideoClick struct { 480 | ID string `xml:"id,attr,omitempty"` 481 | URI string `xml:",cdata"` 482 | } 483 | 484 | // MediaFile defines a reference to a linear creative asset 485 | type MediaFile struct { 486 | // Optional identifier 487 | ID string `xml:"id,attr,omitempty"` 488 | // Method of delivery of ad (either "streaming" or "progressive") 489 | Delivery string `xml:"delivery,attr"` 490 | // MIME type. Popular MIME types include, but are not limited to 491 | // “video/x-ms-wmv” for Windows Media, and “video/x-flv” for Flash 492 | // Video. Image ads or interactive ads can be included in the 493 | // MediaFiles section with appropriate Mime types 494 | Type string `xml:"type,attr"` 495 | // The codec used to produce the media file. 496 | Codec string `xml:"codec,attr,omitempty"` 497 | // Bitrate of encoded video in Kbps. If bitrate is supplied, MinBitrate 498 | // and MaxBitrate should not be supplied. 499 | Bitrate int `xml:"bitrate,attr,omitempty"` 500 | // Minimum bitrate of an adaptive stream in Kbps. If MinBitrate is supplied, 501 | // MaxBitrate must be supplied and Bitrate should not be supplied. 502 | MinBitrate int `xml:"minBitrate,attr,omitempty"` 503 | // Maximum bitrate of an adaptive stream in Kbps. If MaxBitrate is supplied, 504 | // MinBitrate must be supplied and Bitrate should not be supplied. 505 | MaxBitrate int `xml:"maxBitrate,attr,omitempty"` 506 | // Pixel dimensions of video. 507 | Width int `xml:"width,attr"` 508 | // Pixel dimensions of video. 509 | Height int `xml:"height,attr"` 510 | // Whether it is acceptable to scale the image. 511 | Scalable bool `xml:"scalable,attr,omitempty"` 512 | // Whether the ad must have its aspect ratio maintained when scales. 513 | MaintainAspectRatio bool `xml:"maintainAspectRatio,attr,omitempty"` 514 | // The APIFramework defines the method to use for communication if the MediaFile 515 | // is interactive. Suggested values for this element are “VPAID”, “FlashVars” 516 | // (for Flash/Flex), “initParams” (for Silverlight) and “GetVariables” (variables 517 | // placed in key/value pairs on the asset request). 518 | APIFramework string `xml:"apiFramework,attr,omitempty"` 519 | URI string `xml:",cdata"` 520 | } 521 | 522 | // UniversalAdID describes a VAST 4.x universal ad id. 523 | type UniversalAdID struct { 524 | IDRegistry string `xml:"idRegistry,attr"` 525 | IDValue string `xml:"idValue,attr"` 526 | ID string `xml:",cdata"` 527 | } 528 | -------------------------------------------------------------------------------- /vast_test.go: -------------------------------------------------------------------------------- 1 | package vast 2 | 3 | import ( 4 | "encoding/xml" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func loadFixture(path string) (*VAST, string, string, error) { 15 | xmlFile, err := os.Open(path) 16 | if err != nil { 17 | return nil, "", "", err 18 | } 19 | defer xmlFile.Close() 20 | b, _ := ioutil.ReadAll(xmlFile) 21 | 22 | var v VAST 23 | err = xml.Unmarshal(b, &v) 24 | 25 | res, err := xml.MarshalIndent(v, "", " ") 26 | if err != nil { 27 | return nil, "", "", err 28 | 29 | } 30 | 31 | return &v, string(b), string(res), err 32 | } 33 | 34 | func TestCreativeExtensions(t *testing.T) { 35 | v, _, _, err := loadFixture("testdata/creative_extensions.xml") 36 | if !assert.NoError(t, err) { 37 | return 38 | } 39 | 40 | assert.Equal(t, "3.0", v.Version) 41 | if assert.Len(t, v.Ads, 1) { 42 | ad := v.Ads[0] 43 | assert.Equal(t, "abc123", ad.ID) 44 | if assert.NotNil(t, ad.InLine) { 45 | if assert.Len(t, ad.InLine.Creatives, 1) { 46 | exts := *ad.InLine.Creatives[0].CreativeExtensions 47 | if assert.Len(t, exts, 4) { 48 | var ext Extension 49 | // asserting first extension 50 | ext = exts[0] 51 | assert.Equal(t, "geo", ext.Type) 52 | assert.Empty(t, ext.CustomTracking) 53 | assert.Equal(t, "\n US\n 3\n 1680\n ", string(ext.Data)) 54 | // asserting second extension 55 | ext = exts[1] 56 | assert.Equal(t, "activeview", ext.Type) 57 | if assert.Len(t, ext.CustomTracking, 2) { 58 | // first tracker 59 | assert.Equal(t, "viewable_impression", ext.CustomTracking[0].Event) 60 | assert.Equal(t, "https://pubads.g.doubleclick.net/pagead/conversion/?ai=test&label=viewable_impression&acvw=[VIEWABILITY]&gv=[GOOGLE_VIEWABILITY]&ad_mt=[AD_MT]", ext.CustomTracking[0].URI) 61 | // second tracker 62 | assert.Equal(t, "abandon", ext.CustomTracking[1].Event) 63 | assert.Equal(t, "https://pubads.g.doubleclick.net/pagead/conversion/?ai=test&label=video_abandon&acvw=[VIEWABILITY]&gv=[GOOGLE_VIEWABILITY]", ext.CustomTracking[1].URI) 64 | } 65 | assert.Empty(t, string(ext.Data)) 66 | // asserting third extension 67 | ext = exts[2] 68 | assert.Equal(t, "DFP", ext.Type) 69 | assert.Empty(t, ext.CustomTracking) 70 | assert.Equal(t, "\n Generic\n ", string(ext.Data)) 71 | // asserting fourth extension 72 | ext = exts[3] 73 | assert.Equal(t, "metrics", ext.Type) 74 | assert.Empty(t, ext.CustomTracking) 75 | assert.Equal(t, "\n MubmWKCWLs_tiQPYiYrwBw\n CIGpsPCTkdMCFdN-Ygod-xkCKQ\n ", string(ext.Data)) 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | func TestInlineExtensions(t *testing.T) { 83 | v, _, _, err := loadFixture("testdata/inline_extensions.xml") 84 | if !assert.NoError(t, err) { 85 | return 86 | } 87 | 88 | assert.Equal(t, "3.0", v.Version) 89 | if assert.Len(t, v.Ads, 1) { 90 | ad := v.Ads[0] 91 | assert.Equal(t, "708365173", ad.ID) 92 | if assert.NotNil(t, ad.InLine) { 93 | if assert.NotNil(t, ad.InLine.Extensions) { 94 | exts := *ad.InLine.Extensions 95 | if assert.Len(t, exts, 4) { 96 | var ext Extension 97 | // asserting first extension 98 | ext = exts[0] 99 | assert.Equal(t, "geo", ext.Type) 100 | assert.Empty(t, ext.CustomTracking) 101 | assert.Equal(t, "\n US\n 3\n 1680\n ", string(ext.Data)) 102 | // asserting second extension 103 | ext = exts[1] 104 | assert.Equal(t, "activeview", ext.Type) 105 | if assert.Len(t, ext.CustomTracking, 2) { 106 | // first tracker 107 | assert.Equal(t, "viewable_impression", ext.CustomTracking[0].Event) 108 | assert.Equal(t, "https://pubads.g.doubleclick.net/pagead/conversion/?ai=test&label=viewable_impression&acvw=[VIEWABILITY]&gv=[GOOGLE_VIEWABILITY]&ad_mt=[AD_MT]", ext.CustomTracking[0].URI) 109 | // second tracker 110 | assert.Equal(t, "abandon", ext.CustomTracking[1].Event) 111 | assert.Equal(t, "https://pubads.g.doubleclick.net/pagead/conversion/?ai=test&label=video_abandon&acvw=[VIEWABILITY]&gv=[GOOGLE_VIEWABILITY]", ext.CustomTracking[1].URI) 112 | } 113 | assert.Empty(t, string(ext.Data)) 114 | // asserting third extension 115 | ext = exts[2] 116 | assert.Equal(t, "DFP", ext.Type) 117 | assert.Equal(t, "\n Generic\n ", string(ext.Data)) 118 | assert.Empty(t, ext.CustomTracking) 119 | // asserting fourth extension 120 | ext = exts[3] 121 | assert.Equal(t, "metrics", ext.Type) 122 | assert.Equal(t, "\n MubmWKCWLs_tiQPYiYrwBw\n CIGpsPCTkdMCFdN-Ygod-xkCKQ\n ", string(ext.Data)) 123 | assert.Empty(t, ext.CustomTracking) 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | func TestInlineLinear(t *testing.T) { 131 | v, _, _, err := loadFixture("testdata/vast_inline_linear.xml") 132 | if !assert.NoError(t, err) { 133 | return 134 | } 135 | 136 | assert.Equal(t, "2.0", v.Version) 137 | if assert.Len(t, v.Ads, 1) { 138 | ad := v.Ads[0] 139 | assert.Equal(t, "601364", ad.ID) 140 | assert.Nil(t, ad.Wrapper) 141 | assert.Equal(t, 0, ad.Sequence) 142 | if assert.NotNil(t, ad.InLine) { 143 | inline := ad.InLine 144 | assert.Equal(t, "Acudeo Compatible", inline.AdSystem.Name) 145 | assert.Equal(t, "1.0", inline.AdSystem.Version) 146 | assert.Equal(t, "VAST 2.0 Instream Test 1", inline.AdTitle.CDATA) 147 | assert.Equal(t, "VAST 2.0 Instream Test 1", inline.Description.CDATA) 148 | if assert.Len(t, inline.Errors, 2) { 149 | assert.Equal(t, "http://myErrorURL/error", inline.Errors[0].CDATA) 150 | assert.Equal(t, "http://myErrorURL/error2", inline.Errors[1].CDATA) 151 | } 152 | if assert.Len(t, inline.Impressions, 2) { 153 | assert.Equal(t, "http://myTrackingURL/impression", inline.Impressions[0].URI) 154 | assert.Equal(t, "http://myTrackingURL/impression2", inline.Impressions[1].URI) 155 | assert.Equal(t, "foo", inline.Impressions[1].ID) 156 | } 157 | if assert.Len(t, inline.Creatives, 2) { 158 | crea1 := inline.Creatives[0] 159 | assert.Equal(t, "601364", crea1.AdID) 160 | assert.Nil(t, crea1.NonLinearAds) 161 | assert.Nil(t, crea1.CompanionAds) 162 | if assert.NotNil(t, crea1.Linear) { 163 | linear := crea1.Linear 164 | assert.Equal(t, Duration(30*time.Second), linear.Duration) 165 | if assert.Len(t, linear.TrackingEvents, 6) { 166 | assert.Equal(t, linear.TrackingEvents[0].Event, "creativeView") 167 | assert.Equal(t, linear.TrackingEvents[0].URI, "http://myTrackingURL/creativeView") 168 | assert.Equal(t, linear.TrackingEvents[1].Event, "start") 169 | assert.Equal(t, linear.TrackingEvents[1].URI, "http://myTrackingURL/start") 170 | } 171 | if assert.NotNil(t, linear.VideoClicks) { 172 | if assert.Len(t, linear.VideoClicks.ClickThroughs, 1) { 173 | assert.Equal(t, linear.VideoClicks.ClickThroughs[0].URI, "http://www.tremormedia.com") 174 | } 175 | if assert.Len(t, linear.VideoClicks.ClickTrackings, 1) { 176 | assert.Equal(t, linear.VideoClicks.ClickTrackings[0].URI, "http://myTrackingURL/click") 177 | } 178 | assert.Len(t, linear.VideoClicks.CustomClicks, 0) 179 | } 180 | if assert.Len(t, linear.MediaFiles, 1) { 181 | mf := linear.MediaFiles[0] 182 | assert.Equal(t, "progressive", mf.Delivery) 183 | assert.Equal(t, "video/x-flv", mf.Type) 184 | assert.Equal(t, 500, mf.Bitrate) 185 | assert.Equal(t, 400, mf.Width) 186 | assert.Equal(t, 300, mf.Height) 187 | assert.Equal(t, true, mf.Scalable) 188 | assert.Equal(t, true, mf.MaintainAspectRatio) 189 | assert.Equal(t, "http://cdnp.tremormedia.com/video/acudeo/Carrot_400x300_500kb.flv", mf.URI) 190 | } 191 | } 192 | 193 | crea2 := inline.Creatives[1] 194 | assert.Equal(t, "601364-Companion", crea2.AdID) 195 | assert.Nil(t, crea2.NonLinearAds) 196 | assert.Nil(t, crea2.Linear) 197 | if assert.NotNil(t, crea2.CompanionAds) { 198 | assert.Equal(t, "all", crea2.CompanionAds.Required) 199 | if assert.Len(t, crea2.CompanionAds.Companions, 2) { 200 | comp1 := crea2.CompanionAds.Companions[0] 201 | assert.Equal(t, 300, comp1.Width) 202 | assert.Equal(t, 250, comp1.Height) 203 | if assert.NotNil(t, comp1.StaticResource) { 204 | assert.Equal(t, "image/jpeg", comp1.StaticResource.CreativeType) 205 | assert.Equal(t, "http://demo.tremormedia.com/proddev/vast/Blistex1.jpg", comp1.StaticResource.URI) 206 | } 207 | if assert.Len(t, comp1.TrackingEvents, 1) { 208 | assert.Equal(t, "creativeView", comp1.TrackingEvents[0].Event) 209 | assert.Equal(t, "http://myTrackingURL/firstCompanionCreativeView", comp1.TrackingEvents[0].URI) 210 | } 211 | assert.Equal(t, "http://www.tremormedia.com", comp1.CompanionClickThrough.CDATA) 212 | 213 | comp2 := crea2.CompanionAds.Companions[1] 214 | assert.Equal(t, 728, comp2.Width) 215 | assert.Equal(t, 90, comp2.Height) 216 | if assert.NotNil(t, comp2.StaticResource) { 217 | assert.Equal(t, "image/jpeg", comp2.StaticResource.CreativeType) 218 | assert.Equal(t, "http://demo.tremormedia.com/proddev/vast/728x90_banner1.jpg", comp2.StaticResource.URI) 219 | } 220 | assert.Equal(t, "http://www.tremormedia.com", comp2.CompanionClickThrough.CDATA) 221 | } 222 | } 223 | } 224 | } 225 | } 226 | } 227 | 228 | func TestInlineLinearDurationUndefined(t *testing.T) { 229 | v, _, _, err := loadFixture("testdata/vast_inline_linear-duration_undefined.xml") 230 | if !assert.NoError(t, err) { 231 | return 232 | } 233 | 234 | assert.Equal(t, "2.0", v.Version) 235 | if assert.Len(t, v.Ads, 1) { 236 | ad := v.Ads[0] 237 | if assert.NotNil(t, ad.InLine) { 238 | inline := ad.InLine 239 | if assert.Len(t, inline.Creatives, 1) { 240 | crea1 := inline.Creatives[0] 241 | if assert.NotNil(t, crea1.Linear) { 242 | linear := crea1.Linear 243 | assert.Equal(t, Duration(0), linear.Duration) 244 | } 245 | } 246 | } 247 | } 248 | } 249 | 250 | func TestInlineNonLinear(t *testing.T) { 251 | v, _, _, err := loadFixture("testdata/vast_inline_nonlinear.xml") 252 | if !assert.NoError(t, err) { 253 | return 254 | } 255 | 256 | assert.Equal(t, "2.0", v.Version) 257 | if assert.Len(t, v.Ads, 1) { 258 | ad := v.Ads[0] 259 | assert.Equal(t, "602678", ad.ID) 260 | assert.Nil(t, ad.Wrapper) 261 | assert.Equal(t, 0, ad.Sequence) 262 | if assert.NotNil(t, ad.InLine) { 263 | inline := ad.InLine 264 | assert.Equal(t, "Acudeo Compatible", inline.AdSystem.Name) 265 | assert.Equal(t, "NonLinear Test Campaign 1", inline.AdTitle.CDATA) 266 | assert.Equal(t, "NonLinear Test Campaign 1", inline.Description.CDATA) 267 | assert.Equal(t, "http://mySurveyURL/survey", inline.Survey.CDATA) 268 | if assert.Len(t, inline.Errors, 1) { 269 | assert.Equal(t, "http://myErrorURL/error", inline.Errors[0].CDATA) 270 | } 271 | if assert.Len(t, inline.Impressions, 1) { 272 | assert.Equal(t, "http://myTrackingURL/impression", inline.Impressions[0].URI) 273 | } 274 | if assert.Len(t, inline.Creatives, 2) { 275 | crea1 := inline.Creatives[0] 276 | assert.Equal(t, "602678-NonLinear", crea1.AdID) 277 | assert.Nil(t, crea1.Linear) 278 | assert.Nil(t, crea1.CompanionAds) 279 | if assert.NotNil(t, crea1.NonLinearAds) { 280 | nonlin := crea1.NonLinearAds 281 | if assert.Len(t, nonlin.TrackingEvents, 5) { 282 | assert.Equal(t, nonlin.TrackingEvents[0].Event, "creativeView") 283 | assert.Equal(t, nonlin.TrackingEvents[0].URI, "http://myTrackingURL/nonlinear/creativeView") 284 | assert.Equal(t, nonlin.TrackingEvents[1].Event, "expand") 285 | assert.Equal(t, nonlin.TrackingEvents[1].URI, "http://myTrackingURL/nonlinear/expand") 286 | } 287 | if assert.Len(t, nonlin.NonLinears, 2) { 288 | assert.Equal(t, "image/jpeg", nonlin.NonLinears[0].StaticResource.CreativeType) 289 | assert.Equal(t, "http://demo.tremormedia.com/proddev/vast/50x300_static.jpg", strings.TrimSpace(nonlin.NonLinears[0].StaticResource.URI)) 290 | assert.Equal(t, "image/jpeg", nonlin.NonLinears[1].StaticResource.CreativeType) 291 | assert.Equal(t, "http://demo.tremormedia.com/proddev/vast/50x450_static.jpg", strings.TrimSpace(nonlin.NonLinears[1].StaticResource.URI)) 292 | assert.Equal(t, "http://www.tremormedia.com", strings.TrimSpace(nonlin.NonLinears[1].NonLinearClickThrough.CDATA)) 293 | } 294 | } 295 | 296 | crea2 := inline.Creatives[1] 297 | assert.Equal(t, "602678-Companion", crea2.AdID) 298 | assert.Nil(t, crea2.NonLinearAds) 299 | assert.Nil(t, crea2.Linear) 300 | if assert.NotNil(t, crea2.CompanionAds) { 301 | if assert.Len(t, crea2.CompanionAds.Companions, 2) { 302 | comp1 := crea2.CompanionAds.Companions[0] 303 | assert.Equal(t, 300, comp1.Width) 304 | assert.Equal(t, 250, comp1.Height) 305 | if assert.NotNil(t, comp1.StaticResource) { 306 | assert.Equal(t, "application/x-shockwave-flash", comp1.StaticResource.CreativeType) 307 | assert.Equal(t, "http://demo.tremormedia.com/proddev/vast/300x250_companion_1.swf", comp1.StaticResource.URI) 308 | } 309 | assert.Equal(t, "http://www.tremormedia.com", comp1.CompanionClickThrough.CDATA) 310 | 311 | comp2 := crea2.CompanionAds.Companions[1] 312 | assert.Equal(t, 728, comp2.Width) 313 | assert.Equal(t, 90, comp2.Height) 314 | if assert.NotNil(t, comp2.StaticResource) { 315 | assert.Equal(t, "image/jpeg", comp2.StaticResource.CreativeType) 316 | assert.Equal(t, "http://demo.tremormedia.com/proddev/vast/728x90_banner1.jpg", comp2.StaticResource.URI) 317 | } 318 | if assert.Len(t, comp2.TrackingEvents, 1) { 319 | assert.Equal(t, "creativeView", comp2.TrackingEvents[0].Event) 320 | assert.Equal(t, "http://myTrackingURL/secondCompanion", comp2.TrackingEvents[0].URI) 321 | } 322 | assert.Equal(t, "http://www.tremormedia.com", comp2.CompanionClickThrough.CDATA) 323 | } 324 | } 325 | } 326 | } 327 | } 328 | } 329 | 330 | func TestWrapperLinear(t *testing.T) { 331 | v, _, _, err := loadFixture("testdata/vast_wrapper_linear_1.xml") 332 | if !assert.NoError(t, err) { 333 | return 334 | } 335 | 336 | assert.Equal(t, "2.0", v.Version) 337 | if assert.Len(t, v.Ads, 1) { 338 | ad := v.Ads[0] 339 | assert.Equal(t, "602833", ad.ID) 340 | assert.Equal(t, 0, ad.Sequence) 341 | assert.Nil(t, ad.InLine) 342 | if assert.NotNil(t, ad.Wrapper) { 343 | wrapper := ad.Wrapper 344 | assert.Equal(t, true, *wrapper.FallbackOnNoAd) 345 | assert.Equal(t, true, *wrapper.AllowMultipleAds) 346 | assert.Nil(t, wrapper.FollowAdditionalWrappers) 347 | assert.Equal(t, "http://demo.tremormedia.com/proddev/vast/vast_inline_linear.xml", wrapper.VASTAdTagURI.CDATA) 348 | assert.Equal(t, "Acudeo Compatible", wrapper.AdSystem.Name) 349 | if assert.Len(t, wrapper.Errors, 1) { 350 | assert.Equal(t, "http://myErrorURL/wrapper/error", wrapper.Errors[0].CDATA) 351 | } 352 | if assert.Len(t, wrapper.Impressions, 1) { 353 | assert.Equal(t, "http://myTrackingURL/wrapper/impression", wrapper.Impressions[0].URI) 354 | } 355 | 356 | if assert.Len(t, wrapper.Creatives, 3) { 357 | crea1 := wrapper.Creatives[0] 358 | assert.Equal(t, "602833", crea1.AdID) 359 | assert.Nil(t, crea1.NonLinearAds) 360 | assert.Nil(t, crea1.CompanionAds) 361 | if assert.NotNil(t, crea1.Linear) { 362 | linear := crea1.Linear 363 | if assert.Len(t, linear.TrackingEvents, 11) { 364 | assert.Equal(t, linear.TrackingEvents[0].Event, "creativeView") 365 | assert.Equal(t, linear.TrackingEvents[0].URI, "http://myTrackingURL/wrapper/creativeView") 366 | assert.Equal(t, linear.TrackingEvents[1].Event, "start") 367 | assert.Equal(t, linear.TrackingEvents[1].URI, "http://myTrackingURL/wrapper/start") 368 | } 369 | assert.Nil(t, linear.VideoClicks) 370 | } 371 | 372 | crea2 := wrapper.Creatives[1] 373 | assert.Equal(t, "", crea2.AdID) 374 | assert.Nil(t, crea2.CompanionAds) 375 | assert.Nil(t, crea2.NonLinearAds) 376 | if assert.NotNil(t, crea2.Linear) { 377 | if assert.Len(t, crea2.Linear.VideoClicks.ClickTrackings, 1) { 378 | assert.Equal(t, "http://myTrackingURL/wrapper/click", crea2.Linear.VideoClicks.ClickTrackings[0].URI) 379 | } 380 | } 381 | 382 | crea3 := wrapper.Creatives[2] 383 | assert.Equal(t, "602833-NonLinearTracking", crea3.AdID) 384 | assert.Nil(t, crea3.CompanionAds) 385 | assert.Nil(t, crea3.Linear) 386 | if assert.NotNil(t, crea3.NonLinearAds) { 387 | if assert.Len(t, crea3.NonLinearAds.TrackingEvents, 1) { 388 | assert.Equal(t, "creativeView", crea3.NonLinearAds.TrackingEvents[0].Event) 389 | assert.Equal(t, "http://myTrackingURL/wrapper/creativeView", crea3.NonLinearAds.TrackingEvents[0].URI) 390 | } 391 | } 392 | } 393 | } 394 | } 395 | } 396 | 397 | func TestWrapperNonLinear(t *testing.T) { 398 | v, _, _, err := loadFixture("testdata/vast_wrapper_nonlinear_1.xml") 399 | if !assert.NoError(t, err) { 400 | return 401 | } 402 | 403 | assert.Equal(t, "2.0", v.Version) 404 | if assert.Len(t, v.Ads, 1) { 405 | ad := v.Ads[0] 406 | assert.Equal(t, "602867", ad.ID) 407 | assert.Equal(t, 0, ad.Sequence) 408 | assert.Nil(t, ad.InLine) 409 | if assert.NotNil(t, ad.Wrapper) { 410 | wrapper := ad.Wrapper 411 | assert.Equal(t, "http://demo.tremormedia.com/proddev/vast/vast_inline_nonlinear2.xml", wrapper.VASTAdTagURI.CDATA) 412 | assert.Equal(t, "Acudeo Compatible", wrapper.AdSystem.Name) 413 | if assert.Len(t, wrapper.Errors, 1) { 414 | assert.Equal(t, "http://myErrorURL/wrapper/error", wrapper.Errors[0].CDATA) 415 | } 416 | if assert.Len(t, wrapper.Impressions, 1) { 417 | assert.Equal(t, "http://myTrackingURL/wrapper/impression", wrapper.Impressions[0].URI) 418 | } 419 | 420 | if assert.Len(t, wrapper.Creatives, 2) { 421 | crea1 := wrapper.Creatives[0] 422 | assert.Equal(t, "602867", crea1.AdID) 423 | assert.Nil(t, crea1.NonLinearAds) 424 | assert.Nil(t, crea1.CompanionAds) 425 | assert.NotNil(t, crea1.Linear) 426 | 427 | crea2 := wrapper.Creatives[1] 428 | assert.Equal(t, "602867-NonLinearTracking", crea2.AdID) 429 | assert.Nil(t, crea2.CompanionAds) 430 | assert.Nil(t, crea2.Linear) 431 | if assert.NotNil(t, crea2.NonLinearAds) { 432 | if assert.Len(t, crea2.NonLinearAds.TrackingEvents, 5) { 433 | assert.Equal(t, "creativeView", crea2.NonLinearAds.TrackingEvents[0].Event) 434 | assert.Equal(t, "http://myTrackingURL/wrapper/nonlinear/creativeView/creativeView", crea2.NonLinearAds.TrackingEvents[0].URI) 435 | } 436 | } 437 | } 438 | } 439 | } 440 | } 441 | 442 | func TestSpotXVpaid(t *testing.T) { 443 | v, _, _, err := loadFixture("testdata/spotx_vpaid.xml") 444 | if !assert.NoError(t, err) { 445 | return 446 | } 447 | 448 | assert.Equal(t, "2.0", v.Version) 449 | if assert.Len(t, v.Ads, 1) { 450 | ad := v.Ads[0] 451 | assert.Equal(t, "1130507-1818483", ad.ID) 452 | assert.Nil(t, ad.Wrapper) 453 | if assert.NotNil(t, ad.InLine) { 454 | inline := ad.InLine 455 | assert.Equal(t, "SpotXchange", inline.AdSystem.Name) 456 | assert.Equal(t, "1.0", inline.AdSystem.Version) 457 | assert.Equal(t, "IntegralAds_VAST_2_0_Ad_Wrapper", inline.AdTitle.CDATA) 458 | assert.Equal(t, "", inline.Description.CDATA) 459 | 460 | if assert.Len(t, inline.Creatives, 2) { 461 | crea1 := inline.Creatives[0] 462 | assert.Equal(t, 1, crea1.Sequence) 463 | if assert.NotNil(t, crea1.Linear) { 464 | linear := crea1.Linear 465 | adParam, err := os.Open("testdata/spotx_adparameters.txt") 466 | if err != nil { 467 | assert.FailNow(t, "Cannot open adparams file") 468 | } 469 | defer adParam.Close() 470 | b, _ := ioutil.ReadAll(adParam) 471 | assert.Equal(t, linear.AdParameters.Parameters, string(b)) 472 | if assert.Len(t, crea1.Linear.MediaFiles, 1) { 473 | media1 := crea1.Linear.MediaFiles[0] 474 | assert.Equal(t, "progressive", media1.Delivery) 475 | assert.Equal(t, "application/javascript", media1.Type) 476 | assert.Equal(t, 300, media1.Width) 477 | assert.Equal(t, 250, media1.Height) 478 | assert.Equal(t, "https://cdn.spotxcdn.com/integration/instreamadbroker/v1/instreamadbroker/beta.js", media1.URI) 479 | } 480 | } 481 | crea2 := inline.Creatives[1] 482 | assert.Equal(t, 1, crea2.Sequence) 483 | if assert.NotNil(t, crea2.CompanionAds) { 484 | if assert.Len(t, crea2.CompanionAds.Companions, 3) { 485 | companionAds1 := crea2.CompanionAds.Companions[0] 486 | assert.Equal(t, 300, companionAds1.Width) 487 | assert.Equal(t, 250, companionAds1.Height) 488 | assert.Equal(t, "medium_rectangle", companionAds1.ID) 489 | if assert.NotNil(t, companionAds1.IFrameResource) { 490 | assert.Equal(t, "https://search.spotxchange.com/banner?_a=137530&_p=spotx&_z=1&_m=eNpVz99vgjAQB%2FD%2BLTyP0rM%2FLCZ7WOKWbFF5GJr4RAotUBUwgkHZ9rdvVB8W%2B3BJv3eXT45hQhBCwAnhEkKQKA2lZCHLhJYSwExTSgSkqcpBCEIIIKBTTglaf6JQ4glleAISAxOo7LpjOwuCvu9xrip7uJaq1tdK1ThrqgB9eefWnJLibLU385hRoTQ8FylPnSNzMgUtc6EpdY73dB%2B3bVKbflwYg6xp9tYkuqmUrceoNeqUlbg9Nt0lG7HCOGkcVGdtTZ2Z5IHyneU7zHea%2F8D93A6bcPR7e2i1XizuBW4R4oJcKPDx%2B99y5TuKD%2BU23rPlrhiW849dFG%2FstnqrVsPLJYrfYTm88mi%2BZ6uheP4DQppvcA%3D%3D&_l=eNplj01rwkAYhN%2FfsleLvJv9yEbooUXoxU1BDKW5yGazNUk1SZMNtVr%2Fe9VgofQyh5mHGYYqqrhiEEYykIyDABWgAi4wlAAUKKdcRIpD3zZ%2BD5MJSBpB3dQOEI6EkdmRZGVOZgSniEySO9Kar2bwNwcvVt%2B6%2BpeJrtCQbUu7dntbmHrj1m%2FOXXOKiOfYDp3xLl%2FvTPfufLs11v1n8cKeThAni8UoCDhVAparR%2FDdsGuLxrttP7XNDlBxRjORW5pxhRYjIYULc8nDyDLm8vH3%2BRxy%2BBhM7a3p%2FdhKWSgC8XcGvuNK8%2FQp2b%2BuiiKdb1AfNNfVw6eukkBXcfk8T4L0ZVnqQ3L%2FAyjAYKo%3D&_t=eNotj9Fu4yAQRf0tPFdbwK5dE%2B1b1aiVnGpXlVaJKlkDTGxaYyxMtsmm%2BfeFuDzA3MO9wyBhHNFn9Z5WQCtd6vt7xrCSOa2YlLBnZUkpZZnqwYyZ8x2MRmVySZ2JRW2gNZoIorWUpeSasbq4y%2BOOulaFrrGGUtccyQ3ZO28hRO%2F66TFKayy24TRhJMZCh2%2B3ndnHi0%2BjQx9hTmlUPZquTyl%2Bd5XBDq1ydhrQ4ph4hAZka%2BwhijTRwbYeVYCxG9KzM4JXfTtPqIg4kwk8WAzo56Q0%2FjUKU4XHkI55cuHYLriFafqekF3iwE7jQAS%2FIS5m%2BSWuLGM%2FKM1AVOI8i1KQbnASBrIygq6uYHJzWACrGY8sv%2F42gbyoKlpEVAiiTDh9m9gS9NgZNyZWsjqiKprcYQz%2B6uM8X3rppVfJ2eqSfW3Wv05b%2Fvy%2Be2j4br352NqN3f55Ys2%2Fj8%2FNQ3d8WTfHhv8eXl6bn%2F8BtbOdiA%3D%3D&_b=eNozYEjRB8KUpCSzJKMUQ0NLE1NjIJmaYplskmKZaplolmJplKqXUZKbw%2BAX6uODIGp83T0NI7MysiOzHCv8wj0rI3MDq3yNwnKAtEFkuKtBVDiQrvI1jqxKtwUAEJMfUw%3D%3D&resource_type=iframe", companionAds1.IFrameResource.CDATA) 491 | } 492 | companionAds2 := crea2.CompanionAds.Companions[1] 493 | assert.Equal(t, 300, companionAds2.Width) 494 | assert.Equal(t, 250, companionAds2.Height) 495 | assert.Equal(t, "medium_rectangle", companionAds2.ID) 496 | if assert.NotNil(t, companionAds2.HTMLResource) { 497 | htmlResource, err := os.Open("testdata/spotx_html_resource.html") 498 | if err != nil { 499 | assert.FailNow(t, "Cannot open spotx html resource file") 500 | } 501 | defer htmlResource.Close() 502 | b, _ := ioutil.ReadAll(htmlResource) 503 | assert.Equal(t, companionAds2.HTMLResource.HTML, string(b)) 504 | } 505 | companionAds3 := crea2.CompanionAds.Companions[2] 506 | assert.Equal(t, 300, companionAds3.Width) 507 | assert.Equal(t, 250, companionAds3.Height) 508 | assert.Equal(t, "medium_rectangle", companionAds3.ID) 509 | if assert.NotNil(t, companionAds3.StaticResource) { 510 | assert.Equal(t, "image/gif", companionAds3.StaticResource.CreativeType) 511 | assert.Equal(t, "https://search.spotxchange.com/banner?_a=137530&_p=spotx&_z=1&_m=eNpVz99vgjAQB%2FD%2BLTyP0rM%2FLCZ7WOKWbFF5GJr4RAotUBUwgkHZ9rdvVB8W%2B3BJv3eXT45hQhBCwAnhEkKQKA2lZCHLhJYSwExTSgSkqcpBCEIIIKBTTglaf6JQ4glleAISAxOo7LpjOwuCvu9xrip7uJaq1tdK1ThrqgB9eefWnJLibLU385hRoTQ8FylPnSNzMgUtc6EpdY73dB%2B3bVKbflwYg6xp9tYkuqmUrceoNeqUlbg9Nt0lG7HCOGkcVGdtTZ2Z5IHyneU7zHea%2F8D93A6bcPR7e2i1XizuBW4R4oJcKPDx%2B99y5TuKD%2BU23rPlrhiW849dFG%2FstnqrVsPLJYrfYTm88mi%2BZ6uheP4DQppvcA%3D%3D&_l=eNplj01rwkAYhN%2FfsleLvJv9yEbooUXoxU1BDKW5yGazNUk1SZMNtVr%2Fe9VgofQyh5mHGYYqqrhiEEYykIyDABWgAi4wlAAUKKdcRIpD3zZ%2BD5MJSBpB3dQOEI6EkdmRZGVOZgSniEySO9Kar2bwNwcvVt%2B6%2BpeJrtCQbUu7dntbmHrj1m%2FOXXOKiOfYDp3xLl%2FvTPfufLs11v1n8cKeThAni8UoCDhVAparR%2FDdsGuLxrttP7XNDlBxRjORW5pxhRYjIYULc8nDyDLm8vH3%2BRxy%2BBhM7a3p%2FdhKWSgC8XcGvuNK8%2FQp2b%2BuiiKdb1AfNNfVw6eukkBXcfk8T4L0ZVnqQ3L%2FAyjAYKo%3D&_t=eNotj9Fu4yAQRf0tPFdbwK5dE%2B1b1aiVnGpXlVaJKlkDTGxaYyxMtsmm%2BfeFuDzA3MO9wyBhHNFn9Z5WQCtd6vt7xrCSOa2YlLBnZUkpZZnqwYyZ8x2MRmVySZ2JRW2gNZoIorWUpeSasbq4y%2BOOulaFrrGGUtccyQ3ZO28hRO%2F66TFKayy24TRhJMZCh2%2B3ndnHi0%2BjQx9hTmlUPZquTyl%2Bd5XBDq1ydhrQ4ph4hAZka%2BwhijTRwbYeVYCxG9KzM4JXfTtPqIg4kwk8WAzo56Q0%2FjUKU4XHkI55cuHYLriFafqekF3iwE7jQAS%2FIS5m%2BSWuLGM%2FKM1AVOI8i1KQbnASBrIygq6uYHJzWACrGY8sv%2F42gbyoKlpEVAiiTDh9m9gS9NgZNyZWsjqiKprcYQz%2B6uM8X3rppVfJ2eqSfW3Wv05b%2Fvy%2Be2j4br352NqN3f55Ys2%2Fj8%2FNQ3d8WTfHhv8eXl6bn%2F8BtbOdiA%3D%3D&_b=eNpFxl0LgjAUgGF%2FkSc%2FEhZ0EUje6AFFibxznuXmsklbGNKPL6%2FihYd350nnZnsA6Onh29m49za9mWASpDpw8jVxC%2BapBqAt4jzhIQUBi%2FfRT0Gsj4kJ1iXEQuEP6uZhk%2Bd%2FPsVaKUxPK6Z3haOO26xZ2gvKoi6Xa30eMas0rmWEtT5%2BAZ5CLzA%3D", companionAds3.StaticResource.URI) 512 | } 513 | if assert.NotNil(t, companionAds3.CompanionClickThrough) { 514 | assert.Equal(t, "https://search.spotxchange.com/click?_a=137530&_p=spotx&_z=1&_m=eNpVz99vgjAQB%2FD%2BLTyP0rM%2FLCZ7WOKWbFF5GJr4RAotUBUwgkHZ9rdvVB8W%2B3BJv3eXT45hQhBCwAnhEkKQKA2lZCHLhJYSwExTSgSkqcpBCEIIIKBTTglaf6JQ4glleAISAxOo7LpjOwuCvu9xrip7uJaq1tdK1ThrqgB9eefWnJLibLU385hRoTQ8FylPnSNzMgUtc6EpdY73dB%2B3bVKbflwYg6xp9tYkuqmUrceoNeqUlbg9Nt0lG7HCOGkcVGdtTZ2Z5IHyneU7zHea%2F8D93A6bcPR7e2i1XizuBW4R4oJcKPDx%2B99y5TuKD%2BU23rPlrhiW849dFG%2FstnqrVsPLJYrfYTm88mi%2BZ6uheP4DQppvcA%3D%3D&_l=eNplj01rwkAYhN%2FfsleLvJv9yEbooUXoxU1BDKW5yGazNUk1SZMNtVr%2Fe9VgofQyh5mHGYYqqrhiEEYykIyDABWgAi4wlAAUKKdcRIpD3zZ%2BD5MJSBpB3dQOEI6EkdmRZGVOZgSniEySO9Kar2bwNwcvVt%2B6%2BpeJrtCQbUu7dntbmHrj1m%2FOXXOKiOfYDp3xLl%2FvTPfufLs11v1n8cKeThAni8UoCDhVAparR%2FDdsGuLxrttP7XNDlBxRjORW5pxhRYjIYULc8nDyDLm8vH3%2BRxy%2BBhM7a3p%2FdhKWSgC8XcGvuNK8%2FQp2b%2BuiiKdb1AfNNfVw6eukkBXcfk8T4L0ZVnqQ3L%2FAyjAYKo%3D&_t=eNotj0tv3CAUhf1bWEcJYMeOPeomqhplMVOp8iwcVUI87tgkYCzMNPPo%2FPeAHRZwz8c5l4s0Wn5k9QFXHFeqVE9PhEAlclwRIfiBlCXGmGRy4HrMnO%2F5qGUm%2BDiCZ3LJXpEFpTnTCjVIKSFKQRUhdfGYxx1ULQtVQ81LVVNAd%2BjgvOUhel9ef0VptQUWzhNEoi3v4e9Drw%2Fx4lOrMESYYxzVALofUoo%2BLjJYw6SzkwELY%2BIRai6Ytsco0kRHyzzIwMfepGdn4F4ObJ5AouaKJu65hQB%2BTkrBPy0hVXAK6ZgnF05sxYxP0%2FeE5BYHdgoMaugdcjFLb3FlGbnHOONN1VznpmxQb5zgBm10gzcLmNwcVkBqQiPLl98mkBdVhYuIigZJHc7fJrIGPfTajYmVpI6oiiZ3HINffJTmay%2B19iop2dyy%2F93lj%2Bnandm1%2B3P3PrzvXl4%2Fu8v2tG3fzNbuL7tW6a59Hn7%2F3P%2F4Ap5ioKM%3D&_b=eNozYMgoKSkottLXLy7IL6lIzkjMS0%2FVS87PZfAL9fFhsEwzsLQ0NzQ0S7GwMDRMNU8yNjA3TEpKTDM0M3MDArCqmih3V5PIrGxDX5d0g8gqr0x%2Fl0hDfxe3bN8sTwO%2FrLDMyKycDD8XEJ1uCwCmYiLM", companionAds3.CompanionClickThrough.CDATA) 515 | } 516 | if assert.NotNil(t, companionAds3.AltText) { 517 | assert.Equal(t, "IntegralAds_VAST_2_0_Ad_Wrapper", companionAds3.AltText) 518 | } 519 | 520 | } 521 | } 522 | 523 | } 524 | exts := *inline.Extensions 525 | if assert.Len(t, exts, 2) { 526 | ext1 := exts[0] 527 | assert.Equal(t, "LR-Pricing", ext1.Type) 528 | assert.Equal(t, "", strings.TrimSpace(string(ext1.Data))) 529 | ext2 := exts[1] 530 | assert.Equal(t, "SpotX-Count", ext2.Type) 531 | assert.Equal(t, "", strings.TrimSpace(string(ext2.Data))) 532 | } 533 | } 534 | } 535 | } 536 | 537 | func TestExtraSpacesVpaid(t *testing.T) { 538 | v, _, _, err := loadFixture("testdata/extraspaces_vpaid.xml") 539 | if !assert.NoError(t, err) { 540 | return 541 | } 542 | 543 | assert.Equal(t, "2.0", v.Version) 544 | if assert.Len(t, v.Ads, 1) { 545 | ad := v.Ads[0] 546 | assert.Equal(t, "1130507-1818483", ad.ID) 547 | assert.Nil(t, ad.Wrapper) 548 | if assert.NotNil(t, ad.InLine) { 549 | inline := ad.InLine 550 | assert.Equal(t, "SpotXchange", inline.AdSystem.Name) 551 | assert.Equal(t, "1.0", inline.AdSystem.Version) 552 | assert.Equal(t, "IntegralAds_VAST_2_0_Ad_Wrapper", inline.AdTitle.CDATA) 553 | assert.Equal(t, "", inline.Description.CDATA) 554 | 555 | if assert.Len(t, inline.Creatives, 1) { 556 | crea1 := inline.Creatives[0] 557 | assert.Equal(t, 1, crea1.Sequence) 558 | if assert.NotNil(t, crea1.Linear) { 559 | linear := crea1.Linear 560 | 561 | assert.Equal(t, " \n \n \n ", linear.AdParameters.Parameters) 562 | if assert.Len(t, crea1.Linear.MediaFiles, 1) { 563 | media1 := crea1.Linear.MediaFiles[0] 564 | assert.Equal(t, "progressive", media1.Delivery) 565 | assert.Equal(t, "application/javascript", media1.Type) 566 | assert.Equal(t, 300, media1.Width) 567 | assert.Equal(t, 250, media1.Height) 568 | assert.Equal(t, "\n https://dummy.com/dummmy.js \n ", media1.URI) 569 | } 570 | } 571 | } 572 | } 573 | } 574 | } 575 | 576 | func TestIcons(t *testing.T) { 577 | v, _, _, err := loadFixture("testdata/vast_adaptv_attempt_attr.xml") 578 | if !assert.NoError(t, err) { 579 | return 580 | } 581 | 582 | assert.Equal(t, "3.0", v.Version) 583 | if assert.Len(t, v.Ads, 1) { 584 | ad := v.Ads[0] 585 | assert.Equal(t, "a583680", ad.ID) 586 | assert.Nil(t, ad.Wrapper) 587 | if assert.NotNil(t, ad.InLine) { 588 | inline := ad.InLine 589 | assert.Equal(t, "Adap.tv", inline.AdSystem.Name) 590 | assert.Equal(t, "1.0", inline.AdSystem.Version) 591 | assert.Equal(t, "Adap.tv Ad Unit", inline.AdTitle.CDATA) 592 | assert.Equal(t, "", inline.Description.CDATA) 593 | 594 | if assert.Len(t, inline.Creatives, 1) { 595 | crea1 := inline.Creatives[0] 596 | if assert.NotNil(t, crea1.Linear) { 597 | if assert.Len(t, crea1.Linear.Icons.Icon, 1) { 598 | icon1 := crea1.Linear.Icons.Icon[0] 599 | assert.Equal(t, "DAA", icon1.Program) 600 | assert.Equal(t, 77, icon1.Width) 601 | assert.Equal(t, 15, icon1.Height) 602 | assert.Equal(t, "right", icon1.XPosition) 603 | assert.Equal(t, "top", icon1.YPosition) 604 | if assert.NotNil(t, icon1.StaticResource) { 605 | assert.Equal(t, "image/png", icon1.StaticResource.CreativeType) 606 | assert.Equal(t, "https://s.aolcdn.com/ads/adchoices.png", icon1.StaticResource.URI) 607 | assert.Equal(t, "https://adinfo.aol.com", icon1.IconClickThrough.CDATA) 608 | } 609 | } 610 | } 611 | } 612 | } 613 | } 614 | } 615 | 616 | func TestUniversalAdID(t *testing.T) { 617 | v, _, _, err := loadFixture("testdata/vast4_universal_ad_id.xml") 618 | if !assert.NoError(t, err) { 619 | return 620 | } 621 | 622 | assert.Equal(t, "4.0", v.Version) 623 | if assert.Len(t, v.Ads, 1) { 624 | ad := v.Ads[0] 625 | assert.Equal(t, "20008", ad.ID) 626 | if assert.NotNil(t, ad.InLine) { 627 | if assert.NotNil(t, ad.InLine.Extensions) { 628 | if assert.Len(t, ad.InLine.Creatives, 1) { 629 | if assert.NotNil(t, ad.InLine.Creatives[0].UniversalAdID) { 630 | creative := ad.InLine.Creatives[0] 631 | assert.Equal(t, "Ad-ID", creative.UniversalAdID.IDRegistry) 632 | assert.Equal(t, "8465", creative.UniversalAdID.IDValue) 633 | assert.Equal(t, "8465", creative.UniversalAdID.ID) 634 | } 635 | } 636 | } 637 | } 638 | } 639 | } 640 | --------------------------------------------------------------------------------