From 90756ba7838081f359ce40d22226f03fc4021a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20D=C3=A9fago?= Date: Wed, 20 Nov 2024 14:40:58 +0100 Subject: [PATCH 01/68] Improve documentation (#1067) --- .../Player/Player.docc/Articles/playback/playback-article.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Player/Player.docc/Articles/playback/playback-article.md b/Sources/Player/Player.docc/Articles/playback/playback-article.md index ff77c469..74347278 100644 --- a/Sources/Player/Player.docc/Articles/playback/playback-article.md +++ b/Sources/Player/Player.docc/Articles/playback/playback-article.md @@ -15,7 +15,7 @@ Use a ``Player`` to play of one or several items sequentially and be automatical ## Create a Player -You create a player with or without associated items to be played. Since ``Player`` is an [`ObservableObject`](https://developer.apple.com/documentation/combine/observableobject) you usually store an instance as a [`StateObject`](https://developer.apple.com/documentation/swiftui/stateobject) belonging to some [SwiftUI](https://developer.apple.com/documentation/swiftui) view. This not only ensures that the instance remains available for the duration of the view, but also that the view body is automatically updated when the state of the player changes. +You can create a player with or without associated items to be played. Since ``Player`` is an [`ObservableObject`](https://developer.apple.com/documentation/combine/observableobject) you must store an instance as a [`StateObject`](https://developer.apple.com/documentation/swiftui/stateobject) belonging to some [SwiftUI](https://developer.apple.com/documentation/swiftui) view. This not only ensures that the instance remains available for the lifetime of the view, but also that the view body is automatically updated when the state of the player changes. @TabNavigator { @Tab("Empty") { @@ -58,7 +58,7 @@ You create a player with or without associated items to be played. Since ``Playe ## Configure the player -The player can be customized during the instantiation phase by providing a dedicated ``PlayerConfiguration`` object. It is important to note that the configuration is set only at the time of instantiation and remains constant throughout the player's entire life cycle. +The player can be customized during the instantiation phase by providing a dedicated ``PlayerConfiguration`` object. The configuration is set at creation time and cannot be changed afterwards. ## Load custom content From c7be7adb39751669463a765890e97b363f63474a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20D=C3=A9fago?= Date: Sun, 24 Nov 2024 14:49:01 +0100 Subject: [PATCH 02/68] Add method to respond to state changes at the user interface level (#1070) --- Sources/Core/View.swift | 24 +++++++++++++++---- .../state-observation-article.md | 24 ++++++++++++++++++- Sources/Player/UserInterface/View.swift | 20 ++++++++++++++-- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/Sources/Core/View.swift b/Sources/Core/View.swift index bc2a4d11..3c54926b 100644 --- a/Sources/Core/View.swift +++ b/Sources/Core/View.swift @@ -14,16 +14,30 @@ public extension View { /// - publisher: The publisher to subscribe to. /// - keyPath: The key path to extract. /// - binding: The binding to which values must be assigned. - /// - Returns: A view that fills the given binding when the `publisher` emits an event. func onReceive( _ publisher: P, assign keyPath: KeyPath, to binding: Binding ) -> some View where P: Publisher, P.Failure == Never, T: Equatable { - onReceive(publisher.slice(at: keyPath).receiveOnMainThread()) { output in - if binding.wrappedValue != output { - binding.wrappedValue = output - } + onReceive(publisher.slice(at: keyPath).receiveOnMainThread()) { output in + if binding.wrappedValue != output { + binding.wrappedValue = output } + } + } + + /// Observes values emitted by the given publisher at the specified key path. + /// + /// - Parameters: + /// - publisher: The publisher to subscribe to. + /// - keyPath: The key path to extract. + /// - action: A closure to run when the value changes, executed on the main thread. The action is not called for + /// the first value that the publisher might provide upon subscription. + func onReceive( + _ publisher: P, + at keyPath: KeyPath, + perform action: @escaping (T) -> Void + ) -> some View where P: Publisher, P.Failure == Never, T: Equatable { + onReceive(publisher.slice(at: keyPath).dropFirst().receiveOnMainThread(), perform: action) } } diff --git a/Sources/Player/Player.docc/Articles/state-observation/state-observation-article.md b/Sources/Player/Player.docc/Articles/state-observation/state-observation-article.md index b8584c0a..4c8f3e50 100644 --- a/Sources/Player/Player.docc/Articles/state-observation/state-observation-article.md +++ b/Sources/Player/Player.docc/Articles/state-observation/state-observation-article.md @@ -47,7 +47,7 @@ For this reason ``Player`` does not publish time updates automatically. Explicit - Use ``Player/periodicTimePublisher(forInterval:queue:)`` for periodic time updates. - Use ``Player/boundaryTimePublisher(for:queue:)`` to detect time traversal. -When implementing a user interface, you should rather use ``ProgressTracker`` to observe progress changes without the need for explicit time update subscription. +When implementing a user interface you should rather use ``ProgressTracker`` to observe progress changes without the need for explicit time update subscription. ### Explicitly subscribe to frequent updates @@ -76,6 +76,28 @@ struct PlaybackView: View { Check ``PlayerProperties`` for the list of all properties that are available for explicit observation. +### Respond to state updates + +You can respond to state updates at the view level using the ``SwiftUICore/View/onReceive(player:at:perform:)`` modifier. This can be useful to trigger actions when some specific state is reached, for example when ending playback of an item: + +```swift +struct PlaybackView: View { + @StateObject private var player = Player( + item: .simple(url: URL(string: "https://www.server.com/master.m3u8")!) + ) + + var body: some View { + ZStack { + VideoView(player: player) + } + .onReceive(player: player, at: \.playbackState) { playbackState in + guard playbackState == .ended else { return } + // ... + } + } +} +``` + ### Use SwiftUI property wrappers wisely With SwiftUI it is especially important to [properly annotate](https://developer.apple.com/documentation/swiftui/model-data) properties so that changes to your models correctly drive updates to your user interface. diff --git a/Sources/Player/UserInterface/View.swift b/Sources/Player/UserInterface/View.swift index d2acad79..9b0933de 100644 --- a/Sources/Player/UserInterface/View.swift +++ b/Sources/Player/UserInterface/View.swift @@ -13,8 +13,6 @@ public extension View { /// - player: The player. /// - keyPath: The key path to extract. /// - binding: The binding to which the value must be assigned. - /// - Returns: A view that fills the given binding when the player's publisher emits an - /// event. /// /// > Warning: Be careful to associate often updated state to local view scopes to avoid unnecessary view body refreshes. Please /// refer to for more information. @@ -28,6 +26,24 @@ public extension View { } } + /// Observes values emitted by the given player's publisher. + /// + /// - Parameters: + /// - player: The player. + /// - keyPath: The key path to extract. + /// - action: A closure to run when the value changes. + @ViewBuilder + func onReceive(player: Player?, at keyPath: KeyPath, perform action: @escaping (T) -> Void) -> some View where T: Equatable { + if let player { + onReceive(player.propertiesPublisher, at: keyPath, perform: action) + } + else { + self + } + } +} + +public extension View { /// Enable in-app Picture in Picture support. /// /// - Parameter persistable: The object to persist during Picture in Picture. From e7a9d866e8d3443f56208dacd9a37052d14df683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20D=C3=A9fago?= Date: Sun, 24 Nov 2024 18:38:34 +0100 Subject: [PATCH 03/68] Add article about resource optimization (#1072) --- .../Analytics.docc/events-article.md | 2 +- .../Analytics.docc/page-views-article.md | 6 +- .../Analytics.docc/user-consent-article.md | 4 +- .../optimization/optimization-article.md | 62 ++++++++++++++++++ .../optimization/optimization-card.jpg | Bin 0 -> 24311 bytes .../state-observation-article.md | 2 +- ...m-encoding-and-packaging-advice-article.md | 8 +-- .../Articles/tracking/tracking-article.md | 2 +- Sources/Player/Player.docc/PillarboxPlayer.md | 1 + 9 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 Sources/Player/Player.docc/Articles/optimization/optimization-article.md create mode 100644 Sources/Player/Player.docc/Articles/optimization/optimization-card.jpg diff --git a/Sources/Analytics/Analytics.docc/events-article.md b/Sources/Analytics/Analytics.docc/events-article.md index e5cea504..b49c1765 100644 --- a/Sources/Analytics/Analytics.docc/events-article.md +++ b/Sources/Analytics/Analytics.docc/events-article.md @@ -10,7 +10,7 @@ Better understand how your app functionalities are used. As a product team you need to better understand which features are popular and which ones aren't. -The PillarboxAnalytics framework provides a way to send arbitrary events so that your analysts can better understand your users and help you lead your product in the right direction. +The ``PillarboxAnalytics`` framework provides a way to send arbitrary events so that your analysts can better understand your users and help you lead your product in the right direction. > Important: Tracking must be properly setup first. Please refer to for more information. diff --git a/Sources/Analytics/Analytics.docc/page-views-article.md b/Sources/Analytics/Analytics.docc/page-views-article.md index dc52dffc..0f907199 100644 --- a/Sources/Analytics/Analytics.docc/page-views-article.md +++ b/Sources/Analytics/Analytics.docc/page-views-article.md @@ -8,7 +8,7 @@ Identify where users navigate within your app. ## Overview -As a product team you need to better understand where users navigate within your app. The PillarboxAnalytics framework provides a way to track views as they are brought on screen. This makes it possible to improve on user journeys and make your content more discoverable. +As a product team you need to better understand where users navigate within your app. The ``PillarboxAnalytics`` framework provides a way to track views as they are brought on screen. This makes it possible to improve on user journeys and make your content more discoverable. > Important: Tracking must be properly setup first. Please refer to for more information. @@ -64,7 +64,7 @@ private extension HomeView { ### Track page views in UIKit -View controllers commonly represent screens in a UIKit application. The PillarboxAnalytics framework provides a streamlined way to associate page view data with a view controller by having it conform to the ``PageViewTracking`` protocol: +View controllers commonly represent screens in a UIKit application. The ``PillarboxAnalytics`` framework provides a streamlined way to associate page view data with a view controller by having it conform to the ``PageViewTracking`` protocol: ```swift final class HomeViewController: UIViewController { @@ -103,7 +103,7 @@ Only a container can namely decide for which child (or children) page views shou - If page views must be automatically forwarded to all children of a container no additional work is required. - If page views must be automatically forwarded to only selected children, though, then a container must conform to the `ContainerPageViewTracking` protocol to declare which children must be considered active for measurements. -> Tip: The PillarboxAnalytics framework provides native support for standard UIKit containers without any additional work. +> Tip: The ``PillarboxAnalytics`` framework provides native support for standard UIKit containers without any additional work. ### Trigger page views manually diff --git a/Sources/Analytics/Analytics.docc/user-consent-article.md b/Sources/Analytics/Analytics.docc/user-consent-article.md index b2cf019c..73931f61 100644 --- a/Sources/Analytics/Analytics.docc/user-consent-article.md +++ b/Sources/Analytics/Analytics.docc/user-consent-article.md @@ -8,9 +8,9 @@ Take into account user choices about the data they are willing to share. ## Overview -The PillarboxAnalytics framework does not directly implement user consent management but provides a way to forward user consent choices to the Commanders Act and comScore SDKs so that user wishes can be properly taken into account at the data processing level. +The ``PillarboxAnalytics`` framework does not directly implement user consent management but provides a way to forward user consent choices to the Commanders Act and comScore SDKs so that user wishes can be properly taken into account at the data processing level. -> Note: Do not worry if you observe analytics-related network traffic with a proxy tool like [Charles](https://www.charlesproxy.com). The PillarboxAnalytics framework still sends data but user consent is transmitted in each request payload for server-side processing. +> Note: Do not worry if you observe analytics-related network traffic with a proxy tool like [Charles](https://www.charlesproxy.com). The ``PillarboxAnalytics`` framework still sends data but user consent is transmitted in each request payload for server-side processing. ### Setup an analytics data source diff --git a/Sources/Player/Player.docc/Articles/optimization/optimization-article.md b/Sources/Player/Player.docc/Articles/optimization/optimization-article.md new file mode 100644 index 00000000..15cef357 --- /dev/null +++ b/Sources/Player/Player.docc/Articles/optimization/optimization-article.md @@ -0,0 +1,62 @@ +# Optimization + +@Metadata { + @PageColor(purple) + @PageImage(purpose: card, source: optimization-card, alt: "An image depicting a speedometer.") +} + +Avoid wasting system resources unnecessarily. + +## Overview + +Applications must use system resources—such as CPU, memory, and network bandwidth—efficiently to avoid excessive memory consumption (which could lead to app termination) and to conserve battery life. + +Media playback is frequently highlighted in product specifications as a key indicator of battery performance. This is no coincidence, as activities like video streaming involve significant resource demands: the network interface must wake periodically to download content, the processor must decode it, and the screen must remain active to display it to the user. + +By ensuring your application manages resources responsibly, especially when handling video or audio playback, you enhance the user experience. Not only will users be able to enjoy your content for longer, but they’ll also extend their device’s battery life, enabling more use on a single charge. + +This article discusses a few strategies to reduce resource consumption associated with ``PillarboxPlayer`` in your application. + +## Profile your application + +"We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%." +– Donald Knuth + +Use Instruments to identify optimization opportunities. Focus on the following areas: + +- **Allocations Instrument**: Analyze memory usage to identify excessive consumption associated with your application process. You can filter allocations, such as with the keyword _player_, to pinpoint playback-related resources and verify that their count aligns with your expectations. +- **Time Profiler Instrument**: Detect unusual CPU activity and identify potential bottlenecks in your application's performance. +- **Animation Hitches Instrument**: Investigate frame rate hiccups, particularly when players are displayed in scrollable views, to ensure smooth user interactions. +- **Activity Monitor Instrument**: +Since media playback occurs out-of-process through dedicated media services daemons (e.g., _mediaplaybackd_), use Activity Monitor to analyze their CPU and memory usage. Filter for daemons with names containing _media_ to focus on relevant processes. + +To gain a comprehensive understanding of your application's memory and CPU usage, you should therefore evaluate not only its own process but also the media service daemons it interacts with. + +## Restrict the number of players loaded with content + +An empty ``Player`` instance is lightweight, but once loaded with content, it interacts with media service daemons to handle playback. The more ``Player`` instances your application loads simultaneously, the more CPU, memory, and potentially network resources are therefore consumed. + +To minimize resource usage, aim to keep the number of ``Player`` instances loaded with content as low as possible. Consider these strategies: + +- **Implement a Player Pool**: Instead of creating a new player instance for every need, maintain a pool of reusable players. Borrow a player from the pool when needed and return it when done. +- **Clear Unused Players**: Use ``Player/removeAllItems()`` to empty a player's item queue without destroying the player instance. To reload previously played content, use ``PlayerItemConfiguration/position`` to resume playback from where it was last interrupted. +- **Leverage Thumbnails**: Display thumbnails representing the first frame or video content to create the illusion of instant playback without loading the actual video. This approach is especially effective in scrollable lists with autoplay functionality. +- **Limit Buffering**: Control the player's buffering behavior by setting ``PlayerItemConfiguration/preferredForwardBufferDuration`` in a ``PlayerItem`` configuration. While the default buffering can be quite aggressive, reducing the buffer duration lowers memory usage but increases the likelihood of playback stalling and re-buffering. Use this setting judiciously to balance resource usage and playback stability. + +## Implement autoplay wisely + +Autoplay is a common feature, but its implementation requires careful consideration, as it can lead to unnecessary resource consumption. While ``PillarboxPlayer`` does not provide dedicated APIs for autoplay, if you plan to implement this functionality, consider the following best practices to enhance the user experience: + +- **Make Autoplay Optional**: Always provide a setting to disable autoplay. Some users may find this feature intrusive and prefer to turn it off. Offering this option is not only user-friendly but also environmentally conscious, as it helps conserve resources. +- **Disable Autoplay in Poor Conditions**: Automatically disable autoplay when the user is connected to a mobile network or when [Low Data](https://support.apple.com/en-is/102433) or [Low Power](https://support.apple.com/en-us/101604) modes are enabled. This ensures a better experience by reducing resource consumption in constrained conditions. + +## Configure players to minimize resource usage + +``PillarboxPlayer`` provides settings to help reduce resource usage. Consider the following available options: + +- **Disable Non-Essential Players in Low Data Mode**: Players that serve a purely decorative purpose can be automatically disabled when [Low Data Mode](https://support.apple.com/en-is/102433) is enabled. To do this, set ``PlayerConfiguration/allowsConstrainedNetworkAccess`` to `false` in the configuration provided at player creation. +- **Provide a Quality Selector**: Users may want to reduce network bandwidth usage for economical or environmental reasons. Consider offering a range of ``PlayerLimits`` that users can choose from, allowing them to select the best option for their needs. + +## Optimize user interface refreshes + +``PillarboxPlayer`` offers various tools to optimize view layouts in response to . For detailed guidance, please refer to the tutorial. diff --git a/Sources/Player/Player.docc/Articles/optimization/optimization-card.jpg b/Sources/Player/Player.docc/Articles/optimization/optimization-card.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e2406bf2bf3f0460d2fc39e0b3476e43b1fba74b GIT binary patch literal 24311 zcmbrmc_5U17dL**FqWaB#+EEu!bC+dT z+S#u|fRGpGF zBDsjI7`GLJp~#ekA5%$+EMgrU%&|wdktJm@^d)>sD}x>{&sFXY#-`@rdkLj|MSoG6mA zX5a-yrYsi;zRR?$03XV7TRoSB>noVD=CpL=(Rh-eA`AJAj=zr)PsJ*FNL+AmI23r9 z0*A-w3MAQ@^Cat1^5_Fj{arC1*U>~ z%F^jrIu=jD04`ik^_;mF927yv8oiBR>5j1=Jn%;l%OmJva9l9V1wMcUN24h*mTZoR*u3J0grQ0M?OhD3Tegpptf-pC`;742zY!=okWz{wPuYiV#B9vU2qNGDkJ zfD3TsWXNMERJt^<4A}urBms;VL`yv;k%?l34g^t6m$bowLeNu`1vmm8{%cM<&XeL1xJV9*8mAR&c9$d2Xw`R-!el z+!{gz0klJwLoU%Xz%-Ox1j7vEA?qscB@ZE>kR?&r;X?tiU_OSxKq6XGFd`iz#$Zvr zn}Yc0lDIu2Gq4B6rw3AtNGBrfkOBDc;j$1=xT!U(%+jTfZq4Fi;`ZpF1Qr0V0S+># z{{tW7)i}r}fDiL;a-pQ)aRTCCNTN(@gqSkO3=E6`Zy@ZDQyEL)3TGg{Fu|w5H-9q= zOSi)SE$BBuo5e+iSfM2PPplwQyQ%I-T`zsBjMX zH(FpEB>kGl4uP5G^o(1OSRD zODBS95E~RiOD6D%d59HCYrqbTN&-aY;fcLujUl8OurFd|d2E0k1Z1{6(1$&ixDd##iW^QV6@y3+PjNj! zXdV%jPZZFlP~jwqUR1%k@f`mbH;`7=vbt91QN;?QSp!o_3ZQxgH{(IPwB#GeM4(_g z6bx>FQC=&Nlnx&$uecN|5?3CXgmNpd4ji$RR|LcGrJzC;?E&Y)#ZXt^uuHx%!#NQC zB}hOl$V0^lwm5Kr2^Dim9AqFJf=esz9eJP}ARXjg3@m^WfuIO-Kvs|pWCtG(pv5Ur z5Yiao7EpnzBp1RpPKji;?}mzhE0#UF-QTpw6AnQ6|CTJoabXa53_cuHbO8?Db3C~i zR~fpkBpKKUyhtg7K+q7A;>-hQ1G2R%qodrx%LLp;{%=V~X0$YS72(W-`8hj>XI@k(kqNK$kg^0%z5f2hvE(~M}Dj02$#y}Gp@^~4AT&^ML z!MzZG{^mMp3nikWtS${I1e7p7r+S`TB1XdCrcgy8Gr?ML4i!uV0kf1`bUMHZZjVY3 zD%J3rOQjm#z^w;LDjk)o|4A||es}}{eBhWkB#*&xE8rgh zAk@k{3~nfhaWOIQ112f3b~+fgl0PQOSiLF{Pg6&Mgkz+5Gs&mYomV z_Re<9FN&YM3i68{&mFLE=DZr=zW9~+LBXHh4$Jdng*qn*@=P-VePtwtJ?|manmz9f zP0*U1!fsC|Wj=(j>}h7{>~tP17k*RN3GQVWO`ae~$FO30IP+1IkRHbGj={SCzkGB) zk|MT}VU1a14g`3=C1y#W5L@}(34+$dNINsEBD|1hm~M~q!i5Pvny0@%(M)^!O!K4!Z}$7H`JJ4F={gd3B}_17 zCG+9XbJsovW?%j&>V3w@ND@$rfQ?cQfCcmg&S-@|@}P8w6oFSrS6H(!sN$eHf&Wk~ zC#QxfgeQncVs=EvUu{7LS!)i-O@kB_L`nn%AIiscJAfZk0QkhNEFHqIlos@)`jq>c z4-Q78WQF{c&|x*-zpNN7+GSVssANd^WZkbf4}&LJG&6p8=1y;)>piz~V#XI5FV2bD z4*K6a7gohNG+Vd%dH1qGTPK~#VdBjW%D?FS?w&rRbZ z0TBZ3!3bI$locjMw9b{sOA7*i95jwlS>T{>1k?*biw7^3MBBfW7sO9E+yS`*Sm9hv z|JOg*=AO?dI4(Sh9-5t-$pv=XhX|)(4nJ#(d zgU6w$btkWQC%t_WN*hR6VVUf<@k;YkN3jnDKavj4iTt(#-UYV-=R>Q2j7M4n5*YU> zei~ds09Ak%0ka`g_-U3(z<(437!LOLK$1Ws33;IRw@%vQ1rb7zq#dfe=uXM6hCKV- z2h^==73qP0RS48Cl;(dM z!++H)pbRw+C=%d&pebNTNNaSsCC*+F2Eb!Wp}}7wxRL;7@L$wvpaUV9K~gM!|5Ipn ztLpXe^1)L=tg*X)u#rHqgS_to2h2m+$}V4LyI5y|^2dP=~SCxLm$lFelsY z;K|ysTi-t#W{@BQPu^QKIX!*zgT;h}&=n4${8#foUhe(+w%gxq--FBH!2BR85X?b# zLKO$xpsEVrKq7+?{v2`xoDMV|LxC0oHFUtWf0fK58Xi|f#CarCr9tMQLI#8cItI9k zj292k07%rC!I3sAZhgMr@kZ*hjm)bfI-#08_KP~ALz{UoZMpRN#Lo6}x+8zE>|HhY z9d9(BzwveQ{&%e0KmaTDVz+<)TRU4jxNQ47SKk-H&H1MXzRVpKzi~!y>&|my$AWbX z?@I))x^h2MZ@%|>ug1mT0q2W@&p|h#W*4>fNFc)5lo&|6;uRO*M+P~K3E%<*4h1a+ zO*evfLnI~z6hTMTjsm}dS|E)7EwJ#;JiOOH^)a3bod_!9rrj`(u7%AoV!cV<b`B~7+i%%w9*k1_>EwbglA0ae;^;M_kyrYWtJT7Dtcu z`&#%N$#ZOqXsgbAmCyO0B2x4AXF5UmC2XYqhmeG`bRC~GF8qCK(73&J`1J2Ln}vI_z6K8*KQIri5fUb4 z&{d$6S!xEBqQQg&!6#5CgECR!fndOi@kWVA0$Nxah~Oj#_CpfM6BV2g>%kYePyt6k zDMDfhA(=HTm$Rt!(d!+f>!#Ypw^^b+-;_1HR}0Gqt9(fAU4C0!{pSMbs{uW7ctWzm zoBbTmx=sw1*K+oc=<%A5eyg9Eyg&7^r6E`7N5(?PT)cF%j^i2a2YeRyE+2YVn1B5F z_u$iPR^CtLZhtS-N(|WZS;2tdPBSBT5uD}q7-kumy_cOllyW2#0P+}nF&&eqfh1w4 zW26iWX#zHquof2z2fT+s<0JEt_~fnOLkJYXmGKyIGbw{0Pm?br^SjgFzlH!;YxZKp z4DsBlAI1JpD-w?$2^@O<{`imTobyNan*W%491^Kj4b@u+S2|<^1G{V zI8943Q(RaCDxHsv8HPs^49NC$%nUOlMdm#egsUNpASD&;m=u{ojyu19I z@VTHt-I+OD1u-dh+*PwX#m)~08*I|jl(QHY+gJNNNb4xUDHrYnDzh*$R}g?ipaR6; zXrNG$>ZB_Y!<+$^{RRDof-Y8hiV2(J1^88Lrlw9rv5&@!zu3E`h$xzG% zZNB6Jn&MJ)QCz%G0+TYtqW53EVfFT?%+Ng(?a+NZaTD1}cm2YGSICevmLCXByl~Ay z!tukiy0)|^614GXFu%5UNI39kW46-=$H?*@jWuLNuSB2WjiOEyMi-yIJL-O;qO5w; zu;v8=J9$Gzf&uayvYbvr2f;mQPcy{e91sQpj9`wj6AQs!I0tiyB+8cH1m}=Nmw=-I zeM|4hPXg>v4$-tGfCRAjA{L@^5NotQe3vVj(ds>`sg*oABw;f8!1ZBA|FP#Rv+T=5 zGk5QN&zZ56%iK8Bc5`%U_%zdjAhyJ*g2Y2mlK6qUHym)BQuQn|2 z$%7O9ub0Pn=(o>gNgNqgI%l!$qB&E5Kt=e19tq)z44G1hcniS`OhL$z2U{^#2_FiE zJq-gd1HAsWMHF@-1Fi!E*(3Lp^!7r+*_nZ3_^h#&aqPL!7Zn;J#RYDo!+UD{x9Y6g z`f@@?T7P8MvoFm#8WOr5V!heFISW7QgvJGJul}Cd+IjL`baMadj+wd9wwhs74PaMr zuKLU6ubI;~U ziH8e7A-@uRna*wr`UV$s`zHijXo8FE}-}$@0weFpPrnJHeS~aCoXU zkN^TG9r{Q79MIcA6ODgfE{T>4KSU(JgT20Zxb6O#uAAD^_c@+)-%&`39jJNu>esWa zyT8VvQ7zhjDA4dx1n76~zJotRbn~V^&If00)an$1ym{thN8sQaF}&~+QDZD zR!id&q~%~XibN_jMbJvXaNP>pyI9&%PXwY4iBx>13p!N^2%acUPR#_$23pTU@&})H z;uV%i2d{!mjhFwbv9<~*zf<3YrtT<(n9#sZbwlTX+<#iuMh=o1*U^! z6C|5~D1$<>G)hJ40HYf68=+|-3<#k8`OhpB-|WGPf)0Hu8~`*EOytecL<6Qxe`Ssm zY7pd%#@W*EKELA5au^agMP@jeOV=PUs}WTsUG7jB&_>M-n5+ z0Q{AO;QdG@-=io*wiGBOd}JgT08kDn2_l(*7>Xf`1Yp7doPmZ7z!}zL+$OjI6koT=9l^bs~F);sC|RUIvU0?Ax`*I1~mj`Q0GxE06Gn=MVL}CP~g$H4L%TW zpwcCUDn=bIch9sf+{_xiv;V8V*!_Feoc+UW$t{lUTRVq#Tva+c`TALncEx8e%g7&M zzdogX|0#Ss9pgPbGTHKtmxuZK_}2$bYGOMJAEe;6&9 z$+;M+48+1R3{`ZXS^=q;e3I}!E>uGJX{hKCQJpIUB}a$4h!;Iv&_Pfd5f#|eVM;@% z^Q{E=56O-y;i{QSPB;5ghIjsIyS*Z!GKh4|_YVeDplMwIM$lW)8q)KAZT8@};vV|U z{_fl9E<<-puRd^OpgAxaVaRhKU4*a?GBzEQ(cc+B1_@tkpoShcUY2+l1{n$c%F-4A zoX!msE;7yDV0s14DTw!_83>M~@9+f7ku?Kv;d7v+)`$#Bb_uTTW-w}i_EjF%ic6G7X26VGmJ6+9(D(sg zL*oq@8V|z(^zcU*VDAXlL0<*C86aC+<>H;29e!>!{eYDSv}veSixlMG1>XR(5h+B( zVnE|&Xu<~bHME`s&(MMYNC1#%gJ-G7mVUO?A{WINmx@)jMBI9?bwcEi$HvB~nn(2p`9)lypK$e?^K`@{#<^Ef6 zVD$Z&nFF?1sIx}f1tJi}Uv#3L2JZJiWQS1j@AidO2-*Z*qo^UCMNeDc>nH|# zh5-g=|G^IAFDze3z^33D@CgZCB0OVf(ApPGXYoxd4jzaNtQM$H=JDYyo*aKO3$X$! zKdjEZp13_wO`wUS2yQIg* z2QWiIyF8R68F>F0gDe9VGXRcc6s!MPv{vN)efot6OAxpOf(3gSNyv1h4WJl-&3WKS zSqx<>S|%bsL{BTELyq8RqAYPF3`P*Vmez|)(1f6Y0DxgQ zVmLfl_?Q-D``=(O0Y{Kl;6xa9z)XrsEP(kDhL=X9^zdgGP?#uEjejTUXg>!&XF{+5 ztKfm>QV3apaR%t)DukIa73V%&_(9DEH{-5}BRIi1zy(Z1gAf~_KrD}oj*=YBTF}q( z(B!4fiO7VcLkEO7k>HUn#LfVXJeXbqsu1VkyDqeO|IZo^U)mwt{xUL^M zNTfWHJb)4(l!A==TOH6~e#sO?$pR(^d1=K8^hG4l(Ze4KPI_%lU?^N>h(H6+Okq%4 zVDy6+2OhiOrsW||phXXOCJ%q!15N_>hN&DQ9Jl7*=LJwm@#$t6;2kkN3&93#M*IgT zSmHnYK_FCFiA!<@Ee;`yu+@a>yOiSio&co>!tBzc6mUlpJW+iq6|gQL23`&1Iq^(%KaT{bE8ro#0SLtZ&FdD9^qHjp}}@KTmT=k z^Z)~#>EEnEq>t}Wz(A>181^CFh6e!Q@c~$(LUbU)a3U#q?giG-G-xlt?NEGC2Lj$f zyLUSU2rM)rW`J#o5|AGT5i~^0RT}JFp>XLT$^jt9fCGT8VYiAS8D7UASfhx8WwHWL z?ok@CmcRqgP~hbgrWA325}1F((`2Z@L0u0tR4B9rcQd>s#Q#66g9iT?lqBRI;1|@B zm@loFU?K+Kh4DCiEC8t79moKO4kMF{D@^2=E`+*q}ZR<)0vW{089! z+<;dogm`;hy}u85fDl3O!8r^^6Q~hzDjDJePr0GHLyPe*=lsjaz!JbqsGy;04uFHy zM4aw%ivpikAMNdJ7$n~|IKt5rQ>1?aWV;Z0cgQt4Iae7!z7~e z3Cs_a44eQc!hq?PQfp~(1-TCQlf;kz??e#QCX`wbAl&n4ZiJ-QlK6p|jY}`cEK3?Z zQw9!X`;MM6V4??Mz;d_@zytaXK@SK`0I6#P9!aYhI4ih54=iC!20Hz*2lCFE8N9vTI30yJ?VZA$Ah#dd3!GT-8SSQ)=EcQ z5jtoy`OPZ2!ROPh_0Bi2&`&Q}ahC)Sl~in-iK$l}nJW>fWC_K^c&|DwC*W4!zPVd{ zp|(lexH{qLBc-QCce56#3F6CBxAn)ywESuyEBj18E`RvN%a z#=99gdlWMT(rx^CKgxC1G#*mAo%8FoT6ejaOo(lMqvAwHg{<0kdy{8B|6nd(`L5Rt zX0G3BzB(|FXDZ#&R5LM7(T^i(x!!mRKR^E#K6>3|mg()Me0Qx>Y7)$EG1CTb^ZvoA zZrpDyT}zhks`wybhBTYwzvbCRuef0rg- z+Nw{N*VYxSxK#2%-qu#cYq!zLXc1zZs_mhg)-Tk+^S>0QdhRO;`f^bXQ$s$U;KY9W ztUnR%rYYiVA87CWUg}Uw+2mNt07+FPas8)@kE^PR-c!yl+qFYLlC|xPcg3Mrqf+;q zZ4*j?=Pw&3pY}Gc$XBo6b`Yt~oSFSLqu-I@s=u$X{bhsXN^x~<^P(%%vF-#dcc%;~ zBl%jcb%eF2KkpxMOT7_(t=;eK^^={#scaW2SLk1=IJxPtwY9HQQ9@ie&!d1K8PED0 z-JvOqBR;l8Mv_(Oc`qVgFkkowEfZd}+Hm99-HVr!I_YiTME-9M^F2vEb7zKm?ZtYV zr|K2D%64$q^(538zDRfVd8gYQW%4=7xLGcxr&(*_fM(dU9HNz=w?xf#p z10Ii6gyjwThf@!#1ae8=OfYzSWAyyuP{ZR#RJk7t(?v2J_o zOp<0kPHot}goRsc ztFPPs@ns_v_kydG^E;uRmDQh z@RZWX#D(LDHV0e-qo_3xW%_pB^;48e(YjkdaBH5|cCYsTaou@PI97r`i|S?!lcrW)70&{j|yEhs6MG@j?7u;$_dTtgPZ zKc#1+rdB+bd8uu$x@Y;zF*cEwm`($Wj(QQkHr~{7myF39LFXjO?wpIH+?olN`A2f* z=o+i8OOO3+?4DYkmTgejbY9h<{()A>XuLo|h;GAS?VV#M?il$Uof0fj4ANU`u*tK;dt_4(f!gGyR*mxk4$p~*FBYGUA8v_OG<7COwgST?R@zH!s6JMt=Tta zx<>iFsmk`-8x_Td5+=XO!sQpS){iTz+wtP_Z%2JRD+_cT=K-h2X*SGFAi0}Q{nn3qh zk)d74A$6o#@%tWIlC=hH-KDI_^o%dF_r9h~ocg8TK6xeXg#LQDDD?{uHVO4QX4E(L zYD*Lv%^yFqrqGE%zHH(gqjG?ztrPK3ZmpU}%;h{!ne+PMR&sZ4W__vCE0}O`E4IdN zpB!Jl-0G2Z^V}sZ-V?WL)#7!u%AN;P6%W*n4-Z;fzIs{TA++BobYf`lqdRxBvmNcM z*FW&E^s|lKn*7Ce#D*ZA@MeFJiHm2HNK8t(J_pa)oeh3hmAAhc)UZD#@!;xFE_?nc z2jzs=M7N-+(djEZh2%+_zFLH%oFyYgs>6Ley zzikm=x3HMkpP%B9eIY8b{`H!G_ZnJ!i61VxTGAS;SL|PrRN-&#a)gsbhh^)K!`n#QcD-P1 z7ddT@O_~D!U|iYjnn@O-CK8d=4Oc=k zKW6FM?bu@b=pWulo_p+r&}*AW?S$#1Yi&}y zB)k^xpXp`pny%AS-N!SQi&*}-rus$1F%x8y| z-CyhQTl65)Z-@QFhhqnuyUy=9>$84EN*@2{1MRCn{JoU39$|N+R&NR)>pQ8u#{Q1R z2DSAOGFEKa-}tAg9%J5xw%mg4q4^2jb^_-Uj)#}5x3Tn>qLxU}_nh8mlD}b*De;o) zn8W8~*{@~R@$X**2A$Ma@Lum$Tp_b{#K6YdpjlyU!3{TusW0-^N-uBTKI3HS>9ox! zoUf~{LB6-Lb=#YUnh0AFn<=$x7k;XgN~G4V9iPHDO!qqtG_0j(smjzp(79q|kkl%s z%6j#WZT>W0qSsXZ_8zkGQJXy>I}`WvP)?*9+8FF`xzb0?i~TiaZctpDb9_qhnbQ2H ztn2%}y2`rdhZyhpX|68$vblC^^;ltb_~8AOeeY7=He1*$Ue-%}eCP5UxNcD~Tm9ypZ!ccX9VfN5(`J-Jya(Sig?wWdD5&><2kr) z&r`Ly);=YR7~N4uc-c0Ch~ULih0%4=9ha{A-hQwDaAS~FZBejDyLz|lwa8y%t~sAw zeGYNFJo9uz!u8QXMH}fU8DCT0gjCHFU)Hw{4_SPPxnId^bNKxz7Bagw{@1#ShH;T= zeZUk_YsH4zOSbXZqiV2s1xVqsnujBvp>h9n@W9IC4 zX-7fI=hK_lnccn@8^fwy(|xbxmHRf=#yuD9XI+y__xJQSPI82`9El3&iZqeQIUP0S zP@m~2>wLjcZs*lSb#XzhbLupT0%7MI_s=eWIft`nx%CqwOd5hbQjH9nP1jkb-X9E! z6gO|E6uc*Oy|Tm8VxP~79c@okno4t=e_s@y7w=X{W6ue(A7|&KJ9`m_Q%yc4PMM|; z?5wfj+qJImLTYjc@u|R?nBt<|m8_b$%0_$5VF@9?rKsR*9V`uydXY*?vxB$F*=LMYc+@0`8Kg@Vr-X=^^dsX8Z$hRe#eA zN;-Ug;P4%OebuiSIa{C@JRor*O(PwuqW`)Xk^=9uh z+bJvVzVB!o@4Ij|zN(J~b5WYcUpG7}cKliXOI;w8^!iR{h1a{w?=EU^>ufu=TK*5F zJQ+lIz?B=tkuTLfv?Xzm#&->2x6=ZVY3ga(zM93xSv@vWbr$=^_tgi)&Rmi7HdEMyB2Vy>EQ<7V4B@KW*fX ze(Ec{O{%`if#VORddfH~H07cJ%kimNgvo>2agG!%9ghspIrFL$p#y5q$M-c#NpEa1 z>5EsFNuLP(gLRCALQ`bK2kj9?;@~E;vvEVa1OJaFkqu+#msDOWrEBTz1C5c2-Z9^> zT#__R-^wK=_0bDHN_m}3c*SP=ZtJwX=P&zQkN<6zxOJdaitY+G9xGOz^7ooJGxE9h z)lZ9E>ui7J9SYR0y3^-u^7>@B#$c(r!Vk(>d6&}Uz*`pI_U>7i{)NUD>}hGN@%G%e zbr~koV@9Pv_Y1~ZA9{9w%eoAGkyoN+g~6|^T66Cjrk|19=d<;Bk0(!4f8H1Cg%u91 z_OWG{l1xX0O~S80#{o)Yw~UyAZ}K^^R7rw>A;b5ZG4+r?ucpT{+u#c$-&CWMVvhH< z&m{!CF*@k#QW9^f8rs%8#yhg&xz+XDsusUXKQ0@_$DRM#GbKJ5miOC*za!xwp*rZ* z{&hEs4hqj2Wf|6`CyZQPf4OY>tmIYktb}guZv_Sl>J_}K-sHU=?PFdxmY(b4#CO>8 z+ML?^l*Dk%*_P%|X4!9?dg2c@cw1Y^ovK{)TKEB@BBL+EZ@sM*-xiDs`>Hs^2p81Tv1c6WqAmFQ$? zQ(cp-U|<(hB7drZno3z& zM7M8GzGABQA@YV-e(io&--oCK^A*{C?K0gKv#T^-Hvh0oFn3&;TKKX^ddLB#gEA2h zEY>X>-@tpPaa8)6{F5}t+RlW67%$10^NPYPKl%N$WaDmSD;_VuLv3EoV}HJfq!gvd z`%dlM*z?qeH>Rzo)Asw-g*bLQUm>meyec8?l)fRC{L7H>SQCBW)P*6s_i)2YgU%R zp~U~`^}A#RuP#TYOP|NAXp=Q5%r6>=&joJ^Khf@)wV;%i^0oT%8QYW1l`%r>I>zeG zha$Twi%GpQS8sjy33i|tocVl|z&f<%yiA%C+e!<{7SB!*$33-bD;kMq+QA&`;+N$E zEN{-HtPu`gCjKHqVQ6iqcTvy^?fioL!gt+zq4S+{58jL?+n*Ju>byNcRu2@rc0jXe z%`xwpHEaE{zSU+o_+D|%ee}vQfA6)JmsiX#mM$nh3DEBM++Ti;f5Yjg#hjfl^zv9X zmheA6`>l6g?Rg^qTY=wO4u;xtYllX&t43wY6sMLiB6gmflZV9(l$%sDeGWz|9X6Qd zc70wyA^#~qMPET^lYh(cQr=}k5i;I3(RShS3kKbqy<0Z=H*>F6IN-`4X2w``o!PRX zvg&>DE~#H;k;#cvzlJTf0&BuWN(y=(jjZb`4CEWJCs-*cU)7FGkm!8QDD_BE4qv|S z4;GbCXLgF5$Z|Kc&gEy)8|{*#H(U}9dv@V={)ERR1rzw9c}!=ybLr!wz8@Mz)q@MS z#cd_4^QmM<^$pcuSl6lJ+P$)OjrIwP$BeIA_x2pBDf%%YTKAsnmNuaFB0`$w?p&~j z`FmYMoJ#W@LuD7!uLHU=7ct>9nx!(IQ}fM|loF|U0c$&K!9knG+i-l3-jAte8A_>2 z&6hUtSQ*IIyYeWs$LU?Uv&(LMwq0tYcAwkrXO$6xue`z|Q!NTE4kS8uJ&sLwer?*H zu_d3Q&}Cct@|3dddJ8sL%e^mm>aShBv)xw8Ye>DN`0jI`u8S(I^~)c=@U36_G*;_M zer8HfrSi*NYujTVRocDW-ngt-igS1QnY@orv$rX|*;z)q3jYgz+77AM-GOYP(Q2~& z5>-8?Ii*ZgHE)oGmANBs{Uqy^H{GJXbz;TYpZ|#EO0icv?=!y<_&F)qeoQR7bpy}d zm9mZl&GlCahnkvffA7d|`cy{nRGq2F6cwUPh{yAo4B29bwv?umeH(aeI!l*D%c{RHX*pS3S^7Kju<&m`&&3U*BKdu|BVSNE#vp}ITAYd#q%J~4Iik(oL@{BGaL;+jjZX!U+P-n^;b+$5f;%*})* zbVYj)5WIsPUugd<@pF1xmZ@-$fMHRX`$c|1`8zWgCj_^3R*HpZo~cuvr8}wb&fseb zJ=?S4c%R$+vIloc7nOzPA0Iw%Y|DAC=jX~v|6_hd&S}1*s>$0D=0(@nd~eaY5PNq- zOm6kojY`k2I=pT3%eDKs&>Hg2ar#jCitzaJQcd}vImWyuni_$QO!DGg?>q8%+I(tK z4rn>1F1yyp?0#DH?@oG2-b47m?mmmo6+hy>CS?90F}tUyB=4ms#mDPp+ovPcx2nQb z2cu7!8GRrcwnvuVOw4v1Hk2UvE%<7SY*09GfXs9;Z6OKltr{J2Tg-P_pv!+EXlxJ( zay{!{V;93WB7LQ$I(GG&6WQ1iRrPk?W#n4Qe)dW}OgPmew{+)y|38>Vj}C>$`TOuK z_mTM8Lj?;W-iP$R`E*On7wb5j=REk;Kyiapm#0qAA56nufMZt8GC;X0%-U;Be6MX- zx$yRp!*kpL`-XXVF1-qS^i}A1m&DJ^p*h!|(R+TGt_3d6Szz@6}C` zzmx2GG$M)YJ3ZE)T=X~VcGgkm@`?kv2w<1m#ByJ5Z26LQ@ZOYg-CT&u< z>&$hNr1fy#D7!7o6*Y-NZ}zBOD*gtWNYQ-12?0pf+~JPEMw+e&-vP?TwARNrm=1&-(J4n;1XQez~UPD357glGa1d z%ce&hqWdDgZ(8foYfebdon~`R8Iz-nm`+e=<;=1b#cx6d2)A?(vRT~PGE=jcxY)V*DuPVKF4NL5#X#C+v7dP~@=k%Y-I(#hRd;Hz!;|E3qPaE2| zey#eaK6a8*_LFDcYt!L{{X#a?G;cWwKJ-rB0q7RG5v*i>^` z>Dr3UeQGLabvq5Ys^VoFotva5otsSx*Y1-|%K1uTFWHi`rPNLJ*ko;#tuSAxAcwt) zr&gzkal?BD=dj8xs+Z?$yBlq*Pu?E#k51UREUStAOBIc>VndrMUa-FZCvT3E`~H{O4Yo*DZ?HZ9B3%@2st5 z6{DnnrqDhk`RX~N0ZK}av09~aRod}`>aEAABd)p^R=@U6J)TzeRysjCjIVnz>ZL_N zPPxRE#~$3z-|V^n7gg5CkI-X8yR(Qf2-nOA>A$4)c&;7UFKVS!+xy_EEp_-& zMwDb6pR9wRTjw9l)n6yd)hkBJQm5)?P~fa!qsWZYi1y5@@87TcpDzds(QW;UP>BB!(y!^qjx?T2|sdiI}s;@%3t$4Bo*Yx?%)~kD-`&5T-E|Pxa z&vR9Pvhs-qEZA&|MGqu<{W8AVT;AlhyC&Q0M=mezsZYu)ALf?0TO;z%_CKFCRBY(M z1XDeBn8ut591474VzsS_oHu&LkiBx16GqP{DQ-Qck}^H2q32Sko}^G-Ll0lhG5ybu z=I+L`X;n@hmoG#f?-MbanPxxe7tWH&+B%Rw>vMLocn@K^<#qe1uO`KymlKteSXZPx zgRga;skG5bNnc9?6pY_*WAAW1fxRU;he^80~qCxenO7_sI;PJBwE$1$Edh^nj6m(S~HOS1j zG;xQ#;r@ysUO&5ES5itFheX^t0gVL))$h84KRg`gR){=6-l#osXly{_lWO`cL{oTZF7^>KetEw)2)hz zkKZ+p)aJ+-zGa*@#Q4BbneJ~Roa#lnu9%>qd~hxyz4*+JLAA>no(?ImdPIu9iNP8u z-bJ05QF!)H3S};S8I`W%RuMe%c|@pE@+h;=wq`54PHl>9)3oC+tJSZ68~(wnsTuhX z!nKQXEL`7YK6`FeYHRW7{L9*wEk-5xrP3ZtA3mXYI`q@@9}N0$9e(J)iA4B&l(_$H zC87Uj=f_BVE7vOPTCS3ma`H)`Y*Mjue#9ZYSuZR+xBP{SORBzW*(X)s3cT@#zu5>i z-Z6TIN<6f?tsTR*`>YEz$Z5Yb#}hJlv_N`d_k7KYm)y5#IS1KB*xx$s+8p^)??K3= z-^*gY*9y(XUy1p>F{VWF(tZVjyDLt8sm!w5P`>DTdTU@`{+6q(TO(V9QoUxd49Rfa zMl~OMQp8r1yBFshHyknD?XJZ7J$gDh=*y^NzJ;ggQIcr;`pp_~cZxMqe&r7p5G@!o zeTvs>_vNkRR2%!GLQWfR6aKzU!Z>+#%5RAu5z~RUtdqa}!R~77T5H8hA61U^eKUNb zir09{c*r|5#rJucEq^e33F*55Yj#`L+Xx3_^(QO$zfWjZG27DVpnB!lz~r_}`*prk z$_F%e#^j|Qr>^LkQ^l5=@;&Ard1C1xh8_bhO3 zjx7ot@O0aHKtttjSr@pwpbBbSsXleyX7 z`MqX$<;yRMSH5((ECgnY9O1rtv3mA!b#fay*P6<}1ljaen0Gd@^&Pup43%w>6;h&<&$PN%6PYDc?1u69-aUbx*iDDVy#{KPzKiA2@h?Gs&v? zm{6mQb+zvW8ErO(aDHiOlKXKv+r`3_{)>Mf8<8%K~Q-bYomEg(h70v7dGW;%^xHtp`02A{jv%b9nS%Y^d^%ryLCPkwcMGPAjMe^kMa{7Sh$*j|;@ ziDpre*{RENr0=dWSsQ1%e8B2-*%SNtRlXMOaq^@sik3b+Hyggr>}ZX%kHO$n^Y6Zj zZS0qu3^!kCDwF+r__2qE%=86L&uAxKv46Uk=fqhyZg>6mg1ty2KRvjc6ALGZCZGHs zyG33`e3B(7`mLRjGVJf7GFuX8SRrZ*WdY}hcmPN>(IEz+WkjE*XB!G9P0g5o;KB?WWeW8 zm|?rnE}f|mRpotEJtE{fz0kA#C+4q+18x#Vfzxd46qBtd)G@>m&uwmzk8< zt;>ck3>EE-k-TY6y*7WzP<8h*k9T%{96$ZsI-khRoju)@kXX&4{n!`%4E}-zldZc$ zLNgN>p_a;ju%knHo*MFQsqb3_r-UK3Qwmb9H}pjnj+9t-j>&MWY2K|->ao_2{<`4a zGubZ#6!AnMbIG60;`%!(j$4|_)fV0@SFPA3Z7RJlwo$r=r$UEo zRaPvRa{jt9EB4_hZM$l7-ycj#gz&q_vRx-9nl{|hd!8?Jjg5P?>PtT9gFAWi=GrPA z4_uJRAR85kCVT)T2lM7bZ0zH_p(~W>>YgA*VqhX{Lf{#2QP?Q zE37`dv#eahuQKSz9Cf&ba~j6bmwQfzYMXZ$y)9U5O30X5pMI*fe(1|)>PCz3w%PdV z&;$)lFU!s-js`Kxao=t;evW5%-(n4$3iuT*PQc*{m@@&WR_LM6+GxV zHSznfgt_jfKJ2Ix<>j;hzv;v+x%$pd%iT7?SDkWBB?-yoa(q^w6Kt?FCbK6JSs%4_ zBuOW}ov@MlEqr3b*wCq!yZZBzw4+kF<|2hACw|HMQ%~}{#=3gA2lNJ&uU4r|Hpx#| z92mR#sWYc0GqcW9FUR58*#?2*U3-SzmL2K$nc4j6MGF0=p|$ci#f=};a=s0vzZ27Y!tt2n1AqRyl3{9)npUh@h^FeD zL(*F%cgm&=rWE*{aP^B>7a(g&?Z`Sk^Z9bchE$8kO^%x%+4Cs8 ze`GSaVz+9^bZw2~tWZU*3i(KdHUDDi_pi(RH|SzLYlTDLc5zc}toP8Z9Z z{mp5pv?D!DvA8~9P~9afkIKrlYp)+%Aw4>}UOF$b<=3uv`wy z*{PNt`(3L)RW)PvRv}wKX@Sn8#>)D>R}S4&&Wo$rtFR^Q&}GlSzO16vtcH{=&HN2P zZ(4gi^1J;`Xtz#oD=EpmzFQ{x;W0KNdstL0G=tVScq1#c=>g=7c# zzInYEW4HWBS;(!T7CyU9)6nsK5zqMbDdd$hHWJI{Of7b8d}ENSy2TJaQNOLrlZ+susSuO$lo#a8fU5MoAb-e6xw@%WQbF^@8#H zaCM#ap$;xVW?8PDxyL6Zg!T?m>`CqxS(ZIlY(?MO6+Tw^>A)}8d+ON-1~$_g#lDGM z+*lxx!_mgK__dDpgt*=Hmcy)xN2lx-KmSaQ8)i}0f9^SRD!Qtf(a5pORQCXzw`1`A z>@BI3l?|BsrCr(XW&do^D<8i2p>3t7;p$&}t?$)@HEi#)o{h@QK783YWMmxi2lFy6 zq*#nMQhzi0c>9%DWoGG|`QCppO(NH=SLZ*gR^LqBBz2A@*4*Gy5tkRWn=(=nC~n{{ zL2{1Y zHRnKRiq`x6_cl1&8->&zu$-85l#UaViyg~|EeoFc>e%U`zS4SyD97%H5y$*(UheL0 zaLHy<_|C~{^32$k+4kYyDzWCD5|-J(=uywob|@E`|wzfzzQU-(yt1 z(T_fTtP#+6y`reb;`V2y=f2`>UE!%kMT?OeX8S9Yw`fj}SE}xa&gTiLQqQp6|BHTq z{Kwf}aw6V`EiSD4^!n_w;b;7xIuGS=mAC{H(p%~pT(`8zwjF%ERrzMwV(5d0bXDIy z)`|*!&CJml$)qew)LM5-=Th}{lJF*)?)jwe6QQU7)vy02Ckfd0^psw@UG3OK82WQoANiBr(}(Mu!}EU`IPQ*cHyeBm<~_8!YRjp5XF8mp+{ zucmk;=fLzljw-38q@CcDS(N-olI8w|N*Tx6$ztCwgH~^u3J5ny?O(YEPo5o_*s~H? zo04@RJch`nk3cVnM?+^)4?-=i8KZS|HYz~}<}pK@(bdGIrAXwq@-erS@tbFK&_MCQ zBFQTP2FbWv^C#DaX({DNVq&hQ;EZLNgn5lHly^Apb?9|zCR^z={*@T}NQU>#KexnL zB^RaEsbRSu`+WwfWk_RrT-j6-PAjV3wx#EOXLNKNhNV*MsyLDW#T67ZFf36h;k?|~ z^*GEZA+Ld>bsJXPZfbBvjgPMsGS4E%5*A~?3{YsyD1qyD*!}=)2kJ4N<*?<|5%whP zNTh`(_9I?Xm>jyTWF+=&yCC^tSTcrFa7Dqv0D=xPY9m2iBp_}Rw4apxLy*$?Jpwr! z((j)RTC|q4W;CQj-rLXMI1OsWSTN!Z#warwE8$i~0qwk;!Nz5oK|u==z{*DAbHA^b zWffVAT}H%QFVTl zUW=JRN!@eVx9@7IfK<~>3E=2g&kPEZq*#Mv@cu&=X_-WMAZFE>&Dc;>$;=@G#k;=h zhb^kiA7&-p9@Cx!8Fo`kmoDJilbnZ->C;|$nfe7j#AofK7GIF-f^X`ARDunNIHr_O zPYlEH#;xWxXAt%ool<|OSbp~R!}>6nsh#>p2btBRZ?Ttb`_icT>xP!5ru~dA_TyoE zO$5+3%(1c_j4kTrdOY)mny%`cLVhFT(-B1+D6z(YRB!`bO+-|%Nj#>?qWG<=mb#(m zlZzhV&~h5!q^YUDVGF&u?2F=PC#P+hqI5kNTV6vaX_ZqRftSdUt4Y&mG$-DPPt)Q3 z7ajFy@_lt_IsKLa{;$(i5q9&%745d_zGm2l7^&h|qEM=$z+*C`j67*mH`zvi+}(A-7jHaK18uie^ESi~MNJAr5`{$;0~wVZ zRn(G1;9q2vJsVz)8|7>Ak+o@9{k{|afd2s4{6C`SdaQht>#Iq}?2w+4+sqwNyGH2h zY|6lo5sEC{j;X0Bv~I4Z#YiK>Vuv}RtcgjLBa++5#@+t_E8{oL>8a`DiV0RpT}T@y z;cv{HdN#Joko9*uw5;DSh;)E|Gw}Y5y{jcpdMo)IT0QJpGJMDWpHp_Z ziTj`W;yDsjS4Ir1E(q0_W)NlY86-Dvz?|6PqIqhjd8KQ+8=tw!W9+iJMe3Vh?l;w> zl5OU)4{;HHh5jGOW$be*a(FQ+e|TF}WImBgJUrb99ymX7Nto7AAF!0D=DbId!)YdF z%V5#3%q}LLYKJmSBRd{s{mfCc(#I0xgqjZL%yh=vwBWv|N!&YTe z=#(`RUc@2U@jb)h9UQCeXag>W}evYv%S z{Y;jJ&}dOuT1Vf16U$bW)^|flr~L2q_=hg5G*yw!_-|+YrvQcll-v<;b!QoQp{XTD z**|)JO$(+U`yxOdCr$b3f>uMCMKZDvz&ahF%e zUrr=j2OZA64y?m2hcJg>`HcGeW_B#Zl1<4!5>eJoLpse7@f?WgaYtDNbRt-r z#B)>7>Z+uIvLh787=ID-7|b#%nmfE#mM)`+@gH}V=%Clxg_Rm}5D*Jk*djFqWsvWglT7~|T1eY?)Q(>kZil%m<@k=c7E*ZA5BE3q zz{s+t;YvSNWA(&&Z3JG$EI{?y1mFMw09b3Dq6%a(O(LT8CmD{C$tB~W1s9x$+CCU8 zSe9Z*Cgh)r>8k2!A8rX*5AZ%C6uM43mZ~4#Pv~(^nb1_*vZzb+CdbJ7#DFnTo8{Ek z+o)1~)!skD6?#(bb)<)9{O{g;u{}ifE@YZUcIr>Y5mnVe{uJ^j$T0&6p603l0B}!= ugMp-(nEX+n-NDjK(Ec Note: More information about automatic media selection is available from . @@ -60,7 +60,7 @@ Note that I-frame playlists are a [must-have](https://developer.apple.com/docume ### Inspecting and testing streams -Several tools are available for stream encoding and packaging teams to check that the streams they deliver work well with PillarboxPlayer. +Several tools are available for stream encoding and packaging teams to check that the streams they deliver work well with ``PillarboxPlayer``. #### HTTP Live Streaming Tools @@ -88,7 +88,7 @@ HLS streams can always be tested with a variety of native players, most notably: - Safari on iOS / iPadOS. - QuickTime Player on macOS. -> Important: Streams that cannot be correctly played with Apple official players will almost certainly fail to play correctly with PillarboxPlayer. +> Important: Streams that cannot be correctly played with Apple official players will almost certainly fail to play correctly with ``PillarboxPlayer``. #### 3rd party players diff --git a/Sources/Player/Player.docc/Articles/tracking/tracking-article.md b/Sources/Player/Player.docc/Articles/tracking/tracking-article.md index 8061eba8..6849c599 100644 --- a/Sources/Player/Player.docc/Articles/tracking/tracking-article.md +++ b/Sources/Player/Player.docc/Articles/tracking/tracking-article.md @@ -9,7 +9,7 @@ Track player items during playback. ## Overview -The PillarboxPlayer framework offers a way to track an item during playback. This mechanism is mostly useful to gather analytics, perform Quality of Experience (QoE) and Quality of Service (QoS) monitoring or save the current playback position into a local history, for example. +The ``PillarboxPlayer`` framework offers a way to track an item during playback. This mechanism is mostly useful to gather analytics, perform Quality of Experience (QoE) and Quality of Service (QoS) monitoring or save the current playback position into a local history, for example. You define which data is required by a tracker as well as its life cycle by creating a new class type and conforming it to the ``PlayerItemTracker`` protocol. This can be achieved in a few steps discussed below. diff --git a/Sources/Player/Player.docc/PillarboxPlayer.md b/Sources/Player/Player.docc/PillarboxPlayer.md index 6319d630..326b7052 100644 --- a/Sources/Player/Player.docc/PillarboxPlayer.md +++ b/Sources/Player/Player.docc/PillarboxPlayer.md @@ -26,6 +26,7 @@ The PillarboxPlayer framework fully integrates with SwiftUI, embracing its decla - - - + - } ### Asset Resource Loading From 70152310d8bd5156f1d76f9d94156ec1460ed035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20D=C3=A9fago?= Date: Mon, 25 Nov 2024 07:26:57 +0100 Subject: [PATCH 04/68] Add hover support (#1073) --- Demo/Sources/Players/PlaybackView.swift | 21 ++++++++++++++++--- .../Showcase/Playlist/PlaylistView.swift | 6 ++++++ Demo/Sources/Views/CloseButton.swift | 1 + Demo/Sources/Views/View.swift | 10 +++++++++ .../PictureInPictureButton.swift | 1 + 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Demo/Sources/Players/PlaybackView.swift b/Demo/Sources/Players/PlaybackView.swift index edb4d9e8..61040069 100644 --- a/Demo/Sources/Players/PlaybackView.swift +++ b/Demo/Sources/Players/PlaybackView.swift @@ -51,6 +51,14 @@ private struct MainView: View { MetricsView(metricsCollector: metricsCollector) } .statusBarHidden(isFullScreen ? isUserInterfaceHidden : false) + .onContinuousHover { phase in + switch phase { + case .active: + visibilityTracker.reset() + case .ended: + break + } + } .bind(visibilityTracker, to: player) .bind(metricsCollector, to: player) } @@ -352,6 +360,7 @@ private struct SkipBackwardButton: View { .opacity(player.canSkipBackward() ? 1 : 0) .animation(.defaultLinear, value: player.canSkipBackward()) .keyboardShortcut("s", modifiers: []) + .circularHoverEffect() } private func skipBackward() { @@ -375,6 +384,7 @@ private struct SkipForwardButton: View { .opacity(player.canSkipForward() ? 1 : 0) .animation(.defaultLinear, value: player.canSkipForward()) .keyboardShortcut("d", modifiers: []) + .circularHoverEffect() } private func skipForward() { @@ -394,6 +404,7 @@ private struct FullScreenButton: View { .font(.system(size: 20)) } .keyboardShortcut("f", modifiers: []) + .hoverEffect() } } @@ -431,6 +442,7 @@ private struct VolumeButton: View { .font(.system(size: 20)) } .keyboardShortcut("m", modifiers: []) + .hoverEffect() } private var imageName: String { @@ -457,6 +469,7 @@ private struct SettingsMenu: View { .tint(.white) } .menuOrder(.fixed) + .hoverEffect() } @ViewBuilder @@ -560,6 +573,7 @@ private struct LiveButton: View { .fontWeight(.ultraLight) .font(.system(size: 20)) } + .hoverEffect() .accessibilityLabel("Jump to live") } } @@ -829,12 +843,13 @@ private struct PlaybackButton: View { .resizable() .tint(.white) } -#if os(iOS) - .keyboardShortcut(.space, modifiers: []) -#endif .aspectRatio(contentMode: .fit) .frame(minWidth: 120, maxHeight: 90) .accessibilityLabel(accessibilityLabel) +#if os(iOS) + .keyboardShortcut(.space, modifiers: []) + .circularHoverEffect() +#endif } private func play() { diff --git a/Demo/Sources/Showcase/Playlist/PlaylistView.swift b/Demo/Sources/Showcase/Playlist/PlaylistView.swift index 43219c5f..211f72e0 100644 --- a/Demo/Sources/Showcase/Playlist/PlaylistView.swift +++ b/Demo/Sources/Showcase/Playlist/PlaylistView.swift @@ -76,6 +76,7 @@ private struct Toolbar: View { Button(action: player.returnToPrevious) { Image(systemName: "arrow.left") } + .hoverEffect() .accessibilityLabel("Previous") .disabled(!player.canReturnToPrevious()) } @@ -86,22 +87,26 @@ private struct Toolbar: View { Button(action: toggleRepeatMode) { Image(systemName: repeatModeImageName) } + .hoverEffect() .accessibilityLabel(repeatModeAccessibilityLabel) Button(action: model.shuffle) { Image(systemName: "shuffle") } + .hoverEffect() .accessibilityLabel("Shuffle") .disabled(model.isEmpty) Button(action: add) { Image(systemName: "plus") } + .hoverEffect() .accessibilityLabel("Add") Button(action: model.trash) { Image(systemName: "trash") } + .hoverEffect() .accessibilityLabel("Delete all") .disabled(model.isEmpty) } @@ -112,6 +117,7 @@ private struct Toolbar: View { Button(action: player.advanceToNext) { Image(systemName: "arrow.right") } + .hoverEffect() .accessibilityLabel("Next") .disabled(!player.canAdvanceToNext()) } diff --git a/Demo/Sources/Views/CloseButton.swift b/Demo/Sources/Views/CloseButton.swift index 5a40210c..65b01211 100644 --- a/Demo/Sources/Views/CloseButton.swift +++ b/Demo/Sources/Views/CloseButton.swift @@ -22,6 +22,7 @@ struct CloseButton: View { #if os(iOS) .keyboardShortcut(.escape, modifiers: []) #endif + .hoverEffect() } init(topBarStyle: Bool = false) { diff --git a/Demo/Sources/Views/View.swift b/Demo/Sources/Views/View.swift index cc727d22..3c039a19 100644 --- a/Demo/Sources/Views/View.swift +++ b/Demo/Sources/Views/View.swift @@ -116,3 +116,13 @@ extension View { } } } + +#if os(iOS) +extension View { + func circularHoverEffect() -> some View { + padding() + .hoverEffect(.highlight) + .clipShape(Circle()) + } +} +#endif diff --git a/Sources/Player/UserInterface/PictureInPictureButton.swift b/Sources/Player/UserInterface/PictureInPictureButton.swift index 3b3dd5b3..c688e991 100644 --- a/Sources/Player/UserInterface/PictureInPictureButton.swift +++ b/Sources/Player/UserInterface/PictureInPictureButton.swift @@ -25,6 +25,7 @@ public struct PictureInPictureButton: View where Content: View { Button(action: PictureInPicture.shared.custom.toggle) { content(isActive) } + .hoverEffect() .onReceive(PictureInPicture.shared.custom.$isActive) { isActive = $0 } } } From d4c17017c781bb5c18b969d3d3f80771a231a398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20D=C3=A9fago?= Date: Thu, 28 Nov 2024 07:44:47 +0100 Subject: [PATCH 05/68] Fix layout glitches during rotation (#1074) --- Demo/Sources/Players/PlaybackView.swift | 6 +++--- Demo/Sources/Views/View.swift | 10 ---------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/Demo/Sources/Players/PlaybackView.swift b/Demo/Sources/Players/PlaybackView.swift index 61040069..a2e02a3b 100644 --- a/Demo/Sources/Players/PlaybackView.swift +++ b/Demo/Sources/Players/PlaybackView.swift @@ -360,7 +360,7 @@ private struct SkipBackwardButton: View { .opacity(player.canSkipBackward() ? 1 : 0) .animation(.defaultLinear, value: player.canSkipBackward()) .keyboardShortcut("s", modifiers: []) - .circularHoverEffect() + .hoverEffect() } private func skipBackward() { @@ -384,7 +384,7 @@ private struct SkipForwardButton: View { .opacity(player.canSkipForward() ? 1 : 0) .animation(.defaultLinear, value: player.canSkipForward()) .keyboardShortcut("d", modifiers: []) - .circularHoverEffect() + .hoverEffect() } private func skipForward() { @@ -848,7 +848,7 @@ private struct PlaybackButton: View { .accessibilityLabel(accessibilityLabel) #if os(iOS) .keyboardShortcut(.space, modifiers: []) - .circularHoverEffect() + .hoverEffect() #endif } diff --git a/Demo/Sources/Views/View.swift b/Demo/Sources/Views/View.swift index 3c039a19..cc727d22 100644 --- a/Demo/Sources/Views/View.swift +++ b/Demo/Sources/Views/View.swift @@ -116,13 +116,3 @@ extension View { } } } - -#if os(iOS) -extension View { - func circularHoverEffect() -> some View { - padding() - .hoverEffect(.highlight) - .clipShape(Circle()) - } -} -#endif From db443a82978fab547b3aef88ae589c9dc566171a Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:37:07 +0100 Subject: [PATCH 06/68] Use pkgx for check-quality target --- Makefile | 38 +++++++++++++++++--------------------- Scripts/check-quality.sh | 6 +++++- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Makefile b/Makefile index 46424af1..d63de7f9 100644 --- a/Makefile +++ b/Makefile @@ -1,50 +1,46 @@ #!/usr/bin/xcrun make -f -CONFIGURATION_REPOSITORY_URL=https://github.com/SRGSSR/pillarbox-apple-configuration.git -CONFIGURATION_COMMIT_SHA1=dad52a4242c7997c179073caec03b8d6e718fc03 - .PHONY: all all: help -.PHONY: setup -setup: - @echo "Setting up the project..." - @bundle install > /dev/null - @Scripts/checkout-configuration.sh "${CONFIGURATION_REPOSITORY_URL}" "${CONFIGURATION_COMMIT_SHA1}" Configuration +.PHONY: install-tools +install-tools: + @echo "Installing tools..." + @curl -Ssf https://pkgx.sh | sh &> /dev/null @echo "... done.\n" .PHONY: fastlane -fastlane: setup +fastlane: install-tools @bundle exec fastlane .PHONY: archive-demo-ios -archive-demo-ios: setup +archive-demo-ios: install-tools @bundle exec fastlane archive_demo_ios .PHONY: archive-demo-tvos -archive-demo-tvos: setup +archive-demo-tvos: install-tools @bundle exec fastlane archive_demo_tvos .PHONY: deliver-demo-nightly-ios -deliver-demo-nightly-ios: setup +deliver-demo-nightly-ios: install-tools @echo "Delivering demo nightly build for iOS..." @bundle exec fastlane deliver_demo_nightly_ios @echo "... done.\n" .PHONY: deliver-demo-nightly-tvos -deliver-demo-nightly-tvos: setup +deliver-demo-nightly-tvos: install-tools @echo "Delivering demo nightly build for tvOS..." @bundle exec fastlane deliver_demo_nightly_tvos @echo "... done.\n" .PHONY: deliver-demo-release-ios -deliver-demo-release-ios: setup +deliver-demo-release-ios: install-tools @echo "Delivering demo release build for iOS..." @bundle exec fastlane deliver_demo_release_ios @echo "... done.\n" .PHONY: deliver-demo-release-tvos -deliver-demo-release-tvos: setup +deliver-demo-release-tvos: install-tools @echo "Delivering demo release build for tvOS..." @bundle exec fastlane deliver_demo_release_tvos @echo "... done.\n" @@ -62,7 +58,7 @@ test-streams-stop: @echo "... done.\n" .PHONY: test-ios -test-ios: setup +test-ios: install-tools @echo "Running unit tests..." @Scripts/test-streams.sh -s @bundle exec fastlane test_ios @@ -70,7 +66,7 @@ test-ios: setup @echo "... done.\n" .PHONY: test-tvos -test-tvos: setup +test-tvos: install-tools @echo "Running unit tests..." @Scripts/test-streams.sh -s @bundle exec fastlane test_tvos @@ -78,13 +74,13 @@ test-tvos: setup @echo "... done.\n" .PHONY: check-quality -check-quality: setup +check-quality: install-tools @echo "Checking quality..." @Scripts/check-quality.sh @echo "... done.\n" .PHONY: fix-quality -fix-quality: setup +fix-quality: install-tools @echo "Fixing quality..." @Scripts/fix-quality.sh @echo "... done.\n" @@ -131,7 +127,7 @@ find-dead-code: @echo "... done.\n" .PHONY: doc -doc: setup +doc: install-tools @echo "Generating documentation sets..." @bundle exec fastlane doc @echo "... done.\n" @@ -141,7 +137,7 @@ help: @echo "The following targets are available:" @echo "" @echo " all Default target" - @echo " setup Setup project" + @echo " install-tools Install required tools" @echo "" @echo " fastlane Run fastlane" @echo "" diff --git a/Scripts/check-quality.sh b/Scripts/check-quality.sh index 92257992..9441f11e 100755 --- a/Scripts/check-quality.sh +++ b/Scripts/check-quality.sh @@ -2,6 +2,9 @@ set -e +eval "$(pkgx --shellcode)" +env +swiftlint +shellcheck +markdownlint +yamllint + echo "... checking Swift code..." if [ $# -eq 0 ]; then swiftlint --quiet --strict @@ -9,7 +12,8 @@ elif [[ "$1" == "only-changes" ]]; then git diff --staged --name-only | grep ".swift$" | xargs swiftlint lint --quiet --strict fi echo "... checking Ruby scripts..." -bundle exec rubocop --format quiet +echo "... UNCOMMENT ..." +#rubocop --format quiet echo "... checking Shell scripts..." shellcheck Scripts/*.sh hooks/* Artifacts/**/*.sh echo "... checking Markdown documentation..." From a9e448814ac491eb9997a54890796397c4dd61ee Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:40:30 +0100 Subject: [PATCH 07/68] Add check-quality job --- .github/workflows/pull-request.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/pull-request.yml diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 00000000..b1331fbf --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,15 @@ +--- +name: Pull Request + +on: [push] + +jobs: + check-quality: + name: "🔎 Check quality" + runs-on: tart + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run the quality check + run: make check-quality From c493d1ea9c14b473983a69c1dc88162f2d7b2f47 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:03:40 +0100 Subject: [PATCH 08/68] Use pkgx for fastlane target --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d63de7f9..5c35acdc 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ install-tools: .PHONY: fastlane fastlane: install-tools - @bundle exec fastlane + @pkgx fastlane .PHONY: archive-demo-ios archive-demo-ios: install-tools From 18bade0dd1c1c7e0a743b44884b08a643dba0063 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:04:54 +0100 Subject: [PATCH 09/68] Remove Gemfiles --- Gemfile | 10 -- Gemfile.lock | 262 --------------------------------------------------- 2 files changed, 272 deletions(-) delete mode 100644 Gemfile delete mode 100644 Gemfile.lock diff --git a/Gemfile b/Gemfile deleted file mode 100644 index d002b4c3..00000000 --- a/Gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -source 'https://rubygems.org' - -gem 'fastlane' -gem 'rubocop' -gem 'xcode-install' - -plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') -eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index e61f2de9..00000000 --- a/Gemfile.lock +++ /dev/null @@ -1,262 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.7) - base64 - nkf - rexml - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) - artifactory (3.0.17) - ast (2.4.2) - atomos (0.1.3) - aws-eventstream (1.3.0) - aws-partitions (1.998.0) - aws-sdk-core (3.211.0) - aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.992.0) - aws-sigv4 (~> 1.9) - jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.95.0) - aws-sdk-core (~> 3, >= 3.210.0) - aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.169.0) - aws-sdk-core (~> 3, >= 3.210.0) - aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.5) - aws-sigv4 (1.10.1) - aws-eventstream (~> 1, >= 1.0.2) - babosa (1.0.4) - badge (0.13.0) - fastimage (>= 1.6) - fastlane (>= 2.0) - mini_magick (>= 4.9.4, < 5.0.0) - base64 (0.2.0) - claide (1.1.0) - colored (1.2) - colored2 (3.1.2) - commander (4.6.0) - highline (~> 2.0.0) - declarative (0.0.20) - digest-crc (0.6.5) - rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20240107) - dotenv (2.8.1) - emoji_regex (3.2.3) - excon (0.112.0) - faraday (1.10.4) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-cookie_jar (0.0.7) - faraday (>= 0.8.0) - http-cookie (~> 1.0.0) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.2) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) - faraday_middleware (1.2.1) - faraday (~> 1.0) - fastimage (2.3.1) - fastlane (2.225.0) - CFPropertyList (>= 2.3, < 4.0.0) - addressable (>= 2.8, < 3.0.0) - artifactory (~> 3.0) - aws-sdk-s3 (~> 1.0) - babosa (>= 1.0.3, < 2.0.0) - bundler (>= 1.12.0, < 3.0.0) - colored (~> 1.2) - commander (~> 4.6) - dotenv (>= 2.1.1, < 3.0.0) - emoji_regex (>= 0.1, < 4.0) - excon (>= 0.71.0, < 1.0.0) - faraday (~> 1.0) - faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 1.0) - fastimage (>= 2.1.0, < 3.0.0) - fastlane-sirp (>= 1.0.0) - gh_inspector (>= 1.1.2, < 2.0.0) - google-apis-androidpublisher_v3 (~> 0.3) - google-apis-playcustomapp_v1 (~> 0.1) - google-cloud-env (>= 1.6.0, < 2.0.0) - google-cloud-storage (~> 1.31) - highline (~> 2.0) - http-cookie (~> 1.0.5) - json (< 3.0.0) - jwt (>= 2.1.0, < 3) - mini_magick (>= 4.9.4, < 5.0.0) - multipart-post (>= 2.0.0, < 3.0.0) - naturally (~> 2.2) - optparse (>= 0.1.1, < 1.0.0) - plist (>= 3.1.0, < 4.0.0) - rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.5) - simctl (~> 1.6.3) - terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (~> 3) - tty-screen (>= 0.6.3, < 1.0.0) - tty-spinner (>= 0.8.0, < 1.0.0) - word_wrap (~> 1.0.0) - xcodeproj (>= 1.13.0, < 2.0.0) - xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) - fastlane-plugin-badge (1.5.0) - badge (~> 0.13.0) - fastlane-plugin-xcconfig (2.1.0) - fastlane-sirp (1.0.0) - sysrandom (~> 1.0) - gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.54.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.3) - addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) - mini_mime (~> 1.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.a) - rexml - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-playcustomapp_v1 (0.13.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.31.0) - google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.1) - google-cloud-env (>= 1.0, < 3.a) - google-cloud-errors (~> 1.0) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.4.0) - google-cloud-storage (1.47.0) - addressable (~> 2.8) - digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.31.0) - google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) - mini_mime (~> 1.0) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) - jwt (>= 1.4, < 3.0) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - signet (>= 0.16, < 2.a) - highline (2.0.3) - http-cookie (1.0.7) - domain_name (~> 0.5) - httpclient (2.8.3) - jmespath (1.6.2) - json (2.7.5) - jwt (2.9.3) - base64 - language_server-protocol (3.17.0.3) - mini_magick (4.13.2) - mini_mime (1.1.5) - multi_json (1.15.0) - multipart-post (2.4.1) - nanaimo (0.4.0) - naturally (2.2.1) - nkf (0.2.0) - optparse (0.5.0) - os (1.1.4) - parallel (1.26.3) - parser (3.3.5.0) - ast (~> 2.4.1) - racc - plist (3.7.1) - public_suffix (6.0.1) - racc (1.8.1) - rainbow (3.1.1) - rake (13.2.1) - regexp_parser (2.9.2) - representable (3.2.0) - declarative (< 0.1.0) - trailblazer-option (>= 0.1.1, < 0.2.0) - uber (< 0.2.0) - retriable (3.1.2) - rexml (3.3.9) - rouge (2.0.7) - rubocop (1.67.0) - json (~> 2.3) - language_server-protocol (>= 3.17.0) - parallel (~> 1.10) - parser (>= 3.3.0.2) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.4, < 3.0) - rubocop-ast (>= 1.32.2, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.33.0) - parser (>= 3.3.1.0) - ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) - rubyzip (2.3.2) - security (0.1.5) - signet (0.19.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) - multi_json (~> 1.10) - simctl (1.6.10) - CFPropertyList - naturally - sysrandom (1.0.5) - terminal-notifier (2.0.0) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - trailblazer-option (0.1.2) - tty-cursor (0.7.1) - tty-screen (0.8.2) - tty-spinner (0.9.3) - tty-cursor (~> 0.7) - uber (0.1.0) - unicode-display_width (2.6.0) - word_wrap (1.0.0) - xcode-install (2.8.1) - claide (>= 0.9.1) - fastlane (>= 2.1.0, < 3.0.0) - xcodeproj (1.26.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.4.0) - rexml (>= 3.3.6, < 4.0) - xcpretty (0.3.0) - rouge (~> 2.0.7) - xcpretty-travis-formatter (1.0.1) - xcpretty (~> 0.2, >= 0.0.7) - -PLATFORMS - arm64-darwin-21 - arm64-darwin-22 - arm64-darwin-23 - arm64-darwin-24 - x86_64-darwin-21 - x86_64-darwin-22 - -DEPENDENCIES - fastlane - fastlane-plugin-badge - fastlane-plugin-xcconfig - rubocop - xcode-install - -BUNDLED WITH - 2.3.7 From 048fd5fbe447f7ebf8a689c612d59ce0b6ffabd7 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:19:35 +0100 Subject: [PATCH 10/68] Use pkgx for server streams --- Makefile | 4 ++-- Scripts/test-streams.sh | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 5c35acdc..69f683d4 100644 --- a/Makefile +++ b/Makefile @@ -46,13 +46,13 @@ deliver-demo-release-tvos: install-tools @echo "... done.\n" .PHONY: test-streams-start -test-streams-start: +test-streams-start: install-tools @echo "Starting test streams" @Scripts/test-streams.sh -s @echo "... done.\n" .PHONY: test-streams-stop -test-streams-stop: +test-streams-stop: install-tools @echo "Stopping test streams" @Scripts/test-streams.sh -k @echo "... done.\n" diff --git a/Scripts/test-streams.sh b/Scripts/test-streams.sh index a1d13df7..d3c29283 100755 --- a/Scripts/test-streams.sh +++ b/Scripts/test-streams.sh @@ -3,6 +3,9 @@ SCRIPT_NAME=$(basename "$0") SCRIPT_DIR=$(dirname "$0") +eval "$(pkgx --shellcode)" +env +python +ffmpeg +packager + GENERATED_DIR="/tmp/pillarbox" METADATA_DIR="$SCRIPT_DIR/../metadata" @@ -14,8 +17,8 @@ function serve_test_streams { kill_test_streams "$dest_dir" - if ! command -v python3 &> /dev/null; then - echo "python3 could not be found" + if ! command -v python &> /dev/null; then + echo "python could not be found" exit 1 fi @@ -89,7 +92,7 @@ function generate_packaged_streams { mkdir -p "$dest_dir" local on_demand_with_options_dir="$dest_dir/on_demand_with_options" - shaka-packager \ + packager \ "in=$src_dir/source_640x360.mp4,stream=video,segment_template=$on_demand_with_options_dir/640x360/\$Number\$.ts" \ "in=$src_dir/source_audio_eng.mp4,stream=audio,segment_template=$on_demand_with_options_dir/audio_eng/\$Number\$.ts,lang=en,hls_name=English" \ "in=$src_dir/source_audio_fre.mp4,stream=audio,segment_template=$on_demand_with_options_dir/audio_fre/\$Number\$.ts,lang=fr,hls_name=Français" \ @@ -100,19 +103,19 @@ function generate_packaged_streams { --hls_master_playlist_output "$on_demand_with_options_dir/master.m3u8" > /dev/null 2>&1 local on_demand_without_options_dir="$dest_dir/on_demand_without_options" - shaka-packager \ + packager \ "in=$src_dir/source_640x360.mp4,stream=video,segment_template=$on_demand_without_options_dir/640x360/\$Number\$.ts" \ --hls_master_playlist_output "$on_demand_without_options_dir/master.m3u8" > /dev/null 2>&1 local on_demand_with_single_audible_option_dir="$dest_dir/on_demand_with_single_audible_option" - shaka-packager \ + packager \ "in=$src_dir/source_640x360.mp4,stream=video,segment_template=$on_demand_with_single_audible_option_dir/640x360/\$Number\$.ts" \ "in=$src_dir/source_audio_eng.mp4,stream=audio,segment_template=$on_demand_with_single_audible_option_dir/audio_eng/\$Number\$.ts,lang=en,hls_name=English" \ --hls_master_playlist_output "$on_demand_with_single_audible_option_dir/master.m3u8" > /dev/null 2>&1 } function serve_directory { - python3 -m http.server 8123 --directory "$1" > /dev/null 2>&1 & + python -m http.server 8123 --directory "$1" > /dev/null 2>&1 & } function kill_test_streams { From a6aaf1270aef4ed9a0095dbf4d0ba87e8b69aa07 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:20:40 +0100 Subject: [PATCH 11/68] Use pkgx for tests --- .xcode-version | 2 +- Makefile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.xcode-version b/.xcode-version index c32b0ec5..0d68f8a0 100644 --- a/.xcode-version +++ b/.xcode-version @@ -1 +1 @@ -16.1 +16.0 diff --git a/Makefile b/Makefile index 69f683d4..0647b90d 100644 --- a/Makefile +++ b/Makefile @@ -61,7 +61,7 @@ test-streams-stop: install-tools test-ios: install-tools @echo "Running unit tests..." @Scripts/test-streams.sh -s - @bundle exec fastlane test_ios + @pkgx +xcodes fastlane test_ios @Scripts/test-streams.sh -k @echo "... done.\n" @@ -69,7 +69,7 @@ test-ios: install-tools test-tvos: install-tools @echo "Running unit tests..." @Scripts/test-streams.sh -s - @bundle exec fastlane test_tvos + @pkgx fastlane test_tvos @Scripts/test-streams.sh -k @echo "... done.\n" From a5420c3881d4c0b816d966ef99bd0a6006312f86 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:39:57 +0100 Subject: [PATCH 12/68] Add job for tests --- .github/workflows/pull-request.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index b1331fbf..ab5ff02f 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -13,3 +13,16 @@ jobs: - name: Run the quality check run: make check-quality + + tests: + name: "🧪 Tests" + runs-on: tart + strategy: + matrix: + platform: [ios, tvos] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run tests + run: make test-${{ matrix.platform }} From 554b8b6a50b219d769095c6e778fd7da7415cb85 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:40:49 +0100 Subject: [PATCH 13/68] Update xcode version --- .xcode-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.xcode-version b/.xcode-version index 0d68f8a0..c32b0ec5 100644 --- a/.xcode-version +++ b/.xcode-version @@ -1 +1 @@ -16.0 +16.1 From 1cfa6084cfbb6ee533b4d86127d12432a43ec3df Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 19 Nov 2024 21:01:20 +0100 Subject: [PATCH 14/68] Uncomment rubocop quality check --- Scripts/check-quality.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Scripts/check-quality.sh b/Scripts/check-quality.sh index 9441f11e..e31a467b 100755 --- a/Scripts/check-quality.sh +++ b/Scripts/check-quality.sh @@ -3,7 +3,7 @@ set -e eval "$(pkgx --shellcode)" -env +swiftlint +shellcheck +markdownlint +yamllint +env +swiftlint +rubocop +shellcheck +markdownlint +yamllint echo "... checking Swift code..." if [ $# -eq 0 ]; then @@ -12,8 +12,7 @@ elif [[ "$1" == "only-changes" ]]; then git diff --staged --name-only | grep ".swift$" | xargs swiftlint lint --quiet --strict fi echo "... checking Ruby scripts..." -echo "... UNCOMMENT ..." -#rubocop --format quiet +rubocop --format quiet echo "... checking Shell scripts..." shellcheck Scripts/*.sh hooks/* Artifacts/**/*.sh echo "... checking Markdown documentation..." From 2c7ee1460788faa5df397d8707e4ab6d15324818 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 20 Nov 2024 11:21:26 +0100 Subject: [PATCH 15/68] Update documentation building --- .github/workflows/pull-request.yml | 10 ++++++++++ Makefile | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index ab5ff02f..bf915082 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -14,6 +14,16 @@ jobs: - name: Run the quality check run: make check-quality + build-documentation: + name: "📚 Build documentation" + runs-on: tart + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build the documentation + run: make doc + tests: name: "🧪 Tests" runs-on: tart diff --git a/Makefile b/Makefile index 0647b90d..7c5ff37e 100644 --- a/Makefile +++ b/Makefile @@ -129,7 +129,7 @@ find-dead-code: .PHONY: doc doc: install-tools @echo "Generating documentation sets..." - @bundle exec fastlane doc + @pkgx fastlane doc @echo "... done.\n" .PHONY: help From f37085c95466273a0039f2e24f78a4685f7489c5 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 20 Nov 2024 11:22:31 +0100 Subject: [PATCH 16/68] Comment tests --- .github/workflows/pull-request.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index bf915082..34455932 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -24,15 +24,15 @@ jobs: - name: Build the documentation run: make doc - tests: - name: "🧪 Tests" - runs-on: tart - strategy: - matrix: - platform: [ios, tvos] - steps: - - name: Checkout code - uses: actions/checkout@v4 + # tests: + # name: "🧪 Tests" + # runs-on: tart + # strategy: + # matrix: + # platform: [ios, tvos] + # steps: + # - name: Checkout code + # uses: actions/checkout@v4 - - name: Run tests - run: make test-${{ matrix.platform }} + # - name: Run tests + # run: make test-${{ matrix.platform }} From b84ddec1261895d0c55a7cf355866c9e6bb10f42 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:59:44 +0100 Subject: [PATCH 17/68] Use pkgx for clean-imports target --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 7c5ff37e..71b5448e 100644 --- a/Makefile +++ b/Makefile @@ -111,9 +111,9 @@ clean-imports: @echo "Cleaning imports..." @mkdir -p .build @xcodebuild -scheme Pillarbox -destination generic/platform=ios > ./.build/xcodebuild.log - @swiftlint analyze --fix --compiler-log-path ./.build/xcodebuild.log + @pkgx swiftlint analyze --fix --compiler-log-path ./.build/xcodebuild.log @xcodebuild -scheme Pillarbox-demo -project ./Demo/Pillarbox-demo.xcodeproj -destination generic/platform=iOS > ./.build/xcodebuild.log - @swiftlint analyze --fix --compiler-log-path ./.build/xcodebuild.log + @pkgx swiftlint analyze --fix --compiler-log-path ./.build/xcodebuild.log @echo "... done.\n" .PHONY: find-dead-code From 6f154455a90b07e381afd3b851dfeabbb65638d5 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:02:05 +0100 Subject: [PATCH 18/68] Update pre-commit hook --- hooks/pre-commit | 5 ----- 1 file changed, 5 deletions(-) diff --git a/hooks/pre-commit b/hooks/pre-commit index 0c60fb90..b9a31b50 100755 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -1,11 +1,6 @@ #!/bin/sh -#================================================================ # Quality check -#================================================================ - -PATH="$(which swiftlint):$(which ruby):$PATH" - if Scripts/check-quality.sh only-changes; then echo "✅ Quality checked" else From 19a5de8d223b2732bb034f79bbc6c972e8e6bb16 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:30:43 +0100 Subject: [PATCH 19/68] Add script to bypass microphone simulator popup --- Scripts/bypass-simulator-trampoline.sh | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100755 Scripts/bypass-simulator-trampoline.sh diff --git a/Scripts/bypass-simulator-trampoline.sh b/Scripts/bypass-simulator-trampoline.sh new file mode 100755 index 00000000..3fe26acc --- /dev/null +++ b/Scripts/bypass-simulator-trampoline.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# The manipulation of the following database is only possible if the System Integraty Protection (SIP) is disabled. +# SIP can only be updated (enabled/disabled) in recovery mode. +# Check the status of SIP: csrutil status + +## Simulator Trampoline (access to microphone) +## When using freshly cloned virtual machines, running our tests leads to a popup asking for access to the microphone. To avoid that popup, we can preventively write to the database. +sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db "INSERT INTO 'main'.'access' ('service', 'client', 'client_type', 'auth_value', 'auth_reason', 'auth_version', 'csreq', 'policy_id', 'indirect_object_identifier_type', 'indirect_object_identifier', 'indirect_object_code_identity', 'flags', 'last_modified', 'pid', 'pid_version', 'boot_uuid', 'last_reminded') VALUES ('kTCCServiceMicrophone', 'com.apple.CoreSimulator.SimulatorTrampoline', '0', '2', '2', '1', X'fade0c00000000480000000100000006000000020000002b636f6d2e6170706c652e436f726553696d756c61746f722e53696d756c61746f725472616d706f6c696e650000000003', NULL, NULL, 'UNUSED', NULL, '0', strftime('%s','now'), NULL, NULL, 'UNUSED', '0');" From b4a5299ebcbffc050a9d24b2dff0b1c9b032dafa Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:35:53 +0100 Subject: [PATCH 20/68] Update workflow to bypass microphone access popup --- .github/workflows/pull-request.yml | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 34455932..16a86720 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -24,15 +24,18 @@ jobs: - name: Build the documentation run: make doc - # tests: - # name: "🧪 Tests" - # runs-on: tart - # strategy: - # matrix: - # platform: [ios, tvos] - # steps: - # - name: Checkout code - # uses: actions/checkout@v4 - - # - name: Run tests - # run: make test-${{ matrix.platform }} + tests: + name: "🧪 Tests" + runs-on: tart + strategy: + matrix: + platform: [ios, tvos] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Authorize microphone access for simulator + run: Scripts/bypass-simulator-trampoline.sh + + - name: Run tests + run: make test-${{ matrix.platform }} From 5c196cb61f1b04107e13614733a739a8a44e1052 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:47:30 +0100 Subject: [PATCH 21/68] Fix bypass-simulator-trampoline.sh --- Scripts/bypass-simulator-trampoline.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/bypass-simulator-trampoline.sh b/Scripts/bypass-simulator-trampoline.sh index 3fe26acc..b1680d78 100755 --- a/Scripts/bypass-simulator-trampoline.sh +++ b/Scripts/bypass-simulator-trampoline.sh @@ -6,4 +6,4 @@ ## Simulator Trampoline (access to microphone) ## When using freshly cloned virtual machines, running our tests leads to a popup asking for access to the microphone. To avoid that popup, we can preventively write to the database. -sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db "INSERT INTO 'main'.'access' ('service', 'client', 'client_type', 'auth_value', 'auth_reason', 'auth_version', 'csreq', 'policy_id', 'indirect_object_identifier_type', 'indirect_object_identifier', 'indirect_object_code_identity', 'flags', 'last_modified', 'pid', 'pid_version', 'boot_uuid', 'last_reminded') VALUES ('kTCCServiceMicrophone', 'com.apple.CoreSimulator.SimulatorTrampoline', '0', '2', '2', '1', X'fade0c00000000480000000100000006000000020000002b636f6d2e6170706c652e436f726553696d756c61746f722e53696d756c61746f725472616d706f6c696e650000000003', NULL, NULL, 'UNUSED', NULL, '0', strftime('%s','now'), NULL, NULL, 'UNUSED', '0');" +sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db "INSERT OR REPLACE INTO 'main'.'access' ('service', 'client', 'client_type', 'auth_value', 'auth_reason', 'auth_version', 'csreq', 'policy_id', 'indirect_object_identifier_type', 'indirect_object_identifier', 'indirect_object_code_identity', 'flags', 'last_modified', 'pid', 'pid_version', 'boot_uuid', 'last_reminded') VALUES ('kTCCServiceMicrophone', 'com.apple.CoreSimulator.SimulatorTrampoline', '0', '2', '2', '1', X'fade0c00000000480000000100000006000000020000002b636f6d2e6170706c652e436f726553696d756c61746f722e53696d756c61746f725472616d706f6c696e650000000003', NULL, NULL, 'UNUSED', NULL, '0', strftime('%s','now'), NULL, NULL, 'UNUSED', '0');" \ No newline at end of file From c46180d11bc2a4adf0ad52d037c2b0a76fc48a4f Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Thu, 21 Nov 2024 07:07:15 +0100 Subject: [PATCH 22/68] Use pkgx for periphery --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 71b5448e..7aae4ae4 100644 --- a/Makefile +++ b/Makefile @@ -121,9 +121,9 @@ find-dead-code: @echo "Start checking dead code..." @mkdir -p .build @xcodebuild -scheme Pillarbox -destination generic/platform=iOS -derivedDataPath ./.build/derived-data clean build &> /dev/null - @periphery scan --retain-public --skip-build --index-store-path ./.build/derived-data/Index.noindex/DataStore/ + @pkgx periphery scan --retain-public --skip-build --index-store-path ./.build/derived-data/Index.noindex/DataStore/ @xcodebuild -scheme Pillarbox-demo -project ./Demo/Pillarbox-demo.xcodeproj -destination generic/platform=iOS -derivedDataPath ./.build/derived-data clean build &> /dev/null - @periphery scan --project ./Demo/Pillarbox-demo.xcodeproj --schemes Pillarbox-demo --targets Pillarbox-demo --skip-build --index-store-path ./.build/derived-data/Index.noindex/DataStore/ + @pkgx periphery scan --project ./Demo/Pillarbox-demo.xcodeproj --schemes Pillarbox-demo --targets Pillarbox-demo --skip-build --index-store-path ./.build/derived-data/Index.noindex/DataStore/ @echo "... done.\n" .PHONY: doc From 16968ed7f36d05014128e7ea6c1af8d27916e9b5 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Thu, 21 Nov 2024 08:18:00 +0100 Subject: [PATCH 23/68] Add archive-demos job to the workflow --- .github/workflows/pull-request.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 16a86720..78b5d716 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -39,3 +39,16 @@ jobs: - name: Run tests run: make test-${{ matrix.platform }} + + archive-demos: + name: "📦 Archives" + runs-on: tart + strategy: + matrix: + platform: [ios, tvos] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Archive the demo + run: make archive-demo-${{ matrix.platform }} From 2c77232616af29b8f1237cd5b7fada39a45fa784 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Thu, 21 Nov 2024 08:18:28 +0100 Subject: [PATCH 24/68] Use pkgx for demos archiving --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 7aae4ae4..888dabb0 100644 --- a/Makefile +++ b/Makefile @@ -15,11 +15,11 @@ fastlane: install-tools .PHONY: archive-demo-ios archive-demo-ios: install-tools - @bundle exec fastlane archive_demo_ios + @pkgx fastlane archive_demo_ios .PHONY: archive-demo-tvos archive-demo-tvos: install-tools - @bundle exec fastlane archive_demo_tvos + @pkgx fastlane archive_demo_tvos .PHONY: deliver-demo-nightly-ios deliver-demo-nightly-ios: install-tools From ac64ae6e3858e794ceac73f0e2c9fca0cce7b2f5 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:13:28 +0100 Subject: [PATCH 25/68] Provide a way to decode an encoded Apple certificate --- .github/workflows/pull-request.yml | 79 ++++++++++++++++-------------- Scripts/add-apple-certificate.sh | 22 +++++++++ 2 files changed, 64 insertions(+), 37 deletions(-) create mode 100755 Scripts/add-apple-certificate.sh diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 78b5d716..5359a76e 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -4,42 +4,42 @@ name: Pull Request on: [push] jobs: - check-quality: - name: "🔎 Check quality" - runs-on: tart - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Run the quality check - run: make check-quality - - build-documentation: - name: "📚 Build documentation" - runs-on: tart - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Build the documentation - run: make doc - - tests: - name: "🧪 Tests" - runs-on: tart - strategy: - matrix: - platform: [ios, tvos] - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Authorize microphone access for simulator - run: Scripts/bypass-simulator-trampoline.sh - - - name: Run tests - run: make test-${{ matrix.platform }} - + # check-quality: + # name: "🔎 Check quality" + # runs-on: tart + # steps: + # - name: Checkout code + # uses: actions/checkout@v4 + + # - name: Run the quality check + # run: make check-quality + + # build-documentation: + # name: "📚 Build documentation" + # runs-on: tart + # steps: + # - name: Checkout code + # uses: actions/checkout@v4 + + # - name: Build the documentation + # run: make doc + + # tests: + # name: "🧪 Tests" + # runs-on: tart + # strategy: + # matrix: + # platform: [ios, tvos] + # steps: + # - name: Checkout code + # uses: actions/checkout@v4 + + # - name: Authorize microphone access for simulator + # run: Scripts/bypass-simulator-trampoline.sh + + # - name: Run tests + # run: make test-${{ matrix.platform }} + # archive-demos: name: "📦 Archives" runs-on: tart @@ -50,5 +50,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Add Apple certificate + run: | + Scripts/add-apple-certificate.sh \ + ${{ secrets.SRGSSR_APPLE_DEV_CERTIFICATE_B64 }} + - name: Archive the demo - run: make archive-demo-${{ matrix.platform }} + run: sleep 3600 #make archive-demo-${{ matrix.platform }} diff --git a/Scripts/add-apple-certificate.sh b/Scripts/add-apple-certificate.sh new file mode 100755 index 00000000..d647ea0c --- /dev/null +++ b/Scripts/add-apple-certificate.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +apple_certificate_b64="$1" + +if [[ -z $apple_certificate_b64 ]] +then + echo "[!] Usage: $0 " + exit 1 +fi + +apple_certificate_password="" +apple_certificate_decoded_path="/tmp/certificate.p12" + +keychain_password="admin" +keychain_path="$HOME/Library/Keychains/login.keychain-db" + +echo "$apple_certificate_b64" | base64 --decode > "$apple_certificate_decoded_path" + +# Import certificate +security import "$apple_certificate_decoded_path" -k "$keychain_path" -P "$apple_certificate_password" -T /usr/bin/security -T /usr/bin/codesign +# Authorize access to certificate private key +security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain_path" \ No newline at end of file From 01b97259ea4b5a5e701d8bcd3b47c22aeaa44a8a Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:09:01 +0100 Subject: [PATCH 26/68] Add a script to configure private stuff --- Scripts/configure-environment.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100755 Scripts/configure-environment.sh diff --git a/Scripts/configure-environment.sh b/Scripts/configure-environment.sh new file mode 100755 index 00000000..9b2f06a5 --- /dev/null +++ b/Scripts/configure-environment.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +apple_api_key_p8="$1" +apple_account_info_b64="$2" + +if [[ -z $apple_api_key_p8 || -z $apple_account_info_b64 ]] +then + echo "[!] Usage: $0 " + exit 1 +fi + +mkdir -p ../Configuration +echo "$apple_account_info_b64" | base64 --decode > ../Configuration/.env +echo "$apple_api_key_p8" > ../Configuration/AppStoreConnect_API_Key.p8 From 4d5fb52e86abdb7e696e46a94e640d3cb083d51a Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:14:02 +0100 Subject: [PATCH 27/68] Add a step for the project configuration --- .github/workflows/pull-request.yml | 8 +++++++- Scripts/configure-environment.sh | 14 ++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 5359a76e..a902da5d 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -55,5 +55,11 @@ jobs: Scripts/add-apple-certificate.sh \ ${{ secrets.SRGSSR_APPLE_DEV_CERTIFICATE_B64 }} + - name: Configure environment + run: | + Scripts/configure-environment.sh \ + ${{ secrets.APP_STORE_CONNECT_API_KEY_B64 }} \ + ${{ secrets.APPLE_ACCOUNT_INFO_B64 }} + - name: Archive the demo - run: sleep 3600 #make archive-demo-${{ matrix.platform }} + run: make archive-demo-${{ matrix.platform }} diff --git a/Scripts/configure-environment.sh b/Scripts/configure-environment.sh index 9b2f06a5..167d164e 100755 --- a/Scripts/configure-environment.sh +++ b/Scripts/configure-environment.sh @@ -1,14 +1,16 @@ #!/bin/bash -apple_api_key_p8="$1" +set -x + +apple_api_key_b64="$1" apple_account_info_b64="$2" -if [[ -z $apple_api_key_p8 || -z $apple_account_info_b64 ]] +if [[ -z $apple_api_key_b64 || -z $apple_account_info_b64 ]] then - echo "[!] Usage: $0 " + echo "[!] Usage: $0 " exit 1 fi -mkdir -p ../Configuration -echo "$apple_account_info_b64" | base64 --decode > ../Configuration/.env -echo "$apple_api_key_p8" > ../Configuration/AppStoreConnect_API_Key.p8 +mkdir -p Configuration +echo "$apple_account_info_b64" | base64 --decode > Configuration/.env +echo "$apple_api_key_b64" | base64 --decode > Configuration/AppStoreConnect_API_Key.p8 From c70d22a4e7086e83ba61f1f663185fe52dc3f0de Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Fri, 22 Nov 2024 08:47:41 +0100 Subject: [PATCH 28/68] Update iPhone used for fastlane --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index da9a2d4a..b55ee902 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -19,7 +19,7 @@ TESTFLIGHT_PLATFORMS = { }.freeze DEVICES = { - ios: 'iPhone 15', + ios: 'iPhone 16', tvos: 'Apple TV' }.freeze From f3ae4d34249f34d08fa8139ed7cd203ee64dbb6a Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Sat, 23 Nov 2024 07:57:46 +0100 Subject: [PATCH 29/68] Try to clean gems --- .github/workflows/pull-request.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index a902da5d..caba9460 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -62,4 +62,6 @@ jobs: ${{ secrets.APPLE_ACCOUNT_INFO_B64 }} - name: Archive the demo - run: make archive-demo-${{ matrix.platform }} + run: | + gem cleanup + make archive-demo-${{ matrix.platform }} From 33b6e6bb327b15629488f8f737c23208212e2913 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Sun, 24 Nov 2024 10:12:54 +0100 Subject: [PATCH 30/68] Remove gem cleanup --- .github/workflows/pull-request.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index caba9460..40189c3b 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -63,5 +63,4 @@ jobs: - name: Archive the demo run: | - gem cleanup make archive-demo-${{ matrix.platform }} From 2ff0d562615e533b1ac109e1c5536892a1f219ad Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Sun, 24 Nov 2024 10:47:34 +0100 Subject: [PATCH 31/68] Put back other jobs --- .github/workflows/pull-request.yml | 60 +++++++++++++++--------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 40189c3b..e107d291 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -4,42 +4,42 @@ name: Pull Request on: [push] jobs: - # check-quality: - # name: "🔎 Check quality" - # runs-on: tart - # steps: - # - name: Checkout code - # uses: actions/checkout@v4 + check-quality: + name: "🔎 Check quality" + runs-on: tart + steps: + - name: Checkout code + uses: actions/checkout@v4 - # - name: Run the quality check - # run: make check-quality + - name: Run the quality check + run: make check-quality + + build-documentation: + name: "📚 Build documentation" + runs-on: tart + steps: + - name: Checkout code + uses: actions/checkout@v4 - # build-documentation: - # name: "📚 Build documentation" - # runs-on: tart - # steps: - # - name: Checkout code - # uses: actions/checkout@v4 + - name: Build the documentation + run: make doc - # - name: Build the documentation - # run: make doc + tests: + name: "🧪 Tests" + runs-on: tart + strategy: + matrix: + platform: [ios, tvos] + steps: + - name: Checkout code + uses: actions/checkout@v4 - # tests: - # name: "🧪 Tests" - # runs-on: tart - # strategy: - # matrix: - # platform: [ios, tvos] - # steps: - # - name: Checkout code - # uses: actions/checkout@v4 + - name: Authorize microphone access for simulator + run: Scripts/bypass-simulator-trampoline.sh - # - name: Authorize microphone access for simulator - # run: Scripts/bypass-simulator-trampoline.sh + - name: Run tests + run: make test-${{ matrix.platform }} - # - name: Run tests - # run: make test-${{ matrix.platform }} - # archive-demos: name: "📦 Archives" runs-on: tart From 93451bcf6fe0d89a6f2a1348900d351427ae796f Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Mon, 25 Nov 2024 09:43:33 +0100 Subject: [PATCH 32/68] Reintroduce Gemfile for fastlane plugins --- Gemfile | 6 ++ Gemfile.lock | 230 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 Gemfile create mode 100644 Gemfile.lock diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..874d46de --- /dev/null +++ b/Gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..5faac9cb --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,230 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.3.0) + aws-partitions (1.1013.0) + aws-sdk-core (3.213.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.96.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.173.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.10.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + badge (0.13.0) + fastimage (>= 1.6) + fastlane (>= 2.0) + mini_magick (>= 4.9.4, < 5.0.0) + base64 (0.2.0) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.112.0) + faraday (1.10.4) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.3.1) + fastlane (2.225.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored (~> 1.2) + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (>= 0.1.1, < 1.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-plugin-badge (1.5.0) + badge (~> 0.13.0) + fastlane-plugin-xcconfig (2.1.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.7.1) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.7) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.6.2) + json (2.8.2) + jwt (2.9.3) + base64 + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.15.0) + multipart-post (2.4.1) + nanaimo (0.4.0) + naturally (2.2.1) + nkf (0.2.0) + optparse (0.6.0) + os (1.1.4) + plist (3.7.1) + public_suffix (6.0.1) + rake (13.2.1) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.3.9) + rouge (2.0.7) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.5) + signet (0.19.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + sysrandom (1.0.5) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + arm64-darwin-23 + ruby + +DEPENDENCIES + fastlane-plugin-badge + fastlane-plugin-xcconfig + +BUNDLED WITH + 2.5.23 From 39acf68322f7bb3a6a1231205e369a3d3d326b7c Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:10:50 +0100 Subject: [PATCH 33/68] Prefer using bundle for fastlane --- Gemfile | 2 ++ Gemfile.lock | 1 + Makefile | 49 ++++++++++++++++++++++++++++--------------------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/Gemfile b/Gemfile index 874d46de..eb64a720 100644 --- a/Gemfile +++ b/Gemfile @@ -2,5 +2,7 @@ source 'https://rubygems.org' +gem 'fastlane' + plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock index 5faac9cb..d027463b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -223,6 +223,7 @@ PLATFORMS ruby DEPENDENCIES + fastlane fastlane-plugin-badge fastlane-plugin-xcconfig diff --git a/Makefile b/Makefile index 888dabb0..71a5b49a 100644 --- a/Makefile +++ b/Makefile @@ -3,62 +3,69 @@ .PHONY: all all: help -.PHONY: install-tools -install-tools: - @echo "Installing tools..." +.PHONY: install-pkgx +install-pkgx: + @echo "Installing pkgx..." @curl -Ssf https://pkgx.sh | sh &> /dev/null @echo "... done.\n" +.PHONY: install-bundler +install-bundler: + @echo "Installing bundler..." + @pkgx bundle config set path '.bundle' + @pkgx bundle install + @echo "... done.\n" + .PHONY: fastlane -fastlane: install-tools - @pkgx fastlane +fastlane: install-pkgx install-bundler + @pkgx bundle exec fastlane .PHONY: archive-demo-ios -archive-demo-ios: install-tools - @pkgx fastlane archive_demo_ios +archive-demo-ios: install-pkgx install-bundler + @pkgx bundle exec fastlane archive_demo_ios .PHONY: archive-demo-tvos -archive-demo-tvos: install-tools - @pkgx fastlane archive_demo_tvos +archive-demo-tvos: install-pkgx install-bundler + @pkgx bundle exec fastlane archive_demo_tvos .PHONY: deliver-demo-nightly-ios -deliver-demo-nightly-ios: install-tools +deliver-demo-nightly-ios: install-pkgx @echo "Delivering demo nightly build for iOS..." @bundle exec fastlane deliver_demo_nightly_ios @echo "... done.\n" .PHONY: deliver-demo-nightly-tvos -deliver-demo-nightly-tvos: install-tools +deliver-demo-nightly-tvos: install-pkgx @echo "Delivering demo nightly build for tvOS..." @bundle exec fastlane deliver_demo_nightly_tvos @echo "... done.\n" .PHONY: deliver-demo-release-ios -deliver-demo-release-ios: install-tools +deliver-demo-release-ios: install-pkgx @echo "Delivering demo release build for iOS..." @bundle exec fastlane deliver_demo_release_ios @echo "... done.\n" .PHONY: deliver-demo-release-tvos -deliver-demo-release-tvos: install-tools +deliver-demo-release-tvos: install-pkgx @echo "Delivering demo release build for tvOS..." @bundle exec fastlane deliver_demo_release_tvos @echo "... done.\n" .PHONY: test-streams-start -test-streams-start: install-tools +test-streams-start: install-pkgx @echo "Starting test streams" @Scripts/test-streams.sh -s @echo "... done.\n" .PHONY: test-streams-stop -test-streams-stop: install-tools +test-streams-stop: install-pkgx @echo "Stopping test streams" @Scripts/test-streams.sh -k @echo "... done.\n" .PHONY: test-ios -test-ios: install-tools +test-ios: install-pkgx @echo "Running unit tests..." @Scripts/test-streams.sh -s @pkgx +xcodes fastlane test_ios @@ -66,7 +73,7 @@ test-ios: install-tools @echo "... done.\n" .PHONY: test-tvos -test-tvos: install-tools +test-tvos: install-pkgx @echo "Running unit tests..." @Scripts/test-streams.sh -s @pkgx fastlane test_tvos @@ -74,13 +81,13 @@ test-tvos: install-tools @echo "... done.\n" .PHONY: check-quality -check-quality: install-tools +check-quality: install-pkgx @echo "Checking quality..." @Scripts/check-quality.sh @echo "... done.\n" .PHONY: fix-quality -fix-quality: install-tools +fix-quality: install-pkgx @echo "Fixing quality..." @Scripts/fix-quality.sh @echo "... done.\n" @@ -127,7 +134,7 @@ find-dead-code: @echo "... done.\n" .PHONY: doc -doc: install-tools +doc: install-pkgx @echo "Generating documentation sets..." @pkgx fastlane doc @echo "... done.\n" @@ -137,7 +144,7 @@ help: @echo "The following targets are available:" @echo "" @echo " all Default target" - @echo " install-tools Install required tools" + @echo " install-pkgx Install required tools" @echo "" @echo " fastlane Run fastlane" @echo "" From d94c7e2bebe6bdc11739c3aefdcf5c7d50c7dfc7 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:26:52 +0100 Subject: [PATCH 34/68] Remove tests --- .github/workflows/pull-request.yml | 45 - .../xcshareddata/xcschemes/Pillarbox.xcscheme | 72 - Package.swift | 36 - .../ComScore/ComScoreHitExpectation.swift | 52 - .../ComScore/ComScorePageViewTests.swift | 332 ----- .../ComScore/ComScoreTestCase.swift | 78 - .../ComScoreTrackerDvrPropertiesTests.swift | 93 -- .../ComScoreTrackerMetadataTests.swift | 60 - .../ComScoreTrackerPlaybackSpeedTests.swift | 31 - .../ComScore/ComScoreTrackerRateTests.swift | 65 - .../ComScore/ComScoreTrackerSeekTests.swift | 60 - .../ComScore/ComScoreTrackerTests.swift | 237 ---- .../CommandersActEventTests.swift | 100 -- .../CommandersActHeartbeatTests.swift | 88 -- .../CommandersActHitExpectation.swift | 68 - .../CommandersActPageViewTests.swift | 183 --- .../CommandersAct/CommandersActTestCase.swift | 74 - ...mmandersActTrackerDvrPropertiesTests.swift | 126 -- .../CommandersActTrackerMetadataTests.swift | 143 -- .../CommandersActTrackerPositionTests.swift | 139 -- .../CommandersActTrackerSeekTests.swift | 83 -- .../CommandersActTrackerTests.swift | 208 --- .../Extensions/Dictionary.swift | 9 - Tests/AnalyticsTests/TestCase.swift | 45 - Tests/CircumspectTests/ComparatorTests.swift | 20 - Tests/CircumspectTests/Counter.swift | 20 - .../ExpectAtLeastPublishedTests.swift | 64 - .../ExpectNothingPublishedTests.swift | 24 - .../ExpectNotificationsTests.swift | 34 - .../ExpectOnlyPublishedTests.swift | 51 - .../Expectations/ExpectPublishedTests.swift | 56 - .../Expectations/ExpectResultTests.swift | 24 - .../Expectations/ExpectValueTests.swift | 40 - .../ObservableObjectTests.swift | 70 - Tests/CircumspectTests/PublishersTests.swift | 114 -- Tests/CircumspectTests/SimilarityTests.swift | 55 - .../CircumspectTests/TimeIntervalTests.swift | 20 - Tests/CircumspectTests/Tools.swift | 22 - .../AkamaiURLCodingTests.swift | 82 -- .../CoreBusinessTests/DataProviderTests.swift | 25 - Tests/CoreBusinessTests/ErrorsTests.swift | 20 - .../HTTPURLResponseTests.swift | 28 - .../MediaMetadataTests.swift | 76 - Tests/CoreBusinessTests/Mock.swift | 27 - Tests/CoreBusinessTests/PublishersTests.swift | 20 - .../Resources.xcassets/Contents.json | 6 - .../Contents.json | 12 - .../urn_rts_audio_13598743.json | 752 ---------- .../Contents.json | 12 - .../MediaComposition_drm.json | 315 ----- .../Contents.json | 12 - .../urn_rts_audio_3262320.json | 143 -- .../Contents.json | 12 - .../urn_rts_video_13360574.json | 179 --- .../Contents.json | 12 - .../urn_rts_video_14827796.json | 1258 ----------------- .../Contents.json | 12 - .../urn_rts_video_13360574.json | 210 --- .../Contents.json | 13 - .../urn_rts_video_13763072.json | 690 --------- .../CoreTests/AccumulatePublisherTests.swift | 186 --- Tests/CoreTests/ArrayTests.swift | 24 - Tests/CoreTests/CombineLatestTests.swift | 64 - Tests/CoreTests/ComparableTests.swift | 20 - Tests/CoreTests/DemandBufferTests.swift | 81 -- Tests/CoreTests/DispatchPublisherTests.swift | 93 -- Tests/CoreTests/LimitedBufferTests.swift | 31 - Tests/CoreTests/MeasurePublisherTests.swift | 36 - ...tificationPublisherDeallocationTests.swift | 43 - .../NotificationPublisherTests.swift | 47 - .../PublishAndRepeatOnOutputFromTests.swift | 38 - .../CoreTests/PublishOnOutputFromTests.swift | 38 - .../RangeReplaceableCollectionTests.swift | 51 - Tests/CoreTests/ReplaySubjectTests.swift | 170 --- Tests/CoreTests/SlicePublisherTests.swift | 27 - Tests/CoreTests/StopwatchTests.swift | 70 - Tests/CoreTests/TimeTests.swift | 78 - Tests/CoreTests/Tools.swift | 21 - Tests/CoreTests/TriggerTests.swift | 47 - Tests/CoreTests/WaitPublisherTests.swift | 25 - .../CoreTests/WeakCapturePublisherTests.swift | 39 - .../WithPreviousPublisherTests.swift | 45 - .../MetricHitExpectation.swift | 67 - Tests/MonitoringTests/MetricPayload.swift | 13 - Tests/MonitoringTests/MetricsTracker.swift | 27 - .../MonitoringTests/MetricsTrackerTests.swift | 225 --- .../MonitoringTests/MonitoringTestCase.swift | 75 - .../TrackingSessionTests.swift | 41 - .../AVPlayerItemRepeatAllUpdateTests.swift | 189 --- .../AVPlayerItemRepeatOffUpdateTests.swift | 189 --- .../AVPlayerItemRepeatOneUpdateTests.swift | 189 --- .../AVPlayer/AVPlayerItemTests.swift | 101 -- .../PlayerTests/AVPlayer/AVPlayerTests.swift | 50 - .../Asset/AssetCreationTests.swift | 29 - .../PlayerTests/Asset/AssetMetadataMock.swift | 23 - .../PlayerTests/Asset/ResourceItemTests.swift | 41 - .../AVAudioSessionNotificationTests.swift | 71 - .../PlayerTests/Extensions/AVPlayerItem.swift | 13 - .../PlayerTests/Extensions/AssetContent.swift | 16 - .../PlayerTests/Extensions/MetricEvent.swift | 16 - Tests/PlayerTests/Extensions/Player.swift | 15 - Tests/PlayerTests/Extensions/UUID.swift | 21 - .../AVMediaSelectionGroupTests.swift | 58 - .../AVMediaSelectionOptionTests.swift | 30 - .../MediaSelection/MediaSelectionTests.swift | 297 ---- ...erredLanguagesForMediaSelectionTests.swift | 144 -- .../Metrics/AccessLogEventTests.swift | 59 - .../Metrics/MetricsCollectorEventsTests.swift | 43 - .../Metrics/MetricsCollectorTests.swift | 78 - .../Metrics/MetricsStateTests.swift | 115 -- .../Player/BlockedTimeRangeTests.swift | 80 -- Tests/PlayerTests/Player/ErrorTests.swift | 38 +- .../Player/PlaybackSpeedUpdateTests.swift | 39 - Tests/PlayerTests/Player/PlaybackTests.swift | 48 - Tests/PlayerTests/Player/PlayerTests.swift | 76 - Tests/PlayerTests/Player/QueueTests.swift | 184 --- .../Player/ReplayChecksTests.swift | 76 - Tests/PlayerTests/Player/ReplayTests.swift | 77 - .../PlayerTests/Player/SeekChecksTests.swift | 54 - Tests/PlayerTests/Player/SeekTests.swift | 120 -- Tests/PlayerTests/Player/SpeedTests.swift | 205 --- .../Player/TextStyleRulesTests.swift | 48 - .../PlayerItemAssetPublisherTests.swift | 51 - .../PlayerItem/PlayerItemTests.swift | 129 -- .../Playlist/CurrentItemTests.swift | 191 --- .../Playlist/ItemInsertionAfterTests.swift | 88 -- .../Playlist/ItemInsertionBeforeTests.swift | 88 -- .../Playlist/ItemMoveAfterTests.swift | 147 -- .../Playlist/ItemMoveBeforeTests.swift | 147 -- .../ItemNavigationBackwardChecksTests.swift | 38 - .../ItemNavigationBackwardTests.swift | 48 - .../ItemNavigationForwardChecksTests.swift | 38 - .../Playlist/ItemNavigationForwardTests.swift | 63 - .../Playlist/ItemRemovalTests.swift | 62 - Tests/PlayerTests/Playlist/ItemsTests.swift | 70 - .../Playlist/ItemsUpdateTests.swift | 49 - .../NavigationBackwardChecksTests.swift | 111 -- .../Playlist/NavigationBackwardTests.swift | 137 -- .../NavigationForwardChecksTests.swift | 72 - .../Playlist/NavigationForwardTests.swift | 80 -- .../NavigationSmartBackwardChecksTests.swift | 107 -- .../NavigationSmartBackwardTests.swift | 118 -- .../NavigationSmartForwardChecksTests.swift | 72 - .../NavigationSmartForwardTests.swift | 80 -- .../Playlist/RepeatModeTests.swift | 53 - .../ProgressTrackerPlaybackStateTests.swift | 65 - ...ressTrackerProgressAvailabilityTests.swift | 132 -- .../ProgressTrackerProgressTests.swift | 157 -- .../ProgressTrackerRangeTests.swift | 132 -- .../ProgressTrackerSeekBehaviorTests.swift | 68 - .../ProgressTrackerTimeTests.swift | 153 -- .../ProgressTrackerValueTests.swift | 56 - ...etMediaSelectionGroupsPublisherTests.swift | 37 - ...hronousKeyValueLoadingPublisherTests.swift | 62 - .../AVPlayerBoundaryTimePublisherTests.swift | 74 - .../AVPlayerItemErrorPublisherTests.swift | 41 - ...VPlayerItemMetricEventPublisherTests.swift | 54 - .../AVPlayerPeriodicTimePublisherTests.swift | 60 - .../Publishers/MetadataPublisherTests.swift | 94 -- .../NowPlayingInfoPublisherTests.swift | 47 - .../PeriodicMetricsPublisherTests.swift | 72 - .../PlayerItemMetricEventPublisherTests.swift | 28 - .../Publishers/PlayerPublisherTests.swift | 159 --- .../QueuePlayer/QueuePlayerItemsTests.swift | 88 -- .../QueuePlayer/QueuePlayerSeekTests.swift | 286 ---- .../QueuePlayerSeekTimePublisherTests.swift | 94 -- .../QueuePlayerSmoothSeekTests.swift | 173 --- Tests/PlayerTests/Resources/invalid.jpg | 0 Tests/PlayerTests/Resources/pixel.jpg | Bin 373 -> 0 bytes .../Skips/SkipBackwardChecksTests.swift | 35 - .../PlayerTests/Skips/SkipBackwardTests.swift | 79 -- .../Skips/SkipForwardChecksTests.swift | 35 - .../PlayerTests/Skips/SkipForwardTests.swift | 126 -- .../Skips/SkipToDefaultChecksTests.swift | 55 - .../Skips/SkipToDefaultTests.swift | 91 -- .../Tools/AVMediaSelectionOptionMock.swift | 32 - .../Tools/ContentKeySessionDelegateMock.swift | 11 - .../Tools/LanguageIdentifiable.swift | 29 - Tests/PlayerTests/Tools/Matchers.swift | 19 - .../Tools/MediaAccessibilityDisplayType.swift | 25 - Tests/PlayerTests/Tools/PlayerItem.swift | 79 -- .../Tools/PlayerItemTrackerMock.swift | 69 - .../Tools/ResourceLoaderDelegateMock.swift | 24 - Tests/PlayerTests/Tools/Similarity.swift | 78 - Tests/PlayerTests/Tools/TestCase.swift | 22 - Tests/PlayerTests/Tools/Tools.swift | 13 - .../PlayerTests/Tools/TrackerUpdateMock.swift | 55 - .../PlayerItemTrackerLifeCycleTests.swift | 98 -- ...layerItemTrackerMetricPublisherTests.swift | 54 - .../PlayerItemTrackerSessionTests.swift | 31 - .../PlayerItemTrackerUpdateTests.swift | 58 - .../Tracking/PlayerTrackingTests.swift | 133 -- .../PlayerTests/Types/CMTimeRangeTests.swift | 48 - Tests/PlayerTests/Types/CMTimeTests.swift | 103 -- Tests/PlayerTests/Types/ErrorsTests.swift | 49 - .../PlayerTests/Types/ImageSourceTests.swift | 76 - Tests/PlayerTests/Types/ItemErrorTests.swift | 43 - .../Types/PlaybackSpeedTests.swift | 35 - .../Types/PlaybackStateTests.swift | 20 - .../Types/PlayerConfigurationTests.swift | 42 - .../PlayerTests/Types/PlayerLimitsTests.swift | 79 -- .../Types/PlayerMetadataTests.swift | 66 - Tests/PlayerTests/Types/PositionTests.swift | 47 - Tests/PlayerTests/Types/StreamTypeTests.swift | 23 - .../Types/TimePropertiesTests.swift | 69 - .../VisibilityTrackerTests.swift | 184 --- 206 files changed, 4 insertions(+), 17813 deletions(-) delete mode 100644 Tests/AnalyticsTests/ComScore/ComScoreHitExpectation.swift delete mode 100644 Tests/AnalyticsTests/ComScore/ComScorePageViewTests.swift delete mode 100644 Tests/AnalyticsTests/ComScore/ComScoreTestCase.swift delete mode 100644 Tests/AnalyticsTests/ComScore/ComScoreTrackerDvrPropertiesTests.swift delete mode 100644 Tests/AnalyticsTests/ComScore/ComScoreTrackerMetadataTests.swift delete mode 100644 Tests/AnalyticsTests/ComScore/ComScoreTrackerPlaybackSpeedTests.swift delete mode 100644 Tests/AnalyticsTests/ComScore/ComScoreTrackerRateTests.swift delete mode 100644 Tests/AnalyticsTests/ComScore/ComScoreTrackerSeekTests.swift delete mode 100644 Tests/AnalyticsTests/ComScore/ComScoreTrackerTests.swift delete mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActEventTests.swift delete mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActHeartbeatTests.swift delete mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActHitExpectation.swift delete mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActPageViewTests.swift delete mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActTestCase.swift delete mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActTrackerDvrPropertiesTests.swift delete mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActTrackerMetadataTests.swift delete mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActTrackerPositionTests.swift delete mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActTrackerSeekTests.swift delete mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActTrackerTests.swift delete mode 100644 Tests/AnalyticsTests/Extensions/Dictionary.swift delete mode 100644 Tests/AnalyticsTests/TestCase.swift delete mode 100644 Tests/CircumspectTests/ComparatorTests.swift delete mode 100644 Tests/CircumspectTests/Counter.swift delete mode 100644 Tests/CircumspectTests/Expectations/ExpectAtLeastPublishedTests.swift delete mode 100644 Tests/CircumspectTests/Expectations/ExpectNothingPublishedTests.swift delete mode 100644 Tests/CircumspectTests/Expectations/ExpectNotificationsTests.swift delete mode 100644 Tests/CircumspectTests/Expectations/ExpectOnlyPublishedTests.swift delete mode 100644 Tests/CircumspectTests/Expectations/ExpectPublishedTests.swift delete mode 100644 Tests/CircumspectTests/Expectations/ExpectResultTests.swift delete mode 100644 Tests/CircumspectTests/Expectations/ExpectValueTests.swift delete mode 100644 Tests/CircumspectTests/ObservableObjectTests.swift delete mode 100644 Tests/CircumspectTests/PublishersTests.swift delete mode 100644 Tests/CircumspectTests/SimilarityTests.swift delete mode 100644 Tests/CircumspectTests/TimeIntervalTests.swift delete mode 100644 Tests/CircumspectTests/Tools.swift delete mode 100644 Tests/CoreBusinessTests/AkamaiURLCodingTests.swift delete mode 100644 Tests/CoreBusinessTests/DataProviderTests.swift delete mode 100644 Tests/CoreBusinessTests/ErrorsTests.swift delete mode 100644 Tests/CoreBusinessTests/HTTPURLResponseTests.swift delete mode 100644 Tests/CoreBusinessTests/MediaMetadataTests.swift delete mode 100644 Tests/CoreBusinessTests/Mock.swift delete mode 100644 Tests/CoreBusinessTests/PublishersTests.swift delete mode 100644 Tests/CoreBusinessTests/Resources.xcassets/Contents.json delete mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_audioChapters.dataset/Contents.json delete mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_audioChapters.dataset/urn_rts_audio_13598743.json delete mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_drm.dataset/Contents.json delete mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_drm.dataset/MediaComposition_drm.json delete mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_live.dataset/Contents.json delete mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_live.dataset/urn_rts_audio_3262320.json delete mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_missingAnalytics.dataset/Contents.json delete mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_missingAnalytics.dataset/urn_rts_video_13360574.json delete mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_mixed.dataset/Contents.json delete mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_mixed.dataset/urn_rts_video_14827796.json delete mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_onDemand.dataset/Contents.json delete mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_onDemand.dataset/urn_rts_video_13360574.json delete mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_redundant.dataset/Contents.json delete mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_redundant.dataset/urn_rts_video_13763072.json delete mode 100644 Tests/CoreTests/AccumulatePublisherTests.swift delete mode 100644 Tests/CoreTests/ArrayTests.swift delete mode 100644 Tests/CoreTests/CombineLatestTests.swift delete mode 100644 Tests/CoreTests/ComparableTests.swift delete mode 100644 Tests/CoreTests/DemandBufferTests.swift delete mode 100644 Tests/CoreTests/DispatchPublisherTests.swift delete mode 100644 Tests/CoreTests/LimitedBufferTests.swift delete mode 100644 Tests/CoreTests/MeasurePublisherTests.swift delete mode 100644 Tests/CoreTests/NotificationPublisherDeallocationTests.swift delete mode 100644 Tests/CoreTests/NotificationPublisherTests.swift delete mode 100644 Tests/CoreTests/PublishAndRepeatOnOutputFromTests.swift delete mode 100644 Tests/CoreTests/PublishOnOutputFromTests.swift delete mode 100644 Tests/CoreTests/RangeReplaceableCollectionTests.swift delete mode 100644 Tests/CoreTests/ReplaySubjectTests.swift delete mode 100644 Tests/CoreTests/SlicePublisherTests.swift delete mode 100644 Tests/CoreTests/StopwatchTests.swift delete mode 100644 Tests/CoreTests/TimeTests.swift delete mode 100644 Tests/CoreTests/Tools.swift delete mode 100644 Tests/CoreTests/TriggerTests.swift delete mode 100644 Tests/CoreTests/WaitPublisherTests.swift delete mode 100644 Tests/CoreTests/WeakCapturePublisherTests.swift delete mode 100644 Tests/CoreTests/WithPreviousPublisherTests.swift delete mode 100644 Tests/MonitoringTests/MetricHitExpectation.swift delete mode 100644 Tests/MonitoringTests/MetricPayload.swift delete mode 100644 Tests/MonitoringTests/MetricsTracker.swift delete mode 100644 Tests/MonitoringTests/MetricsTrackerTests.swift delete mode 100644 Tests/MonitoringTests/MonitoringTestCase.swift delete mode 100644 Tests/MonitoringTests/TrackingSessionTests.swift delete mode 100644 Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatAllUpdateTests.swift delete mode 100644 Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatOffUpdateTests.swift delete mode 100644 Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatOneUpdateTests.swift delete mode 100644 Tests/PlayerTests/AVPlayer/AVPlayerItemTests.swift delete mode 100644 Tests/PlayerTests/AVPlayer/AVPlayerTests.swift delete mode 100644 Tests/PlayerTests/Asset/AssetCreationTests.swift delete mode 100644 Tests/PlayerTests/Asset/AssetMetadataMock.swift delete mode 100644 Tests/PlayerTests/Asset/ResourceItemTests.swift delete mode 100644 Tests/PlayerTests/AudioSession/AVAudioSessionNotificationTests.swift delete mode 100644 Tests/PlayerTests/Extensions/AVPlayerItem.swift delete mode 100644 Tests/PlayerTests/Extensions/AssetContent.swift delete mode 100644 Tests/PlayerTests/Extensions/MetricEvent.swift delete mode 100644 Tests/PlayerTests/Extensions/Player.swift delete mode 100644 Tests/PlayerTests/Extensions/UUID.swift delete mode 100644 Tests/PlayerTests/MediaSelection/AVMediaSelectionGroupTests.swift delete mode 100644 Tests/PlayerTests/MediaSelection/AVMediaSelectionOptionTests.swift delete mode 100644 Tests/PlayerTests/MediaSelection/MediaSelectionTests.swift delete mode 100644 Tests/PlayerTests/MediaSelection/PreferredLanguagesForMediaSelectionTests.swift delete mode 100644 Tests/PlayerTests/Metrics/AccessLogEventTests.swift delete mode 100644 Tests/PlayerTests/Metrics/MetricsCollectorEventsTests.swift delete mode 100644 Tests/PlayerTests/Metrics/MetricsCollectorTests.swift delete mode 100644 Tests/PlayerTests/Metrics/MetricsStateTests.swift delete mode 100644 Tests/PlayerTests/Player/BlockedTimeRangeTests.swift delete mode 100644 Tests/PlayerTests/Player/PlaybackSpeedUpdateTests.swift delete mode 100644 Tests/PlayerTests/Player/PlaybackTests.swift delete mode 100644 Tests/PlayerTests/Player/PlayerTests.swift delete mode 100644 Tests/PlayerTests/Player/QueueTests.swift delete mode 100644 Tests/PlayerTests/Player/ReplayChecksTests.swift delete mode 100644 Tests/PlayerTests/Player/ReplayTests.swift delete mode 100644 Tests/PlayerTests/Player/SeekChecksTests.swift delete mode 100644 Tests/PlayerTests/Player/SeekTests.swift delete mode 100644 Tests/PlayerTests/Player/SpeedTests.swift delete mode 100644 Tests/PlayerTests/Player/TextStyleRulesTests.swift delete mode 100644 Tests/PlayerTests/PlayerItem/PlayerItemAssetPublisherTests.swift delete mode 100644 Tests/PlayerTests/PlayerItem/PlayerItemTests.swift delete mode 100644 Tests/PlayerTests/Playlist/CurrentItemTests.swift delete mode 100644 Tests/PlayerTests/Playlist/ItemInsertionAfterTests.swift delete mode 100644 Tests/PlayerTests/Playlist/ItemInsertionBeforeTests.swift delete mode 100644 Tests/PlayerTests/Playlist/ItemMoveAfterTests.swift delete mode 100644 Tests/PlayerTests/Playlist/ItemMoveBeforeTests.swift delete mode 100644 Tests/PlayerTests/Playlist/ItemNavigationBackwardChecksTests.swift delete mode 100644 Tests/PlayerTests/Playlist/ItemNavigationBackwardTests.swift delete mode 100644 Tests/PlayerTests/Playlist/ItemNavigationForwardChecksTests.swift delete mode 100644 Tests/PlayerTests/Playlist/ItemNavigationForwardTests.swift delete mode 100644 Tests/PlayerTests/Playlist/ItemRemovalTests.swift delete mode 100644 Tests/PlayerTests/Playlist/ItemsTests.swift delete mode 100644 Tests/PlayerTests/Playlist/ItemsUpdateTests.swift delete mode 100644 Tests/PlayerTests/Playlist/NavigationBackwardChecksTests.swift delete mode 100644 Tests/PlayerTests/Playlist/NavigationBackwardTests.swift delete mode 100644 Tests/PlayerTests/Playlist/NavigationForwardChecksTests.swift delete mode 100644 Tests/PlayerTests/Playlist/NavigationForwardTests.swift delete mode 100644 Tests/PlayerTests/Playlist/NavigationSmartBackwardChecksTests.swift delete mode 100644 Tests/PlayerTests/Playlist/NavigationSmartBackwardTests.swift delete mode 100644 Tests/PlayerTests/Playlist/NavigationSmartForwardChecksTests.swift delete mode 100644 Tests/PlayerTests/Playlist/NavigationSmartForwardTests.swift delete mode 100644 Tests/PlayerTests/Playlist/RepeatModeTests.swift delete mode 100644 Tests/PlayerTests/ProgressTracker/ProgressTrackerPlaybackStateTests.swift delete mode 100644 Tests/PlayerTests/ProgressTracker/ProgressTrackerProgressAvailabilityTests.swift delete mode 100644 Tests/PlayerTests/ProgressTracker/ProgressTrackerProgressTests.swift delete mode 100644 Tests/PlayerTests/ProgressTracker/ProgressTrackerRangeTests.swift delete mode 100644 Tests/PlayerTests/ProgressTracker/ProgressTrackerSeekBehaviorTests.swift delete mode 100644 Tests/PlayerTests/ProgressTracker/ProgressTrackerTimeTests.swift delete mode 100644 Tests/PlayerTests/ProgressTracker/ProgressTrackerValueTests.swift delete mode 100644 Tests/PlayerTests/Publishers/AVAssetMediaSelectionGroupsPublisherTests.swift delete mode 100644 Tests/PlayerTests/Publishers/AVAsynchronousKeyValueLoadingPublisherTests.swift delete mode 100644 Tests/PlayerTests/Publishers/AVPlayerBoundaryTimePublisherTests.swift delete mode 100644 Tests/PlayerTests/Publishers/AVPlayerItemErrorPublisherTests.swift delete mode 100644 Tests/PlayerTests/Publishers/AVPlayerItemMetricEventPublisherTests.swift delete mode 100644 Tests/PlayerTests/Publishers/AVPlayerPeriodicTimePublisherTests.swift delete mode 100644 Tests/PlayerTests/Publishers/MetadataPublisherTests.swift delete mode 100644 Tests/PlayerTests/Publishers/NowPlayingInfoPublisherTests.swift delete mode 100644 Tests/PlayerTests/Publishers/PeriodicMetricsPublisherTests.swift delete mode 100644 Tests/PlayerTests/Publishers/PlayerItemMetricEventPublisherTests.swift delete mode 100644 Tests/PlayerTests/Publishers/PlayerPublisherTests.swift delete mode 100644 Tests/PlayerTests/QueuePlayer/QueuePlayerItemsTests.swift delete mode 100644 Tests/PlayerTests/QueuePlayer/QueuePlayerSeekTests.swift delete mode 100644 Tests/PlayerTests/QueuePlayer/QueuePlayerSeekTimePublisherTests.swift delete mode 100644 Tests/PlayerTests/QueuePlayer/QueuePlayerSmoothSeekTests.swift delete mode 100644 Tests/PlayerTests/Resources/invalid.jpg delete mode 100644 Tests/PlayerTests/Resources/pixel.jpg delete mode 100644 Tests/PlayerTests/Skips/SkipBackwardChecksTests.swift delete mode 100644 Tests/PlayerTests/Skips/SkipBackwardTests.swift delete mode 100644 Tests/PlayerTests/Skips/SkipForwardChecksTests.swift delete mode 100644 Tests/PlayerTests/Skips/SkipForwardTests.swift delete mode 100644 Tests/PlayerTests/Skips/SkipToDefaultChecksTests.swift delete mode 100644 Tests/PlayerTests/Skips/SkipToDefaultTests.swift delete mode 100644 Tests/PlayerTests/Tools/AVMediaSelectionOptionMock.swift delete mode 100644 Tests/PlayerTests/Tools/ContentKeySessionDelegateMock.swift delete mode 100644 Tests/PlayerTests/Tools/LanguageIdentifiable.swift delete mode 100644 Tests/PlayerTests/Tools/Matchers.swift delete mode 100644 Tests/PlayerTests/Tools/MediaAccessibilityDisplayType.swift delete mode 100644 Tests/PlayerTests/Tools/PlayerItem.swift delete mode 100644 Tests/PlayerTests/Tools/PlayerItemTrackerMock.swift delete mode 100644 Tests/PlayerTests/Tools/ResourceLoaderDelegateMock.swift delete mode 100644 Tests/PlayerTests/Tools/Similarity.swift delete mode 100644 Tests/PlayerTests/Tools/TestCase.swift delete mode 100644 Tests/PlayerTests/Tools/Tools.swift delete mode 100644 Tests/PlayerTests/Tools/TrackerUpdateMock.swift delete mode 100644 Tests/PlayerTests/Tracking/PlayerItemTrackerLifeCycleTests.swift delete mode 100644 Tests/PlayerTests/Tracking/PlayerItemTrackerMetricPublisherTests.swift delete mode 100644 Tests/PlayerTests/Tracking/PlayerItemTrackerSessionTests.swift delete mode 100644 Tests/PlayerTests/Tracking/PlayerItemTrackerUpdateTests.swift delete mode 100644 Tests/PlayerTests/Tracking/PlayerTrackingTests.swift delete mode 100644 Tests/PlayerTests/Types/CMTimeRangeTests.swift delete mode 100644 Tests/PlayerTests/Types/CMTimeTests.swift delete mode 100644 Tests/PlayerTests/Types/ErrorsTests.swift delete mode 100644 Tests/PlayerTests/Types/ImageSourceTests.swift delete mode 100644 Tests/PlayerTests/Types/ItemErrorTests.swift delete mode 100644 Tests/PlayerTests/Types/PlaybackSpeedTests.swift delete mode 100644 Tests/PlayerTests/Types/PlaybackStateTests.swift delete mode 100644 Tests/PlayerTests/Types/PlayerConfigurationTests.swift delete mode 100644 Tests/PlayerTests/Types/PlayerLimitsTests.swift delete mode 100644 Tests/PlayerTests/Types/PlayerMetadataTests.swift delete mode 100644 Tests/PlayerTests/Types/PositionTests.swift delete mode 100644 Tests/PlayerTests/Types/StreamTypeTests.swift delete mode 100644 Tests/PlayerTests/Types/TimePropertiesTests.swift delete mode 100644 Tests/PlayerTests/UserInterface/VisibilityTrackerTests.swift diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e107d291..2eebad66 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -4,26 +4,6 @@ name: Pull Request on: [push] jobs: - check-quality: - name: "🔎 Check quality" - runs-on: tart - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Run the quality check - run: make check-quality - - build-documentation: - name: "📚 Build documentation" - runs-on: tart - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Build the documentation - run: make doc - tests: name: "🧪 Tests" runs-on: tart @@ -39,28 +19,3 @@ jobs: - name: Run tests run: make test-${{ matrix.platform }} - - archive-demos: - name: "📦 Archives" - runs-on: tart - strategy: - matrix: - platform: [ios, tvos] - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Add Apple certificate - run: | - Scripts/add-apple-certificate.sh \ - ${{ secrets.SRGSSR_APPLE_DEV_CERTIFICATE_B64 }} - - - name: Configure environment - run: | - Scripts/configure-environment.sh \ - ${{ secrets.APP_STORE_CONNECT_API_KEY_B64 }} \ - ${{ secrets.APPLE_ACCOUNT_INFO_B64 }} - - - name: Archive the demo - run: | - make archive-demo-${{ matrix.platform }} diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Pillarbox.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Pillarbox.xcscheme index 2fe926da..9787d909 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Pillarbox.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Pillarbox.xcscheme @@ -185,78 +185,6 @@ region = "CH" codeCoverageEnabled = "YES"> - - - - - - - - - - - - - - - - - - - - - - - - Void - - fileprivate init(name: ComScoreHit.Name, evaluate: @escaping (ComScoreLabels) -> Void) { - self.name = name - self.evaluate = evaluate - } - - static func match(hit: ComScoreHit, with expectation: Self) -> Bool { - guard hit.name == expectation.name else { return false } - expectation.evaluate(hit.labels) - return true - } -} - -extension ComScoreHitExpectation: CustomDebugStringConvertible { - var debugDescription: String { - name.rawValue - } -} - -extension ComScoreTestCase { - func play(evaluate: @escaping (ComScoreLabels) -> Void = { _ in }) -> ComScoreHitExpectation { - ComScoreHitExpectation(name: .play, evaluate: evaluate) - } - - func playrt(evaluate: @escaping (ComScoreLabels) -> Void = { _ in }) -> ComScoreHitExpectation { - ComScoreHitExpectation(name: .playrt, evaluate: evaluate) - } - - func pause(evaluate: @escaping (ComScoreLabels) -> Void = { _ in }) -> ComScoreHitExpectation { - ComScoreHitExpectation(name: .pause, evaluate: evaluate) - } - - func end(evaluate: @escaping (ComScoreLabels) -> Void = { _ in }) -> ComScoreHitExpectation { - ComScoreHitExpectation(name: .end, evaluate: evaluate) - } - - func view(evaluate: @escaping (ComScoreLabels) -> Void = { _ in }) -> ComScoreHitExpectation { - ComScoreHitExpectation(name: .view, evaluate: evaluate) - } -} diff --git a/Tests/AnalyticsTests/ComScore/ComScorePageViewTests.swift b/Tests/AnalyticsTests/ComScore/ComScorePageViewTests.swift deleted file mode 100644 index 4e8e80da..00000000 --- a/Tests/AnalyticsTests/ComScore/ComScorePageViewTests.swift +++ /dev/null @@ -1,332 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import Nimble -import PillarboxCircumspect -import UIKit - -private class AutomaticMockViewController: UIViewController, PageViewTracking { - private var pageName: String { - title ?? "automatic" - } - - var comScorePageView: ComScorePageView { - .init(name: pageName) - } - - var commandersActPageView: CommandersActPageView { - .init(name: pageName, type: "type") - } - - init(title: String? = nil) { - super.init(nibName: nil, bundle: nil) - self.title = title - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -private class ManualMockViewController: UIViewController, PageViewTracking { - private var pageName: String { - "manual" - } - - var comScorePageView: ComScorePageView { - .init(name: pageName) - } - - var commandersActPageView: CommandersActPageView { - .init(name: pageName, type: "type") - } - - var isTrackedAutomatically: Bool { - false - } -} - -final class ComScorePageViewTests: ComScoreTestCase { - func testGlobals() { - expectAtLeastHits( - view { labels in - expect(labels.c2).to(equal("6036016")) - expect(labels.ns_ap_an).to(equal("xctest")) - expect(labels.c8).to(equal("name")) - expect(labels.ns_st_mp).to(beNil()) - expect(labels.ns_st_mv).to(beNil()) - expect(labels.mp_brand).to(equal("SRG")) - expect(labels.mp_v).notTo(beEmpty()) - expect(labels.cs_ucfr).to(beEmpty()) - } - ) { - Analytics.shared.trackPageView( - comScore: .init(name: "name"), - commandersAct: .init(name: "name", type: "type") - ) - } - } - - func testBlankTitle() { - guard nimbleThrowAssertionsAvailable() else { return } - expect(Analytics.shared.trackPageView( - comScore: .init(name: " "), - commandersAct: .init(name: "name", type: "type") - )).to(throwAssertion()) - } - - func testCustomLabels() { - expectAtLeastHits( - view { labels in - expect(labels["key"]).to(equal("value")) - } - ) { - Analytics.shared.trackPageView( - comScore: .init(name: "name", labels: ["key": "value"]), - commandersAct: .init(name: "name", type: "type") - ) - } - } - - func testCustomLabelsForbiddenOverrides() { - expectAtLeastHits( - view { labels in - expect(labels.c8).to(equal("name")) - expect(labels.cs_ucfr).to(beEmpty()) - } - ) { - Analytics.shared.trackPageView( - comScore: .init(name: "name", labels: [ - "c8": "overridden_title", - "cs_ucfr": "42" - ]), - commandersAct: .init(name: "name", type: "type") - ) - } - } - - func testDefaultProtocolImplementation() { - let viewController = AutomaticMockViewController() - expect(viewController.isTrackedAutomatically).to(beTrue()) - } - - func testCustomProtocolImplementation() { - let viewController = ManualMockViewController() - expect(viewController.isTrackedAutomatically).to(beFalse()) - } - - func testAutomaticTrackingWithoutProtocolImplementation() { - let viewController = UIViewController() - expectNoHits(during: .seconds(2)) { - viewController.simulateViewAppearance() - } - } - - func testManualTrackingWithoutProtocolImplementation() { - let viewController = UIViewController() - expectNoHits(during: .seconds(2)) { - viewController.trackPageView() - } - } - - func testAutomaticTrackingWhenViewAppears() { - let viewController = AutomaticMockViewController() - expectAtLeastHits( - view { labels in - expect(labels.c8).to(equal("automatic")) - } - ) { - viewController.simulateViewAppearance() - } - } - - func testSinglePageViewWhenContainerViewAppears() { - let viewController = AutomaticMockViewController() - let navigationController = UINavigationController(rootViewController: viewController) - expectHits( - view { labels in - expect(labels.c8).to(equal("automatic")) - }, - during: .seconds(2) - ) { - navigationController.simulateViewAppearance() - viewController.simulateViewAppearance() - } - } - - func testAutomaticTrackingWhenActiveViewInParentAppears() { - let viewController1 = AutomaticMockViewController(title: "title1") - let viewController2 = AutomaticMockViewController(title: "title2") - let tabBarController = UITabBarController() - tabBarController.viewControllers = [viewController1, viewController2] - expectAtLeastHits( - view { labels in - expect(labels.c8).to(equal("title1")) - } - ) { - viewController1.simulateViewAppearance() - } - } - - func testAutomaticTrackingWhenInactiveViewInParentAppears() { - let viewController1 = AutomaticMockViewController(title: "title1") - let viewController2 = AutomaticMockViewController(title: "title2") - let tabBarController = UITabBarController() - tabBarController.viewControllers = [viewController1, viewController2] - expectNoHits(during: .seconds(2)) { - viewController2.simulateViewAppearance() - } - } - - func testAutomaticTrackingWhenViewAppearsInActiveHierarchy() { - let viewController1 = AutomaticMockViewController(title: "title1") - let viewController2 = AutomaticMockViewController(title: "title2") - let tabBarController = UITabBarController() - tabBarController.viewControllers = [ - UINavigationController(rootViewController: viewController1), - UINavigationController(rootViewController: viewController2) - ] - expectAtLeastHits( - view { labels in - expect(labels.c8).to(equal("title1")) - } - ) { - viewController1.simulateViewAppearance() - } - } - - func testAutomaticTrackingWhenViewAppearsInInactiveHierarchy() { - let viewController1 = AutomaticMockViewController(title: "title1") - let viewController2 = AutomaticMockViewController(title: "title2") - let tabBarController = UITabBarController() - tabBarController.viewControllers = [ - UINavigationController(rootViewController: viewController1), - UINavigationController(rootViewController: viewController2) - ] - expectNoHits(during: .seconds(2)) { - viewController2.simulateViewAppearance() - } - } - - func testManualTracking() { - let viewController = ManualMockViewController() - expectNoHits(during: .seconds(2)) { - viewController.simulateViewAppearance() - } - expectAtLeastHits( - view { labels in - expect(labels.c8).to(equal("manual")) - } - ) { - viewController.trackPageView() - } - } - - func testRecursiveAutomaticTrackingOnViewController() { - let viewController = AutomaticMockViewController() - expectAtLeastHits( - view { labels in - expect(labels.c8).to(equal("automatic")) - } - ) { - viewController.recursivelyTrackAutomaticPageViews() - } - } - - func testRecursiveAutomaticTrackingOnNavigationController() { - let viewController = UINavigationController(rootViewController: AutomaticMockViewController(title: "root")) - viewController.pushViewController(AutomaticMockViewController(title: "pushed"), animated: false) - expectAtLeastHits( - view { labels in - expect(labels.c8).to(equal("pushed")) - } - ) { - viewController.recursivelyTrackAutomaticPageViews() - } - } - - func testRecursiveAutomaticTrackingOnPageViewController() { - let viewController = UIPageViewController() - viewController.setViewControllers([AutomaticMockViewController()], direction: .forward, animated: false) - expectAtLeastHits( - view { labels in - expect(labels.c8).to(equal("automatic")) - } - ) { - viewController.recursivelyTrackAutomaticPageViews() - } - } - - func testRecursiveAutomaticTrackingOnSplitViewController() { - let viewController = UISplitViewController() - viewController.viewControllers = [ - AutomaticMockViewController(title: "title1"), - AutomaticMockViewController(title: "title2") - ] - expectAtLeastHits( - view { labels in - expect(labels.c8).to(equal("title1")) - } - ) { - viewController.recursivelyTrackAutomaticPageViews() - } - } - - func testRecursiveAutomaticTrackingOnTabBarController() { - let viewController = UITabBarController() - viewController.viewControllers = [ - AutomaticMockViewController(title: "title1"), - AutomaticMockViewController(title: "title2"), - AutomaticMockViewController(title: "title3") - ] - expectAtLeastHits( - view { labels in - expect(labels.c8).to(equal("title1")) - } - ) { - viewController.recursivelyTrackAutomaticPageViews() - } - } - - func testRecursiveAutomaticTrackingOnWindow() { - let window = UIWindow() - window.makeKeyAndVisible() - window.rootViewController = AutomaticMockViewController() - expectAtLeastHits( - view { labels in - expect(labels.c8).to(equal("automatic")) - } - ) { - window.recursivelyTrackAutomaticPageViews() - } - } - - func testRecursiveAutomaticTrackingOnWindowWithModalPresentation() { - let window = UIWindow() - let rootViewController = AutomaticMockViewController(title: "root") - window.makeKeyAndVisible() - window.rootViewController = rootViewController - rootViewController.present(AutomaticMockViewController(title: "modal"), animated: false) - expectHits( - view { labels in - expect(labels.c8).to(equal("modal")) - }, - during: .seconds(2) - ) { - window.recursivelyTrackAutomaticPageViews() - } - } -} - -private extension UIViewController { - func simulateViewAppearance() { - beginAppearanceTransition(true, animated: false) - endAppearanceTransition() - } -} diff --git a/Tests/AnalyticsTests/ComScore/ComScoreTestCase.swift b/Tests/AnalyticsTests/ComScore/ComScoreTestCase.swift deleted file mode 100644 index 5c76107d..00000000 --- a/Tests/AnalyticsTests/ComScore/ComScoreTestCase.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import Dispatch -import PillarboxCircumspect - -class ComScoreTestCase: TestCase {} - -extension ComScoreTestCase { - /// Collects hits emitted by comScore during some time interval and matches them against expectations. - /// - /// A network connection is required by the comScore SDK to properly emit hits. - func expectHits( - _ expectations: ComScoreHitExpectation..., - during interval: DispatchTimeInterval = .seconds(20), - file: StaticString = #file, - line: UInt = #line, - while executing: (() -> Void)? = nil - ) { - AnalyticsListener.captureComScoreHits { publisher in - expectPublished( - values: expectations, - from: publisher, - to: ComScoreHitExpectation.match, - during: interval, - file: file, - line: line, - while: executing - ) - } - } - - /// Expects hits emitted by comScore during some time interval and matches them against expectations. - /// - /// A network connection is required by the comScore SDK to properly emit hits. - func expectAtLeastHits( - _ expectations: ComScoreHitExpectation..., - timeout: DispatchTimeInterval = .seconds(20), - file: StaticString = #file, - line: UInt = #line, - while executing: (() -> Void)? = nil - ) { - AnalyticsListener.captureComScoreHits { publisher in - expectAtLeastPublished( - values: expectations, - from: publisher, - to: ComScoreHitExpectation.match, - timeout: timeout, - file: file, - line: line, - while: executing - ) - } - } - - /// Expects no hits emitted by comScore during some time interval. - func expectNoHits( - during interval: DispatchTimeInterval = .seconds(20), - file: StaticString = #file, - line: UInt = #line, - while executing: (() -> Void)? = nil - ) { - AnalyticsListener.captureComScoreHits { publisher in - expectNothingPublished( - from: publisher, - during: interval, - file: file, - line: line, - while: executing - ) - } - } -} diff --git a/Tests/AnalyticsTests/ComScore/ComScoreTrackerDvrPropertiesTests.swift b/Tests/AnalyticsTests/ComScore/ComScoreTrackerDvrPropertiesTests.swift deleted file mode 100644 index b391dd75..00000000 --- a/Tests/AnalyticsTests/ComScore/ComScoreTrackerDvrPropertiesTests.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import CoreMedia -import Nimble -import PillarboxPlayer -import PillarboxStreams - -final class ComScoreTrackerDvrPropertiesTests: ComScoreTestCase { - func testOnDemand() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - - expectAtLeastHits( - play { labels in - expect(labels.ns_st_ldw).to(equal(0)) - expect(labels.ns_st_ldo).to(equal(0)) - } - ) { - player.play() - } - } - - func testLive() { - let player = Player(item: .simple( - url: Stream.live.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - - expectAtLeastHits( - play { labels in - expect(labels.ns_st_ldw).to(equal(0)) - expect(labels.ns_st_ldo).to(equal(0)) - } - ) { - player.play() - } - } - - func testDvrAtLiveEdge() { - let player = Player(item: .simple( - url: Stream.dvr.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - - expectAtLeastHits( - play { labels in - expect(labels.ns_st_ldo).to(equal(0)) - expect(labels.ns_st_ldw).to(equal(Stream.dvr.duration.seconds)) - } - ) { - player.play() - } - } - - func testDvrAwayFromLiveEdge() { - let player = Player(item: .simple( - url: Stream.dvr.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - - player.play() - expect(player.playbackState).toEventually(equal(.playing)) - - expectAtLeastHits( - pause { labels in - expect(labels.ns_st_ldo).to(equal(0)) - expect(labels.ns_st_ldw).to(equal(Stream.dvr.duration.seconds)) - }, - play { labels in - expect(labels.ns_st_ldo).to(beCloseTo(10, within: 3)) - expect(labels.ns_st_ldw).to(equal(Stream.dvr.duration.seconds)) - } - ) { - player.seek(at(player.time() - CMTime(value: 10, timescale: 1))) - } - } -} diff --git a/Tests/AnalyticsTests/ComScore/ComScoreTrackerMetadataTests.swift b/Tests/AnalyticsTests/ComScore/ComScoreTrackerMetadataTests.swift deleted file mode 100644 index 92e43ce9..00000000 --- a/Tests/AnalyticsTests/ComScore/ComScoreTrackerMetadataTests.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import Nimble -import PillarboxPlayer -import PillarboxStreams - -final class ComScoreTrackerMetadataTests: ComScoreTestCase { - func testMetadata() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in - ["meta_1": "custom-1", "meta_2": "42"] - } - ] - )) - - expectAtLeastHits( - play { labels in - expect(labels["meta_1"]).to(equal("custom-1")) - expect(labels["meta_2"]).to(equal(42)) - expect(labels["cs_ucfr"]).to(beEmpty()) - } - ) { - player.play() - } - } - - func testEmptyMetadata() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in [:] } - ] - )) - - expectNoHits(during: .seconds(3)) { - player.play() - } - } - - func testNoMetadata() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in [:] } - ] - )) - - expectNoHits(during: .seconds(3)) { - player.play() - } - } -} diff --git a/Tests/AnalyticsTests/ComScore/ComScoreTrackerPlaybackSpeedTests.swift b/Tests/AnalyticsTests/ComScore/ComScoreTrackerPlaybackSpeedTests.swift deleted file mode 100644 index 5ccc00f1..00000000 --- a/Tests/AnalyticsTests/ComScore/ComScoreTrackerPlaybackSpeedTests.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import Nimble -import PillarboxPlayer -import PillarboxStreams - -final class ComScoreTrackerPlaybackSpeedTests: ComScoreTestCase { - func testRateAtStart() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - player.setDesiredPlaybackSpeed(0.5) - - expectAtLeastHits( - play { labels in - expect(labels.ns_st_rt).to(equal(50)) - } - ) { - player.play() - } - } -} diff --git a/Tests/AnalyticsTests/ComScore/ComScoreTrackerRateTests.swift b/Tests/AnalyticsTests/ComScore/ComScoreTrackerRateTests.swift deleted file mode 100644 index fb88e1be..00000000 --- a/Tests/AnalyticsTests/ComScore/ComScoreTrackerRateTests.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import Nimble -import PillarboxPlayer -import PillarboxStreams - -final class ComScoreTrackerRateTests: ComScoreTestCase { - func testInitialRate() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - - expectAtLeastHits( - play { labels in - expect(labels.ns_st_rt).to(equal(200)) - } - ) { - player.setDesiredPlaybackSpeed(2) - player.play() - } - } - - func testWhenDifferentRateApplied() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - player.play() - expect(player.playbackState).toEventually(equal(.playing)) - - expectAtLeastHits( - playrt { labels in - expect(labels.ns_st_rt).to(equal(200)) - } - ) { - player.setDesiredPlaybackSpeed(2) - } - } - - func testWhenSameRateApplied() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - player.play() - expect(player.playbackState).toEventually(equal(.playing)) - - expectNoHits(during: .seconds(2)) { - player.setDesiredPlaybackSpeed(1) - } - } -} diff --git a/Tests/AnalyticsTests/ComScore/ComScoreTrackerSeekTests.swift b/Tests/AnalyticsTests/ComScore/ComScoreTrackerSeekTests.swift deleted file mode 100644 index e7e3d963..00000000 --- a/Tests/AnalyticsTests/ComScore/ComScoreTrackerSeekTests.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import CoreMedia -import Nimble -import PillarboxPlayer -import PillarboxStreams - -final class ComScoreTrackerSeekTests: ComScoreTestCase { - func testSeekWhilePlaying() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - - player.play() - expect(player.playbackState).toEventually(equal(.playing)) - - expectAtLeastHits( - pause { labels in - expect(labels.ns_st_po).to(beCloseTo(0, within: 0.5)) - }, - play { labels in - expect(labels.ns_st_po).to(beCloseTo(7, within: 0.5)) - } - ) { - player.seek(at(.init(value: 7, timescale: 1))) - } - } - - func testSeekWhilePaused() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - - expect(player.playbackState).toEventually(equal(.paused)) - - expectNoHits(during: .seconds(2)) { - player.seek(at(.init(value: 7, timescale: 1))) - } - - expectAtLeastHits( - play { labels in - expect(labels.ns_st_po).to(beCloseTo(7, within: 0.5)) - } - ) { - player.play() - } - } -} diff --git a/Tests/AnalyticsTests/ComScore/ComScoreTrackerTests.swift b/Tests/AnalyticsTests/ComScore/ComScoreTrackerTests.swift deleted file mode 100644 index a9082760..00000000 --- a/Tests/AnalyticsTests/ComScore/ComScoreTrackerTests.swift +++ /dev/null @@ -1,237 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import CoreMedia -import Nimble -import PillarboxPlayer -import PillarboxStreams - -// Testing comScore end events is a bit tricky: -// 1. Apparently comScore will never emit events if a play event is followed by an end event within ~5 seconds. For -// this reason all tests checking end events must wait ~5 seconds after a play event. -// 2. End events are emitted automatically to close a session when the `SCORStreamingAnalytics` is destroyed. Since -// we are not notifying the end event ourselves in such cases we cannot customize the end event labels directly. -// Fortunately we can customize them indirectly, though, since the end event inherits labels from a former event. -// Thus, to test end events resulting from tracker deallocation we need to have another event sent within the same -// expectation first so that the end event is provided a listener identifier. -final class ComScoreTrackerTests: ComScoreTestCase { - func testGlobals() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - expectAtLeastHits( - play { labels in - expect(labels.ns_st_mp).to(equal("Pillarbox")) - expect(labels.ns_st_mv).to(equal(PackageInfo.version)) - expect(labels.cs_ucfr).to(beEmpty()) - } - ) { - player.play() - } - } - - func testInitiallyPlaying() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - expectAtLeastHits( - play { labels in - expect(labels.ns_st_po).to(beCloseTo(0, within: 2)) - } - ) { - player.play() - } - } - - func testInitiallyPaused() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - expectNoHits(during: .seconds(2)) { - player.pause() - } - } - - func testPauseDuringPlayback() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - - player.play() - expect(player.time().seconds).toEventually(beGreaterThan(1)) - - expectAtLeastHits( - pause { labels in - expect(labels.ns_st_po).to(beCloseTo(1, within: 0.5)) - } - ) { - player.pause() - } - } - - func testPlaybackEnd() { - let player = Player(item: .simple( - // See 1. at the top of this file. - url: Stream.mediumOnDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - expectAtLeastHits( - play(), - end { labels in - expect(labels.ns_st_po).to(beCloseTo(Stream.mediumOnDemand.duration.seconds, within: 0.5)) - } - ) { - player.play() - } - } - - func testDestroyPlayerDuringPlayback() { - var player: Player? = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - expectAtLeastHits( - play(), - end { labels in - expect(labels.ns_st_po).to(beCloseTo(5, within: 0.5)) - } - ) { - // See 2. at the top of this file. - player?.play() - // See 1. at the top of this file. - expect(player?.time().seconds).toEventually(beGreaterThan(5)) - player = nil - } - } - - func testFailure() { - let player = Player(item: .simple( - url: Stream.unavailable.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - expectNoHits(during: .seconds(3)) { - player.play() - } - } - - func testDisableTrackingDuringPlayback() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - expectAtLeastHits(play(), end()) { - // See 2. at the top of this file. - player.play() - // See 1. at the top of this file. - expect(player.time().seconds).toEventually(beGreaterThan(5)) - player.isTrackingEnabled = false - } - } - - func testEnableTrackingDuringPlayback() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - - player.isTrackingEnabled = false - - expectNoHits(during: .seconds(2)) { - player.play() - } - - expectAtLeastHits(play()) { - player.isTrackingEnabled = true - } - } - - func testInitialPlaybackRate() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - expectAtLeastHits( - play { labels in - expect(labels.ns_st_rt).to(equal(200)) - } - ) { - player.setDesiredPlaybackSpeed(2) - player.play() - } - } - - func testOnDemandStartAtGivenPosition() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ], - configuration: .init(position: at(.init(value: 100, timescale: 1))) - )) - expectAtLeastHits( - play { labels in - expect(labels.ns_st_po).to(beCloseTo(100, within: 5)) - } - ) { - player.play() - } - } - - func testReplay() { - let player = Player(item: .simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [ - ComScoreTracker.adapter { _ in .test } - ] - )) - var ns_st_id: String? - expectAtLeastHits( - play { labels in - expect(labels["media_title"]).to(equal("name")) - expect(labels.ns_st_id).notTo(beNil()) - ns_st_id = labels.ns_st_id - }, - end() - ) { - player.play() - } - expectAtLeastHits( - play { labels in - expect(labels["media_title"]).to(equal("name")) - expect(labels.ns_st_id).notTo(beNil()) - expect(labels.ns_st_id).notTo(equal(ns_st_id)) - } - ) { - player.replay() - } - } -} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActEventTests.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActEventTests.swift deleted file mode 100644 index 0bee15fd..00000000 --- a/Tests/AnalyticsTests/CommandersAct/CommandersActEventTests.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import Nimble -import PillarboxCircumspect -import TCServerSide - -final class CommandersActEventTests: CommandersActTestCase { - func testMergingWithGlobals() { - let event = CommandersActEvent( - name: "name", - labels: [ - "event-label": "event", - "common-label": "event" - ] - ) - let globals = CommandersActGlobals( - consentServices: ["service1,service2,service3"], - labels: [ - "globals-label": "globals", - "common-label": "globals" - ] - ) - - expect(event.merging(globals: globals).labels).to(equal([ - "consent_services": "service1,service2,service3", - "globals-label": "globals", - "event-label": "event", - "common-label": "globals" - ])) - } - - func testBlankName() { - guard nimbleThrowAssertionsAvailable() else { return } - expect(Analytics.shared.sendEvent(commandersAct: .init(name: " "))).to(throwAssertion()) - } - - func testName() { - expectAtLeastHits(custom(name: "name")) { - Analytics.shared.sendEvent(commandersAct: .init(name: "name")) - } - } - - func testCustomLabels() { - expectAtLeastHits( - custom(name: "name") { labels in - // Use `media_player_display`, a media-only key, so that its value can be parsed. - expect(labels.media_player_display).to(equal("value")) - } - ) { - Analytics.shared.sendEvent(commandersAct: .init( - name: "name", - labels: ["media_player_display": "value"] - )) - } - } - - func testUniqueIdentifier() { - let identifier = TCPredefinedVariables.sharedInstance().uniqueIdentifier() - expectAtLeastHits( - custom(name: "name") { labels in - expect(labels.context.device.sdk_id).to(equal(identifier)) - expect(labels.user.consistent_anonymous_id).to(equal(identifier)) - } - ) { - Analytics.shared.sendEvent(commandersAct: .init(name: "name")) - } - } - - func testGlobals() { - expectAtLeastHits( - custom(name: "name") { labels in - expect(labels.consent_services).to(equal("service1,service2,service3")) - } - ) { - Analytics.shared.sendEvent(commandersAct: .init(name: "name")) - } - } - - func testCustomLabelsForbiddenOverrides() { - expectAtLeastHits( - custom(name: "name") { labels in - expect(labels.consent_services).to(equal("service1,service2,service3")) - } - ) { - Analytics.shared.sendEvent(commandersAct: .init( - name: "name", - labels: [ - "event_name": "overridden_name", - "consent_services": "service42" - ] - )) - } - } -} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActHeartbeatTests.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActHeartbeatTests.swift deleted file mode 100644 index 497cc2f8..00000000 --- a/Tests/AnalyticsTests/CommandersAct/CommandersActHeartbeatTests.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import Combine -import Nimble -import PillarboxPlayer -import PillarboxStreams - -final class CommandersActHeartbeatTests: CommandersActTestCase { - private var cancellables = Set() - - private static func player(from stream: PillarboxStreams.Stream, into cancellables: inout Set) -> Player { - let heartbeat = CommandersActHeartbeat(delay: 0.1, posInterval: 0.1, uptimeInterval: 0.2) - let player = Player(item: .simple(url: stream.url)) - player.propertiesPublisher - .sink { properties in - heartbeat.update(with: properties) { properties in - ["media_volume": properties.isMuted ? "0" : "100"] - } - } - .store(in: &cancellables) - return player - } - - override func tearDown() { - super.tearDown() - cancellables = [] - } - - func testNoHeartbeatInitially() { - _ = Self.player(from: .onDemand, into: &cancellables) - expectNoHits(during: .milliseconds(300)) - } - - func testOnDemandHeartbeatAfterPlay() { - let player = Self.player(from: .onDemand, into: &cancellables) - expectAtLeastHits(pos(), pos()) { - player.play() - } - } - - func testLiveHeartbeatAfterPlay() { - let player = Self.player(from: .live, into: &cancellables) - expectAtLeastHits(pos(), uptime(), pos(), pos(), uptime()) { - player.play() - } - } - - func testDvrHeartbeatAfterPlay() { - let player = Self.player(from: .dvr, into: &cancellables) - expectAtLeastHits(pos(), uptime(), pos(), pos(), uptime()) { - player.play() - } - } - - func testNoHeartbeatAfterPause() { - let player = Self.player(from: .onDemand, into: &cancellables) - expectAtLeastHits(pos()) { - player.play() - } - expectNoHits(during: .milliseconds(300)) { - player.pause() - } - } - - func testHeartbeatPropertyUpdate() { - let player = Self.player(from: .onDemand, into: &cancellables) - expectAtLeastHits( - pos { labels in - expect(labels.media_volume).notTo(equal(0)) - } - ) { - player.play() - } - expectAtLeastHits( - pos { labels in - expect(labels.media_volume).to(equal(0)) - } - ) { - player.isMuted = true - } - } -} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActHitExpectation.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActHitExpectation.swift deleted file mode 100644 index 3ba8bcb5..00000000 --- a/Tests/AnalyticsTests/CommandersAct/CommandersActHitExpectation.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import PillarboxAnalytics - -/// Describes a Commanders Act stream hit expectation. -struct CommandersActHitExpectation { - private let name: CommandersActHit.Name - private let evaluate: (CommandersActLabels) -> Void - - fileprivate init(name: CommandersActHit.Name, evaluate: @escaping (CommandersActLabels) -> Void) { - self.name = name - self.evaluate = evaluate - } - - static func match(hit: CommandersActHit, with expectation: Self) -> Bool { - guard hit.name == expectation.name else { return false } - expectation.evaluate(hit.labels) - return true - } -} - -extension CommandersActHitExpectation: CustomDebugStringConvertible { - var debugDescription: String { - name.rawValue - } -} - -extension CommandersActTestCase { - func play(evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { - CommandersActHitExpectation(name: .play, evaluate: evaluate) - } - - func pause(evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { - CommandersActHitExpectation(name: .pause, evaluate: evaluate) - } - - func seek(evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { - CommandersActHitExpectation(name: .seek, evaluate: evaluate) - } - - func stop(evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { - CommandersActHitExpectation(name: .stop, evaluate: evaluate) - } - - func eof(evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { - CommandersActHitExpectation(name: .eof, evaluate: evaluate) - } - - func pos(evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { - CommandersActHitExpectation(name: .pos, evaluate: evaluate) - } - - func uptime(evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { - CommandersActHitExpectation(name: .uptime, evaluate: evaluate) - } - - func page_view(evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { - CommandersActHitExpectation(name: .page_view, evaluate: evaluate) - } - - func custom(name: String, evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { - CommandersActHitExpectation(name: .custom(name), evaluate: evaluate) - } -} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActPageViewTests.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActPageViewTests.swift deleted file mode 100644 index 7aa5c7fa..00000000 --- a/Tests/AnalyticsTests/CommandersAct/CommandersActPageViewTests.swift +++ /dev/null @@ -1,183 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import Nimble -import PillarboxCircumspect - -final class CommandersActPageViewTests: CommandersActTestCase { - func testMergingWithGlobals() { - let pageView = CommandersActPageView( - name: "name", - type: "type", - labels: [ - "pageview-label": "pageview", - "common-label": "pageview" - ] - ) - let globals = CommandersActGlobals( - consentServices: ["service1,service2,service3"], - labels: [ - "globals-label": "globals", - "common-label": "globals" - ] - ) - - expect(pageView.merging(globals: globals).labels).to(equal([ - "consent_services": "service1,service2,service3", - "globals-label": "globals", - "pageview-label": "pageview", - "common-label": "globals" - ])) - } - - func testLabels() { - expectAtLeastHits( - page_view { labels in - expect(labels.page_type).to(equal("type")) - expect(labels.page_name).to(equal("name")) - expect(labels.navigation_level_0).to(beNil()) - expect(labels.navigation_level_1).to(equal("level_1")) - expect(labels.navigation_level_2).to(equal("level_2")) - expect(labels.navigation_level_3).to(equal("level_3")) - expect(labels.navigation_level_4).to(equal("level_4")) - expect(labels.navigation_level_5).to(equal("level_5")) - expect(labels.navigation_level_6).to(equal("level_6")) - expect(labels.navigation_level_7).to(equal("level_7")) - expect(labels.navigation_level_8).to(equal("level_8")) - expect(labels.navigation_level_9).to(beNil()) - expect(["phone", "tablet", "tvbox", "phone"]).to(contain([labels.navigation_device])) - expect(labels.app_library_version).to(equal(Analytics.version)) - expect(labels.navigation_app_site_name).to(equal("site")) - expect(labels.navigation_property_type).to(equal("app")) - expect(labels.navigation_bu_distributer).to(equal("SRG")) - expect(labels.consent_services).to(equal("service1,service2,service3")) - } - ) { - Analytics.shared.trackPageView( - comScore: .init(name: "name"), - commandersAct: .init( - name: "name", - type: "type", - levels: [ - "level_1", - "level_2", - "level_3", - "level_4", - "level_5", - "level_6", - "level_7", - "level_8" - ] - ) - ) - } - } - - func testBlankTitle() { - guard nimbleThrowAssertionsAvailable() else { return } - expect(Analytics.shared.trackPageView( - comScore: .init(name: "name"), - commandersAct: .init(name: " ", type: "type") - )).to(throwAssertion()) - } - - func testBlankType() { - guard nimbleThrowAssertionsAvailable() else { return } - expect(Analytics.shared.trackPageView( - comScore: .init(name: "name"), - commandersAct: .init(name: "name", type: " ") - )).to(throwAssertion()) - } - - func testBlankLevels() { - expectAtLeastHits( - page_view { labels in - expect(labels.page_type).to(equal("type")) - expect(labels.page_name).to(equal("name")) - expect(labels.navigation_level_1).to(beNil()) - expect(labels.navigation_level_2).to(beNil()) - expect(labels.navigation_level_3).to(beNil()) - expect(labels.navigation_level_4).to(beNil()) - expect(labels.navigation_level_5).to(beNil()) - expect(labels.navigation_level_6).to(beNil()) - expect(labels.navigation_level_7).to(beNil()) - expect(labels.navigation_level_8).to(beNil()) - } - ) { - Analytics.shared.trackPageView( - comScore: .init(name: "name"), - commandersAct: .init( - name: "name", - type: "type", - levels: [ - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " " - ] - ) - ) - } - } - - func testCustomLabels() { - expectAtLeastHits( - page_view { labels in - // Use `media_player_display`, a media-only key, so that its value can be parsed. - expect(labels.media_player_display).to(equal("value")) - } - ) { - Analytics.shared.trackPageView( - comScore: .init(name: "name"), - commandersAct: .init( - name: "name", - type: "type", - labels: ["media_player_display": "value"] - ) - ) - } - } - - func testGlobals() { - expectAtLeastHits( - page_view { labels in - expect(labels.consent_services).to(equal("service1,service2,service3")) - } - ) { - Analytics.shared.trackPageView( - comScore: .init(name: "name"), - commandersAct: .init(name: "name", type: "type") - ) - } - } - - func testLabelsForbiddenOverrides() { - expectAtLeastHits( - page_view { labels in - expect(labels.page_name).to(equal("name")) - expect(labels.consent_services).to(equal("service1,service2,service3")) - } - ) { - Analytics.shared.trackPageView( - comScore: .init(name: "name"), - commandersAct: .init( - name: "name", - type: "type", - labels: [ - "page_name": "overridden_title", - "consent_services": "service42" - ] - ) - ) - } - } -} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActTestCase.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActTestCase.swift deleted file mode 100644 index b43edc44..00000000 --- a/Tests/AnalyticsTests/CommandersAct/CommandersActTestCase.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import Dispatch -import PillarboxCircumspect - -class CommandersActTestCase: TestCase {} - -extension CommandersActTestCase { - /// Collects hits emitted by Commanders Act during some time interval and matches them against expectations. - func expectHits( - _ expectations: CommandersActHitExpectation..., - during interval: DispatchTimeInterval = .seconds(20), - file: StaticString = #file, - line: UInt = #line, - while executing: (() -> Void)? = nil - ) { - AnalyticsListener.captureCommandersActHits { publisher in - expectPublished( - values: expectations, - from: publisher, - to: CommandersActHitExpectation.match, - during: interval, - file: file, - line: line, - while: executing - ) - } - } - - /// Expects hits emitted by Commanders Act during some time interval and matches them against expectations. - func expectAtLeastHits( - _ expectations: CommandersActHitExpectation..., - timeout: DispatchTimeInterval = .seconds(20), - file: StaticString = #file, - line: UInt = #line, - while executing: (() -> Void)? = nil - ) { - AnalyticsListener.captureCommandersActHits { publisher in - expectAtLeastPublished( - values: expectations, - from: publisher, - to: CommandersActHitExpectation.match, - timeout: timeout, - file: file, - line: line, - while: executing - ) - } - } - - /// Expects no hits emitted by Commanders Act during some time interval. - func expectNoHits( - during interval: DispatchTimeInterval = .seconds(20), - file: StaticString = #file, - line: UInt = #line, - while executing: (() -> Void)? = nil - ) { - AnalyticsListener.captureCommandersActHits { publisher in - expectNothingPublished( - from: publisher, - during: interval, - file: file, - line: line, - while: executing - ) - } - } -} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerDvrPropertiesTests.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerDvrPropertiesTests.swift deleted file mode 100644 index 529456ec..00000000 --- a/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerDvrPropertiesTests.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import CoreMedia -import Nimble -import PillarboxCircumspect -import PillarboxPlayer -import PillarboxStreams - -final class CommandersActTrackerDvrPropertiesTests: CommandersActTestCase { - func testOnDemand() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - expectAtLeastHits( - play { labels in - expect(labels.media_timeshift).to(beNil()) - } - ) { - player.play() - } - } - - func testLive() { - let player = Player(item: .simple( - url: Stream.live.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - expectAtLeastHits( - play { labels in - expect(labels.media_timeshift).to(equal(0)) - } - ) { - player.play() - } - } - - func testDvrAtLiveEdge() { - let player = Player(item: .simple( - url: Stream.dvr.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - expectAtLeastHits( - play { labels in - expect(labels.media_timeshift).to(equal(0)) - } - ) { - player.play() - } - } - - func testDvrAwayFromLiveEdge() { - let player = Player(item: .simple( - url: Stream.dvr.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - player.play() - expect(player.playbackState).toEventually(equal(.playing)) - - expectAtLeastHits( - seek { labels in - expect(labels.media_timeshift).to(equal(0)) - }, - play { labels in - expect(labels.media_timeshift).to(beCloseTo(4, within: 2)) - } - ) { - player.seek(at(player.time() - CMTime(value: 4, timescale: 1))) - } - } - - func testDestroyPlayerDuringPlayback() { - var player: Player? = Player(item: .simple( - url: Stream.dvr.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - player?.play() - expect(player?.playbackState).toEventually(equal(.playing)) - - player?.pause() - wait(for: .seconds(2)) - - expectAtLeastHits( - stop { labels in - expect(labels.media_position).to(equal(0)) - } - ) { - player = nil - } - } - - func testDestroyPlayerWhileInitiallyPaused() { - var player: Player? = Player(item: .simple( - url: Stream.dvr.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - expect(player?.playbackState).toEventually(equal(.paused)) - - expectNoHits(during: .seconds(5)) { - player = nil - } - } -} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerMetadataTests.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerMetadataTests.swift deleted file mode 100644 index 4735135e..00000000 --- a/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerMetadataTests.swift +++ /dev/null @@ -1,143 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import Nimble -import PillarboxPlayer -import PillarboxStreams - -final class CommandersActTrackerMetadataTests: CommandersActTestCase { - func testWhenInitialized() { - var player: Player? - expectAtLeastHits( - play { labels in - expect(labels.media_player_display).to(equal("Pillarbox")) - expect(labels.media_player_version).to(equal(PackageInfo.version)) - expect(labels.media_volume).notTo(beNil()) - expect(labels.media_title).to(equal("name")) - expect(labels.media_audio_track).to(equal("UND")) - expect(labels.consent_services).to(equal("service1,service2,service3")) - } - ) { - player = Player(item: .simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - player?.setDesiredPlaybackSpeed(0.5) - player?.play() - } - } - - func testWhenDestroyed() { - var player: Player? = Player(item: .simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - player?.play() - expect(player?.playbackState).toEventually(equal(.playing)) - - expectAtLeastHits( - stop { labels in - expect(labels.media_player_display).to(equal("Pillarbox")) - expect(labels.media_player_version).to(equal(PackageInfo.version)) - expect(labels.media_volume).notTo(beNil()) - expect(labels.media_title).to(equal("name")) - expect(labels.media_audio_track).to(equal("UND")) - } - ) { - player = nil - } - } - - func testMuted() { - var player: Player? - expectAtLeastHits( - play { labels in - expect(labels.media_volume).to(equal(0)) - } - ) { - player = Player(item: .simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - player?.isMuted = true - player?.play() - } - } - - func testAudioTrack() { - let player = Player(item: .simple( - url: Stream.onDemandWithOptions.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - player.setMediaSelection(preferredLanguages: ["fr"], for: .audible) - player.play() - expect(player.playbackState).toEventually(equal(.playing)) - - expectAtLeastHits( - pause { labels in - expect(labels.media_audio_track).to(equal("FR")) - } - ) { - player.pause() - } - } - - func testSubtitlesOff() { - let player = Player(item: .simple( - url: Stream.onDemandWithOptions.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - player.play() - expect(player.playbackState).toEventually(equal(.playing)) - player.select(mediaOption: .off, for: .legible) - expect(player.currentMediaOption(for: .legible)).toEventually(equal(.off)) - - expectAtLeastHits( - pause { labels in - expect(labels.media_subtitles_on).to(beFalse()) - } - ) { - player.pause() - } - } - - func testSubtitlesOn() { - let player = Player(item: .simple( - url: Stream.onDemandWithOptions.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - player.setMediaSelection(preferredLanguages: ["fr"], for: .legible) - player.play() - expect(player.playbackState).toEventually(equal(.playing)) - - expectAtLeastHits( - pause { labels in - expect(labels.media_subtitles_on).to(beTrue()) - expect(labels.media_subtitle_selection).to(equal("FR")) - } - ) { - player.pause() - } - } -} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerPositionTests.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerPositionTests.swift deleted file mode 100644 index 5c870d27..00000000 --- a/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerPositionTests.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import CoreMedia -import Nimble -import PillarboxCircumspect -import PillarboxPlayer -import PillarboxStreams - -final class CommandersActTrackerPositionTests: CommandersActTestCase { - func testLivePlayback() { - let player = Player(item: .simple( - url: Stream.live.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - player.play() - expect(player.playbackState).toEventually(equal(.playing)) - wait(for: .seconds(2)) - - expectAtLeastHits( - pause { labels in - expect(labels.media_position).to(equal(2)) - } - ) { - player.pause() - } - } - - func testDvrPlayback() { - let player = Player(item: .simple( - url: Stream.dvr.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - player.play() - expect(player.playbackState).toEventually(equal(.playing)) - wait(for: .seconds(2)) - - expectAtLeastHits( - pause { labels in - expect(labels.media_position).to(equal(2)) - } - ) { - player.pause() - } - } - - func testSeekDuringDvrPlayback() { - let player = Player(item: .simple( - url: Stream.dvr.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - player.play() - expect(player.playbackState).toEventually(equal(.playing)) - - expectAtLeastHits( - seek { labels in - expect(labels.media_position).to(equal(0)) - }, - play { labels in - expect(labels.media_position).to(equal(0)) - } - ) { - player.seek(at(.init(value: 7, timescale: 1))) - } - } - - func testDestroyDuringLivePlayback() { - var player: Player? = Player(item: .simple( - url: Stream.live.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - player?.play() - expect(player?.playbackState).toEventually(equal(.playing)) - wait(for: .seconds(2)) - - expectAtLeastHits( - stop { labels in - expect(labels.media_position).to(equal(2)) - } - ) { - player = nil - } - } - - func testDestroyDuringDvrPlayback() { - var player: Player? = Player(item: .simple( - url: Stream.dvr.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - player?.play() - expect(player?.playbackState).toEventually(equal(.playing)) - wait(for: .seconds(2)) - - expectAtLeastHits( - stop { labels in - expect(labels.media_position).to(equal(2)) - } - ) { - player = nil - } - } - - func testOnDemandStartAtGivenPosition() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ], - configuration: .init(position: at(.init(value: 100, timescale: 1))) - )) - expectAtLeastHits( - play { labels in - expect(labels.media_position).to(equal(100)) - } - ) { - player.play() - } - } -} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerSeekTests.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerSeekTests.swift deleted file mode 100644 index 599d8aa6..00000000 --- a/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerSeekTests.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import Nimble -import PillarboxPlayer -import PillarboxStreams - -final class CommandersActTrackerSeekTests: CommandersActTestCase { - func testSeekWhilePlaying() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - player.play() - expect(player.playbackState).toEventually(equal(.playing)) - - expectAtLeastHits( - seek { labels in - expect(labels.media_position).to(equal(0)) - }, - play { labels in - expect(labels.media_position).to(equal(7)) - } - ) { - player.seek(at(.init(value: 7, timescale: 1))) - } - } - - func testSeekWhilePaused() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - expect(player.playbackState).toEventually(equal(.paused)) - - expectNoHits(during: .seconds(2)) { - player.seek(at(.init(value: 7, timescale: 1))) - } - - expectAtLeastHits( - play { labels in - expect(labels.media_position).to(equal(7)) - } - ) { - player.play() - } - } - - func testDestroyPlayerWhileSeeking() { - var player: Player? = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - player?.play() - expect(player?.playbackState).toEventually(equal(.playing)) - - expectAtLeastHits( - seek { labels in - expect(labels.media_position).to(equal(0)) - }, - stop { labels in - expect(labels.media_position).to(equal(7)) - } - ) { - player?.seek(at(.init(value: 7, timescale: 1))) - player = nil - } - } -} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerTests.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerTests.swift deleted file mode 100644 index b5e47067..00000000 --- a/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerTests.swift +++ /dev/null @@ -1,208 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxAnalytics - -import Nimble -import PillarboxPlayer -import PillarboxStreams - -final class CommandersActTrackerTests: CommandersActTestCase { - func testInitiallyPlaying() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - expectAtLeastHits( - play { labels in - expect(labels.media_position).to(equal(0)) - } - ) { - player.play() - } - } - - func testInitiallyPaused() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in [:] } - ] - )) - expectNoHits(during: .seconds(2)) { - player.pause() - } - } - - func testPauseDuringPlayback() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - player.play() - expect(player.time().seconds).toEventually(beGreaterThan(1)) - - expectAtLeastHits( - pause { labels in - expect(labels.media_position).to(equal(1)) - } - ) { - player.pause() - } - } - - func testPlaybackEnd() { - let player = Player(item: .simple( - url: Stream.mediumOnDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - expectAtLeastHits( - play(), - eof { labels in - expect(labels.media_position).to(equal(Int(Stream.mediumOnDemand.duration.seconds))) - } - ) { - player.play() - } - } - - func testDestroyPlayerDuringPlayback() { - var player: Player? = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - - player?.play() - expect(player?.time().seconds).toEventually(beGreaterThan(5)) - - expectAtLeastHits( - stop { labels in - expect(labels.media_position).to(equal(5)) - } - ) { - player = nil - } - } - - func testDestroyPlayerDuringPlaybackAtNonStandardPlaybackSpeed() { - var player: Player? = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in .test } - ] - )) - player?.setDesiredPlaybackSpeed(2) - - player?.play() - expect(player?.time().seconds).toEventually(beGreaterThan(2)) - - expectAtLeastHits( - stop { labels in - expect(labels.media_position).to(equal(2)) - } - ) { - player = nil - } - } - - func testDestroyPlayerAfterPlayback() { - var player: Player? = Player(item: .simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in [:] } - ] - )) - - expectAtLeastHits(play(), eof()) { - player?.play() - } - - expectNoHits(during: .seconds(2)) { - player = nil - } - } - - func testFailure() { - let player = Player(item: .simple( - url: Stream.unavailable.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in [:] } - ] - )) - expectNoHits(during: .seconds(3)) { - player.play() - } - } - - func testDisableTrackingDuringPlayback() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in [:] } - ] - )) - - player.play() - expect(player.time().seconds).toEventually(beGreaterThan(5)) - - expectAtLeastHits(stop()) { - player.isTrackingEnabled = false - } - } - - func testEnableTrackingDuringPlayback() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in [:] } - ] - )) - - player.isTrackingEnabled = false - - expectNoHits(during: .seconds(2)) { - player.play() - } - - expectAtLeastHits(play()) { - player.isTrackingEnabled = true - } - } - - func testEnableTrackingAgainWhilePaused() { - let player = Player() - player.append(.simple( - url: Stream.onDemand.url, - trackerAdapters: [ - CommandersActTracker.adapter { _ in [:] } - ] - )) - - expectAtLeastHits(play()) { - player.play() - } - expectAtLeastHits(stop()) { - player.isTrackingEnabled = false - } - - player.pause() - expect(player.playbackState).toEventually(equal(.paused)) - - expectAtLeastHits(play()) { - player.isTrackingEnabled = true - player.play() - } - } -} diff --git a/Tests/AnalyticsTests/Extensions/Dictionary.swift b/Tests/AnalyticsTests/Extensions/Dictionary.swift deleted file mode 100644 index 7a81bfee..00000000 --- a/Tests/AnalyticsTests/Extensions/Dictionary.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -extension [String: String] { - static let test = ["media_title": "name"] -} diff --git a/Tests/AnalyticsTests/TestCase.swift b/Tests/AnalyticsTests/TestCase.swift deleted file mode 100644 index a6d6572e..00000000 --- a/Tests/AnalyticsTests/TestCase.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import Nimble -import PillarboxAnalytics -import XCTest - -private final class TestCaseDataSource: AnalyticsDataSource { - var comScoreGlobals: ComScoreGlobals { - .init(consent: .unknown, labels: [:]) - } - - var commandersActGlobals: CommandersActGlobals { - .init(consentServices: ["service1", "service2", "service3"], labels: [:]) - } -} - -/// A simple test suite with more tolerant Nimble settings. Beware that `toAlways` and `toNever` expectations appearing -/// in tests will use the same value by default and should likely always provide an explicit `until` parameter. -class TestCase: XCTestCase { - private static let dataSource = TestCaseDataSource() - - override class func setUp() { - PollingDefaults.timeout = .seconds(20) - PollingDefaults.pollInterval = .milliseconds(100) - try? Analytics.shared.start( - with: .init(vendor: .SRG, sourceKey: .developmentSourceKey, appSiteName: "site"), - dataSource: dataSource - ) - } - - override class func tearDown() { - PollingDefaults.timeout = .seconds(1) - PollingDefaults.pollInterval = .milliseconds(10) - } - - override func setUp() { - waitUntil { done in - AnalyticsListener.start(completion: done) - } - } -} diff --git a/Tests/CircumspectTests/ComparatorTests.swift b/Tests/CircumspectTests/ComparatorTests.swift deleted file mode 100644 index d3124016..00000000 --- a/Tests/CircumspectTests/ComparatorTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCircumspect - -import Nimble -import XCTest - -final class ComparatorTests: XCTestCase { - func testClose() { - expect(beClose(within: 0.1)(0.3, 0.3)).to(beTrue()) - } - - func testDistant() { - expect(beClose(within: 0.1)(0.3, 0.5)).to(beFalse()) - } -} diff --git a/Tests/CircumspectTests/Counter.swift b/Tests/CircumspectTests/Counter.swift deleted file mode 100644 index 5caae25d..00000000 --- a/Tests/CircumspectTests/Counter.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import Combine -import Foundation - -final class Counter: ObservableObject { - @Published var count = 0 - - init() { - Timer.publish(every: 0.2, on: .main, in: .common) - .autoconnect() - .map { _ in 1 } - .scan(0) { $0 + $1 } - .assign(to: &$count) - } -} diff --git a/Tests/CircumspectTests/Expectations/ExpectAtLeastPublishedTests.swift b/Tests/CircumspectTests/Expectations/ExpectAtLeastPublishedTests.swift deleted file mode 100644 index 28455cf7..00000000 --- a/Tests/CircumspectTests/Expectations/ExpectAtLeastPublishedTests.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCircumspect - -import Combine -import XCTest - -final class ExpectAtLeastPublishedTests: XCTestCase { - func testExpectAtLeastEqualPublishedValues() { - expectAtLeastEqualPublished( - values: [1, 2, 3, 4, 5], - from: [1, 2, 3, 4, 5].publisher - ) - } - - func testExpectAtLeastEqualPublishedValuesWhileExecuting() { - let subject = PassthroughSubject() - expectAtLeastEqualPublished( - values: [4, 7], - from: subject - ) { - subject.send(4) - subject.send(7) - subject.send(completion: .finished) - } - } - - func testExpectAtLeastEqualPublishedNextValues() { - expectAtLeastEqualPublishedNext( - values: [2, 3, 4, 5], - from: [1, 2, 3, 4, 5].publisher - ) - } - - func testExpectAtLeastEqualPublishedNextValuesWhileExecuting() { - let subject = PassthroughSubject() - expectAtLeastEqualPublishedNext( - values: [7, 8], - from: subject - ) { - subject.send(4) - subject.send(7) - subject.send(8) - subject.send(completion: .finished) - } - } - - func testExpectAtLeastEqualFollowingExpectEqual() { - let publisher = PassthroughSubject() - expectEqualPublished(values: [1, 2], from: publisher, during: .milliseconds(100)) { - publisher.send(1) - publisher.send(2) - } - expectAtLeastEqualPublished(values: [3, 4, 5], from: publisher) { - publisher.send(3) - publisher.send(4) - publisher.send(5) - } - } -} diff --git a/Tests/CircumspectTests/Expectations/ExpectNothingPublishedTests.swift b/Tests/CircumspectTests/Expectations/ExpectNothingPublishedTests.swift deleted file mode 100644 index 71d7d9cd..00000000 --- a/Tests/CircumspectTests/Expectations/ExpectNothingPublishedTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCircumspect - -import Combine -import XCTest - -final class ExpectNothingPublishedTests: XCTestCase { - func testExpectNothingPublished() { - let subject = PassthroughSubject() - expectNothingPublished(from: subject, during: .seconds(1)) - } - - func testExpectNothingPublishedNext() { - let subject = PassthroughSubject() - expectNothingPublishedNext(from: subject, during: .seconds(1)) { - subject.send(4) - } - } -} diff --git a/Tests/CircumspectTests/Expectations/ExpectNotificationsTests.swift b/Tests/CircumspectTests/Expectations/ExpectNotificationsTests.swift deleted file mode 100644 index 302c4227..00000000 --- a/Tests/CircumspectTests/Expectations/ExpectNotificationsTests.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCircumspect - -import XCTest - -final class ExpectNotificationsTests: XCTestCase { - func testExpectAtLeastReceivedNotifications() { - expectAtLeastReceived( - notifications: [ - Notification(name: .testNotification, object: self) - ], - for: [.testNotification] - ) { - NotificationCenter.default.post(name: .testNotification, object: self) - } - } - - func testExpectReceivedNotificationsDuringInterval() { - expectReceived( - notifications: [ - Notification(name: .testNotification, object: self) - ], - for: [.testNotification], - during: .milliseconds(500) - ) { - NotificationCenter.default.post(name: .testNotification, object: self) - } - } -} diff --git a/Tests/CircumspectTests/Expectations/ExpectOnlyPublishedTests.swift b/Tests/CircumspectTests/Expectations/ExpectOnlyPublishedTests.swift deleted file mode 100644 index 2fb55a85..00000000 --- a/Tests/CircumspectTests/Expectations/ExpectOnlyPublishedTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCircumspect - -import Combine -import XCTest - -final class ExpectOnlyPublishedTests: XCTestCase { - func testExpectOnlyEqualPublishedValues() { - expectOnlyEqualPublished( - values: [1, 2, 3, 4, 5], - from: [1, 2, 3, 4, 5].publisher - ) - } - - func testExpectOnlyEqualPublishedValuesWhileExecuting() { - let subject = PassthroughSubject() - expectOnlyEqualPublished( - values: [4, 7], - from: subject - ) { - subject.send(4) - subject.send(7) - subject.send(completion: .finished) - } - } - - func testExpectOnlyEqualPublishedNextValues() { - expectOnlyEqualPublishedNext( - values: [2, 3, 4, 5], - from: [1, 2, 3, 4, 5].publisher - ) - } - - func testExpectOnlyEqualPublishedNextValuesWhileExecuting() { - let subject = PassthroughSubject() - expectOnlyEqualPublishedNext( - values: [7, 8], - from: subject - ) { - subject.send(4) - subject.send(7) - subject.send(8) - subject.send(completion: .finished) - } - } -} diff --git a/Tests/CircumspectTests/Expectations/ExpectPublishedTests.swift b/Tests/CircumspectTests/Expectations/ExpectPublishedTests.swift deleted file mode 100644 index 19460c7f..00000000 --- a/Tests/CircumspectTests/Expectations/ExpectPublishedTests.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCircumspect - -import Combine -import XCTest - -final class ExpectPublishedTests: XCTestCase { - func testExpectEqualPublishedValuesDuringInterval() { - let counter = Counter() - expectEqualPublished( - values: [0, 1, 2], - from: counter.$count, - during: .milliseconds(500) - ) - } - - func testExpectEqualPublishedValuesDuringIntervalWhileExecuting() { - let subject = PassthroughSubject() - expectEqualPublished( - values: [4, 7, 8], - from: subject, - during: .milliseconds(500) - ) { - subject.send(4) - subject.send(7) - subject.send(8) - } - } - - func testExpectEqualPublishedNextValuesDuringInterval() { - let counter = Counter() - expectEqualPublishedNext( - values: [1, 2], - from: counter.$count, - during: .milliseconds(500) - ) - } - - func testExpectEqualPublishedNextValuesDuringIntervalWhileExecuting() { - let subject = PassthroughSubject() - expectEqualPublishedNext( - values: [7, 8], - from: subject, - during: .milliseconds(500) - ) { - subject.send(4) - subject.send(7) - subject.send(8) - } - } -} diff --git a/Tests/CircumspectTests/Expectations/ExpectResultTests.swift b/Tests/CircumspectTests/Expectations/ExpectResultTests.swift deleted file mode 100644 index bede2084..00000000 --- a/Tests/CircumspectTests/Expectations/ExpectResultTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCircumspect - -import Combine -import XCTest - -final class ExpectResultTests: XCTestCase { - func testExpectSuccess() { - expectSuccess(from: Empty()) - } - - func testExpectFailure() { - expectFailure(from: Fail(error: StructError())) - } - - func testExpectFailureWithError() { - expectFailure(StructError(), from: Fail(error: StructError())) - } -} diff --git a/Tests/CircumspectTests/Expectations/ExpectValueTests.swift b/Tests/CircumspectTests/Expectations/ExpectValueTests.swift deleted file mode 100644 index 9fe7cdb0..00000000 --- a/Tests/CircumspectTests/Expectations/ExpectValueTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCircumspect - -import Combine -import XCTest - -private class Object: ObservableObject { - @Published var value = 0 -} - -final class ExpectValueTests: XCTestCase { - func testSingleValue() { - expectValue(from: Just(1)) - } - - func testMultipleValues() { - expectValue(from: [1, 2, 3].publisher) - } - - func testSingleChange() { - let object = Object() - expectChange(from: object) { - object.value = 1 - } - } - - func testMultipleChanges() { - let object = Object() - expectChange(from: object) { - object.value = 1 - object.value = 2 - object.value = 3 - } - } -} diff --git a/Tests/CircumspectTests/ObservableObjectTests.swift b/Tests/CircumspectTests/ObservableObjectTests.swift deleted file mode 100644 index 5c23cfc0..00000000 --- a/Tests/CircumspectTests/ObservableObjectTests.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCircumspect - -import Nimble -import XCTest - -private class TestObservableObject: ObservableObject { - @Published var publishedProperty1 = 1 - @Published var publishedProperty2 = "a" - - var nonPublishedProperty: Int { - publishedProperty1 * 2 - } -} - -final class ObservableObjectTests: XCTestCase { - func testNonPublishedPropertyInitialValue() { - let object = TestObservableObject() - expectAtLeastEqualPublished( - values: [2], - from: object.changePublisher(at: \.nonPublishedProperty) - ) - } - - func testPublishedPropertyInitialValue() { - let object = TestObservableObject() - expectAtLeastEqualPublished( - values: [1], - from: object.changePublisher(at: \.publishedProperty1) - ) - } - - func testNonPublishedPropertyChanges() { - let object = TestObservableObject() - expectAtLeastEqualPublished( - values: [2, 8, 8], - from: object.changePublisher(at: \.nonPublishedProperty) - ) { - object.publishedProperty1 = 4 - object.publishedProperty2 = "b" - } - } - - func testPublishedPropertyChanges() { - let object = TestObservableObject() - expectAtLeastEqualPublished( - values: [1, 3, 3, 3], - from: object.changePublisher(at: \.publishedProperty1) - ) { - object.publishedProperty1 = 2 - object.publishedProperty1 = 3 - object.publishedProperty2 = "b" - } - } - - func testDeallocation() { - var object: TestObservableObject? = TestObservableObject() - _ = object?.changePublisher(at: \.nonPublishedProperty) - weak var weakObject = object - autoreleasepool { - object = nil - } - expect(weakObject).to(beNil()) - } -} diff --git a/Tests/CircumspectTests/PublishersTests.swift b/Tests/CircumspectTests/PublishersTests.swift deleted file mode 100644 index 4878f69a..00000000 --- a/Tests/CircumspectTests/PublishersTests.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCircumspect - -import Combine -import Nimble -import XCTest - -final class PublisherTests: XCTestCase { - func testWaitForSuccessResult() { - let values = try? waitForResult(from: [1, 2, 3, 4, 5].publisher).get() - expect(values).to(equal([1, 2, 3, 4, 5])) - } - - func testWaitForSuccessResultWhileExecuting() { - let subject = PassthroughSubject() - let values = try? waitForResult(from: subject) { - subject.send(4) - subject.send(7) - subject.send(completion: .finished) - }.get() - expect(values).to(equal([4, 7])) - } - - func testWaitForFailureResult() { - let values = try? waitForResult(from: Fail(error: StructError())).get() - expect(values).to(beNil()) - } - - func testWaitForOutput() throws { - let values = try waitForOutput(from: [1, 2, 3].publisher) - expect(values).to(equal([1, 2, 3])) - } - - func testWaitForOutputWhileExecuting() throws { - let subject = PassthroughSubject() - let values = try waitForOutput(from: subject) { - subject.send(4) - subject.send(7) - subject.send(completion: .finished) - } - expect(values).to(equal([4, 7])) - } - - func testWaitForSingleOutput() throws { - let value = try waitForSingleOutput(from: [1].publisher) - expect(value).to(equal(1)) - } - - func testWaitForSingleOutputWhileExecuting() throws { - let subject = PassthroughSubject() - let value = try waitForSingleOutput(from: subject) { - subject.send(4) - subject.send(completion: .finished) - } - expect(value).to(equal(4)) - } - - func testWaitForFailure() throws { - let error = try waitForFailure(from: Fail(error: StructError())) - expect(error).notTo(beNil()) - } - - func testWaitForFailureWhileExecuting() throws { - let subject = PassthroughSubject() - let error = try waitForFailure(from: Fail(error: StructError())) { - subject.send(4) - subject.send(7) - subject.send(completion: .failure(StructError())) - } - expect(error).notTo(beNil()) - } - - func testCollectOutput() { - let counter = Counter() - let values = collectOutput(from: counter.$count, during: .milliseconds(500)) - expect(values).to(equal([0, 1, 2])) - } - - func testCollectOutputWhileExecuting() { - let subject = PassthroughSubject() - let values = collectOutput(from: subject, during: .milliseconds(500)) { - subject.send(4) - subject.send(7) - } - expect(values).to(equal([4, 7])) - } - - func testCollectOutputImmediately() { - let values = collectOutput( - from: [1, 2, 3, 4, 5].publisher, - during: .never - ) - expect(values).to(equal([1, 2, 3, 4, 5])) - } - - func testCollectFirst() throws { - let values = try waitForOutput( - from: [1, 2, 3, 4, 5].publisher.collectFirst(3) - ).flatMap { $0 } - expect(values).to(equal([1, 2, 3])) - } - - func testCollectNext() throws { - let values = try waitForOutput( - from: [1, 2, 3, 4, 5].publisher.collectNext(3) - ).flatMap { $0 } - expect(values).to(equal([2, 3, 4])) - } -} diff --git a/Tests/CircumspectTests/SimilarityTests.swift b/Tests/CircumspectTests/SimilarityTests.swift deleted file mode 100644 index b359d9e7..00000000 --- a/Tests/CircumspectTests/SimilarityTests.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCircumspect - -import Nimble -import XCTest - -final class SimilarityTests: XCTestCase { - func testOperatorForInstances() { - expect(NamedPerson(name: "Alice") ~~ NamedPerson(name: "alice")).to(beTrue()) - expect(NamedPerson(name: "Alice") ~~ NamedPerson(name: "bob")).to(beFalse()) - } - - func testOperatorForOptionals() { - let alice1: NamedPerson? = NamedPerson(name: "Alice") - let alice2: NamedPerson? = NamedPerson(name: "alice") - let bob = NamedPerson(name: "Bob") - expect(alice1 ~~ alice2).to(beTrue()) - expect(alice1 ~~ bob).to(beFalse()) - } - - func testOperatorForArrays() { - let array1 = [NamedPerson(name: "Alice"), NamedPerson(name: "Bob")] - let array2 = [NamedPerson(name: "alice"), NamedPerson(name: "bob")] - let array3 = [NamedPerson(name: "bob"), NamedPerson(name: "alice")] - expect(array1 ~~ array2).to(beTrue()) - expect(array1 ~~ array3).to(beFalse()) - } - - func testBeSimilarForInstances() { - expect(NamedPerson(name: "Alice")).to(beSimilarTo(NamedPerson(name: "alice"))) - expect(NamedPerson(name: "Alice")).notTo(beSimilarTo(NamedPerson(name: "bob"))) - } - - func testBeSimilarForOptionals() { - let alice1: NamedPerson? = NamedPerson(name: "Alice") - let alice2: NamedPerson? = NamedPerson(name: "alice") - let bob = NamedPerson(name: "Bob") - expect(alice1).to(beSimilarTo(alice2)) - expect(alice1).notTo(beNil()) - expect(alice1).notTo(beSimilarTo(bob)) - } - - func testBeSimilarForArrays() { - let array1 = [NamedPerson(name: "Alice"), NamedPerson(name: "Bob")] - let array2 = [NamedPerson(name: "alice"), NamedPerson(name: "bob")] - let array3 = [NamedPerson(name: "bob"), NamedPerson(name: "alice")] - expect(array1).to(beSimilarTo(array2)) - expect(array1).notTo(beSimilarTo(array3)) - } -} diff --git a/Tests/CircumspectTests/TimeIntervalTests.swift b/Tests/CircumspectTests/TimeIntervalTests.swift deleted file mode 100644 index 4f9f082f..00000000 --- a/Tests/CircumspectTests/TimeIntervalTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCircumspect - -import Dispatch -import Nimble -import XCTest - -final class TimeIntervalTests: XCTestCase { - func testDoubleConversion() { - expect(DispatchTimeInterval.seconds(1).double()).to(equal(1)) - expect(DispatchTimeInterval.milliseconds(1_000).double()).to(equal(1)) - expect(DispatchTimeInterval.microseconds(1_000_000).double()).to(equal(1)) - expect(DispatchTimeInterval.nanoseconds(1_000_000_000).double()).to(equal(1)) - } -} diff --git a/Tests/CircumspectTests/Tools.swift b/Tests/CircumspectTests/Tools.swift deleted file mode 100644 index c35423dd..00000000 --- a/Tests/CircumspectTests/Tools.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import Foundation -import PillarboxCircumspect - -struct StructError: Error {} - -struct NamedPerson: Similar { - let name: String - - static func ~~ (lhs: Self, rhs: Self) -> Bool { - lhs.name.localizedCaseInsensitiveContains(rhs.name) - } -} - -extension Notification.Name { - static let testNotification = Notification.Name("TestNotification") -} diff --git a/Tests/CoreBusinessTests/AkamaiURLCodingTests.swift b/Tests/CoreBusinessTests/AkamaiURLCodingTests.swift deleted file mode 100644 index ab3a58ab..00000000 --- a/Tests/CoreBusinessTests/AkamaiURLCodingTests.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCoreBusiness - -import Nimble -import XCTest - -final class AkamaiURLCodingTests: XCTestCase { - private static let uuid = "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" - - func testEncoding() { - expect(AkamaiURLCoding.encodeUrl( - URL(string: "http://www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, - id: UUID(uuidString: Self.uuid)! - )) - .to(equal(URL(string: "akamai+E621E1F8-C36C-495A-93FC-0C247A3E6E5F+http://www.server.com/playlist.m3u8?param1=value1¶m2=value2"))) - - expect(AkamaiURLCoding.encodeUrl( - URL(string: "https://www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, - id: UUID(uuidString: Self.uuid)! - )) - .to(equal(URL(string: "akamai+E621E1F8-C36C-495A-93FC-0C247A3E6E5F+https://www.server.com/playlist.m3u8?param1=value1¶m2=value2"))) - } - - func testFailedEncoding() { - expect(AkamaiURLCoding.encodeUrl( - URL(string: "//www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, - id: UUID(uuidString: Self.uuid)! - )) - .to(equal(URL(string: "//www.server.com/playlist.m3u8?param1=value1¶m2=value2"))) - } - - func testDecoding() { - expect(AkamaiURLCoding.decodeUrl( - URL(string: "akamai+E621E1F8-C36C-495A-93FC-0C247A3E6E5F+http://www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, - id: UUID(uuidString: Self.uuid)! - )) - .to(equal(URL(string: "http://www.server.com/playlist.m3u8?param1=value1¶m2=value2"))) - - expect(AkamaiURLCoding.decodeUrl( - URL(string: "akamai+E621E1F8-C36C-495A-93FC-0C247A3E6E5F+https://www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, - id: UUID(uuidString: Self.uuid)! - )) - .to(equal(URL(string: "https://www.server.com/playlist.m3u8?param1=value1¶m2=value2"))) - } - - func testFailedDecoding() { - expect(AkamaiURLCoding.decodeUrl( - URL(string: "http://www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, - id: UUID(uuidString: Self.uuid)! - )) - .to(beNil()) - - expect(AkamaiURLCoding.decodeUrl( - URL(string: "https://www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, - id: UUID(uuidString: Self.uuid)! - )) - .to(beNil()) - - expect(AkamaiURLCoding.decodeUrl( - URL(string: "custom://www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, - id: UUID(uuidString: Self.uuid)! - )) - .to(beNil()) - - expect(AkamaiURLCoding.decodeUrl( - URL(string: "//www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, - id: UUID(uuidString: Self.uuid)! - )) - .to(beNil()) - - expect(AkamaiURLCoding.decodeUrl( - URL(string: "akamai+1111111-1111-1111-1111-111111111111+https://www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, - id: UUID(uuidString: Self.uuid)! - )) - .to(beNil()) - } -} diff --git a/Tests/CoreBusinessTests/DataProviderTests.swift b/Tests/CoreBusinessTests/DataProviderTests.swift deleted file mode 100644 index 8c95d437..00000000 --- a/Tests/CoreBusinessTests/DataProviderTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCoreBusiness - -import PillarboxCircumspect -import XCTest - -final class DataProviderTests: XCTestCase { - func testExistingMediaMetadata() { - expectSuccess( - from: DataProvider().mediaCompositionPublisher(forUrn: "urn:rts:video:6820736") - ) - } - - func testNonExistingMediaMetadata() { - expectFailure( - DataError.http(withStatusCode: 404), - from: DataProvider().mediaCompositionPublisher(forUrn: "urn:rts:video:unknown") - ) - } -} diff --git a/Tests/CoreBusinessTests/ErrorsTests.swift b/Tests/CoreBusinessTests/ErrorsTests.swift deleted file mode 100644 index 50cb74ae..00000000 --- a/Tests/CoreBusinessTests/ErrorsTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCoreBusiness - -import Nimble -import XCTest - -final class ErrorTests: XCTestCase { - func testHttpError() { - expect(DataError.http(withStatusCode: 404)).notTo(beNil()) - } - - func testNotHttpNSError() { - expect(DataError.http(withStatusCode: 200)).to(beNil()) - } -} diff --git a/Tests/CoreBusinessTests/HTTPURLResponseTests.swift b/Tests/CoreBusinessTests/HTTPURLResponseTests.swift deleted file mode 100644 index ef384e0f..00000000 --- a/Tests/CoreBusinessTests/HTTPURLResponseTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCoreBusiness - -import Nimble -import XCTest - -final class HTTPURLResponseTests: XCTestCase { - func testFixedLocalizedStringForValidStatusCode() { - expect(HTTPURLResponse.fixedLocalizedString(forStatusCode: 404)).to(equal("Not found")) - } - - func testFixedLocalizedStringForInvalidStatusCode() { - expect(HTTPURLResponse.fixedLocalizedString(forStatusCode: 956)).to(equal("Server error")) - } - - func testNetworkLocalizedStringForValidKey() { - expect(HTTPURLResponse.coreNetworkLocalizedString(forKey: "not found")).to(equal("Not found")) - } - - func testNetworkLocalizedStringForInvalidKey() { - expect(HTTPURLResponse.coreNetworkLocalizedString(forKey: "Some key which does not exist")).to(equal("Unknown error.")) - } -} diff --git a/Tests/CoreBusinessTests/MediaMetadataTests.swift b/Tests/CoreBusinessTests/MediaMetadataTests.swift deleted file mode 100644 index 6df3cf9e..00000000 --- a/Tests/CoreBusinessTests/MediaMetadataTests.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCoreBusiness - -import Nimble -import XCTest - -final class MediaMetadataTests: XCTestCase { - private static func metadata(_ kind: Mock.MediaCompositionKind) throws -> MediaMetadata { - try MediaMetadata( - mediaCompositionResponse: .init( - mediaComposition: Mock.mediaComposition(kind), - response: .init() - ), - dataProvider: DataProvider() - ) - } - - func testStandardMetadata() throws { - let metadata = try Self.metadata(.onDemand) - expect(metadata.title).to(equal("Yadebat")) - expect(metadata.subtitle).to(equal("On réunit des ex après leur rupture")) - expect(metadata.description).to(equal(""" - Dans ce nouvel épisode de YADEBAT, Mélissa réunit 3 couples qui se sont séparés récemment. \ - Elles les a questionné en face à face pour connaître leurs différents ressentis et réactions. - """)) - expect(metadata.episodeInformation).to(equal(.long(season: 2, episode: 12))) - } - - func testRedundantMetadata() throws { - let metadata = try Self.metadata(.redundant) - expect(metadata.title).to(equal("19h30")) - expect(metadata.subtitle).to(contain("February")) - expect(metadata.description).to(beNil()) - expect(metadata.episodeInformation).to(beNil()) - } - - func testLiveMetadata() throws { - let metadata = try Self.metadata(.live) - expect(metadata.title).to(equal("La 1ère en direct")) - expect(metadata.subtitle).to(beNil()) - expect(metadata.description).to(beNil()) - expect(metadata.episodeInformation).to(beNil()) - } - - func testMainChapter() throws { - let metadata = try Self.metadata(.onDemand) - expect(metadata.mainChapter.urn).to(equal(metadata.mediaComposition.chapterUrn)) - } - - func testChapters() throws { - let metadata = try Self.metadata(.mixed) - expect(metadata.chapters).to(haveCount(10)) - } - - func testAudioChapterRemoval() throws { - let metadata = try Self.metadata(.audioChapters) - expect(metadata.chapters).to(beEmpty()) - } - - func testAnalytics() throws { - let metadata = try Self.metadata(.onDemand) - expect(metadata.analyticsData).notTo(beEmpty()) - expect(metadata.analyticsMetadata).notTo(beEmpty()) - } - - func testMissingChapterAnalytics() throws { - let metadata = try Self.metadata(.missingAnalytics) - expect(metadata.analyticsData).to(beEmpty()) - expect(metadata.analyticsMetadata).to(beEmpty()) - } -} diff --git a/Tests/CoreBusinessTests/Mock.swift b/Tests/CoreBusinessTests/Mock.swift deleted file mode 100644 index 88ef7dbe..00000000 --- a/Tests/CoreBusinessTests/Mock.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCoreBusiness - -import Foundation -import UIKit - -enum Mock { - enum MediaCompositionKind: String { - case missingAnalytics - case drm - case live - case onDemand - case redundant - case mixed - case audioChapters - } - - static func mediaComposition(_ kind: MediaCompositionKind = .onDemand) -> MediaComposition { - let data = NSDataAsset(name: "MediaComposition_\(kind.rawValue)", bundle: .module)!.data - return try! DataProvider.decoder().decode(MediaComposition.self, from: data) - } -} diff --git a/Tests/CoreBusinessTests/PublishersTests.swift b/Tests/CoreBusinessTests/PublishersTests.swift deleted file mode 100644 index 610e95c8..00000000 --- a/Tests/CoreBusinessTests/PublishersTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCoreBusiness - -import PillarboxCircumspect -import XCTest - -final class PublishersTests: XCTestCase { - func testHttpError() { - expectFailure( - DataError.http(withStatusCode: 404), - from: URLSession(configuration: .default).dataTaskPublisher(for: URL(string: "http://localhost:8123/not_found")!) - .mapHttpErrors() - ) - } -} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/Contents.json b/Tests/CoreBusinessTests/Resources.xcassets/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/Tests/CoreBusinessTests/Resources.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_audioChapters.dataset/Contents.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_audioChapters.dataset/Contents.json deleted file mode 100644 index 2ac61c6c..00000000 --- a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_audioChapters.dataset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "data" : [ - { - "filename" : "urn_rts_audio_13598743.json", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_audioChapters.dataset/urn_rts_audio_13598743.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_audioChapters.dataset/urn_rts_audio_13598743.json deleted file mode 100644 index d6b7b9a1..00000000 --- a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_audioChapters.dataset/urn_rts_audio_13598743.json +++ /dev/null @@ -1,752 +0,0 @@ -{ - "chapterUrn" : "urn:rts:audio:13598743", - "episode" : { - "id" : "13598757", - "title" : "Forum du 12.12.2022", - "publishedDate" : "2022-12-12T18:00:00+01:00", - "imageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/16x9", - "imageTitle" : "Logo Forum [RTS]" - }, - "show" : { - "id" : "1784426", - "vendor" : "RTS", - "transmission" : "RADIO", - "urn" : "urn:rts:show:radio:1784426", - "title" : "Forum", - "lead" : "7 jours sur 7, Forum questionne en direct les acteurs de l’actualité, ouvre le débat sur les controverses qui animent la vie politique, culturelle et économique.", - "description" : "7 jours sur 7, Forum questionne en direct les acteurs de l'actualité, ouvre le débat sur les controverses qui animent la vie politique, culturelle et économique. C'est un lieu d'écoute, d'échanges, de remise en question. Forum propose chaque soir un regard attentif et acéré sur l’actualité suisse et internationale.", - "imageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/16x9", - "imageTitle" : "Logo Forum [RTS]", - "bannerImageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/3x1", - "posterImageUrl" : "https://ws.srf.ch/asset/image/audio/e0322b37-5697-474d-93ac-19a4044a6a24/POSTER.jpg", - "posterImageIsFallbackUrl" : true, - "homepageUrl" : "https://details.rts.ch/la-1ere/programmes/forum/", - "podcastSubscriptionUrl" : "https://www.rts.ch/la-1ere/programmes/forum/podcast/", - "primaryChannelId" : "a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", - "primaryChannelUrn" : "urn:rts:channel:radio:a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", - "audioDescriptionAvailable" : false, - "subtitlesAvailable" : false, - "multiAudioLanguagesAvailable" : false, - "allowIndexing" : false - }, - "channel" : { - "id" : "a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", - "vendor" : "RTS", - "urn" : "urn:rts:channel:radio:a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", - "title" : "La 1ère", - "imageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/16x9", - "imageTitle" : "Logo Forum [RTS]", - "transmission" : "RADIO" - }, - "chapterList" : [ { - "id" : "13598743", - "mediaType" : "AUDIO", - "vendor" : "RTS", - "urn" : "urn:rts:audio:13598743", - "title" : "Forum - Présenté par Tania Sazpinar et Esther Coquoz", - "imageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/16x9", - "imageTitle" : "Logo Forum [RTS]", - "type" : "EPISODE", - "date" : "2022-12-12T18:00:00+01:00", - "duration" : 3600000, - "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598743/d96d95c9-23cf-38b8-92af-42e89cd85afa.mp3", - "playableAbroad" : true, - "socialCountList" : [ { - "key" : "srgView", - "value" : 898 - }, { - "key" : "srgLike", - "value" : 0 - }, { - "key" : "fbShare", - "value" : 0 - }, { - "key" : "twitterShare", - "value" : 0 - }, { - "key" : "googleShare", - "value" : 0 - }, { - "key" : "whatsAppShare", - "value" : 0 - } ], - "displayable" : true, - "position" : 0, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Forum - Présenté par Tania Sazpinar et Esther Coquoz", - "media_type" : "Audio", - "media_segment_id" : "13598743", - "media_episode_length" : "3600", - "media_segment_length" : "3600", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "long", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:audio:13598743", - "media_sub_set_id" : "EPISODE" - }, - "eventData" : "$ec997ecbd9874fae$258ca058508c83574cb22919f3635503768c1a1c0add2b04ca590b77d2b9457e657aacd76afc90a50061e17ac60c3e1b67605cb41d8dbc3fcfb4eda6081a00dad23404d36ad5c0847b7094c4dbdf712baff4d617d6908953f61b3fd1052da55e58b1762651dc68ab1edc98f3895efd8b3de1ef01a92d0dee673380413069693faf88e3d89798afe7702404805004026b09179557a9338275a432ee9565179bf6551b3364914984da3c5bc88caa91d9b58e6cd3d2373d8ff4526bfc407467ed82cd9235d6718249b36b1d16bf2711443d330799f7a4daa3a739e3b314cf29569e2f064917e6b7adfb7a7ef08b0a415a0dc51b17bcf404d74b84c0a8eedc6c0d1b8c119db4f09d0a9e5e819aafddb6e721cad6e712beb3499f590d293a523af2d5c1529292ca9c2be172e43c321c199e55604299c18745e27c6e6a8f8ab728d0e7484e4932e10e9e1d5a4698bc146261d54a537619f59cd4677770cbd389f454732c002e3598846ce835295afe13ad03e2", - "fullLengthMarkIn" : 0, - "fullLengthMarkOut" : 0, - "resourceList" : [ { - "url" : "https://rts-aod-dd.akamaized.net/ww/13598743/d96d95c9-23cf-38b8-92af-42e89cd85afa.mp3", - "quality" : "HQ", - "protocol" : "HTTPS", - "encoding" : "MP3", - "mimeType" : "audio/mpeg", - "presentation" : "DEFAULT", - "streaming" : "PROGRESSIVE", - "dvr" : false, - "live" : false, - "mediaContainer" : "NONE", - "audioCodec" : "MP3", - "videoCodec" : "NONE", - "tokenType" : "NONE", - "analyticsMetadata" : { - "media_streaming_quality" : "HQ", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598743/d96d95c9-23cf-38b8-92af-42e89cd85afa.mp3" - } - } ] - }, { - "id" : "13622547", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13622547", - "title" : "Forum (vidéo) - Présenté par Tania Sazpinar et Esther Coquoz", - "imageUrl" : "https://www.rts.ch/2022/12/12/23/08/13622546.image/16x9", - "imageTitle" : "Forum (vidéo) - Présenté par Tania Sazpinar et Esther Coquoz [RTS]", - "type" : "CLIP", - "date" : "2022-12-12T18:00:00+01:00", - "duration" : 3600840, - "validFrom" : "2022-12-12T19:00:00+01:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:audio:13598743", - "position" : 1, - "noEmbed" : false, - "analyticsData" : { - "ns_st_ep" : "Forum (vidéo) - Présenté par Tania Sazpinar et Esther Coquoz", - "ns_st_ty" : "Video", - "ns_st_ci" : "13622547", - "ns_st_el" : "3600840", - "ns_st_cl" : "3600840", - "ns_st_sl" : "3600840", - "srg_mgeobl" : "false", - "ns_st_tp" : "1", - "ns_st_cn" : "1", - "ns_st_ct" : "vc12", - "ns_st_pn" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc" - }, - "analyticsMetadata" : { - "media_segment" : "Forum (vidéo) - Présenté par Tania Sazpinar et Esther Coquoz", - "media_type" : "Video", - "media_segment_id" : "13622547", - "media_episode_length" : "3601", - "media_segment_length" : "3601", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "long", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:13622547", - "media_sub_set_id" : "CLIP" - }, - "eventData" : "$f992085ed4fa2c25$c6998ff87df1d9ea9b7cb76e4b23da9ccb9d0748c5f7c36f018bc86158a004d801a0736b824af35c828a67092a5607683d005f30cb1d4e9a4d6a98640119a7376fe4018ca8b0a34f7d08970687edc20b19931236b201953449b0c2fed80dc7807f436c2e96b8636a941cf7ad31e297d87411254cd1c9c1e876dc1269b9ff899bfed9c36153b83a4d8aaf09953bdc0e89f808894cdd69d0a83089a730d6d73bfa6561ea171738e61b27961494af846a485a55e415fae74b124d3f81c0276fa75c9f37af3d9c0f5711fa062e15e232bdce3eddebc1d02eb77f236dd10c12aac38ebceb53beaba79f683f2295fa3abf3c1a014f075500913f9c14ffd6c48e4c01dbf9ddb044db5d35fa0aebf1ff3bb177b790a979301a88013d8d3aa4fd3b0350f27a9f443dc4a89b67eb55965ad027f8a0958055b4e6e82a9a15ea22192d9273cf0d6adf93159f41e566749d6a485971fd3229d78144f708ff746967c267ac2c65999de99e444684cdeedbe0f4ae8beece", - "resourceList" : [ { - "url" : "https://rts-vod-amd.akamaized.net/ww/13622547/ba5e0176-bace-398f-9d89-ed0a3f18dab4/master.m3u8", - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - } ], - "subtitleInformationList" : [ { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "HLS", - "type" : "SDH" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-vod-amd.akamaized.net/ww/13622547/ba5e0176-bace-398f-9d89-ed0a3f18dab4/master.m3u8" - } - } ], - "aspectRatio" : "16:9", - "spriteSheet" : { - "urn" : "urn:rts:video:13622547", - "rows" : 26, - "columns" : 20, - "thumbnailHeight" : 84, - "thumbnailWidth" : 150, - "interval" : 7000, - "url" : "https://il.srgssr.ch/spritesheet/urn/rts/video/13622547/sprite-13622547.jpeg" - } - }, { - "id" : "13598744", - "mediaType" : "AUDIO", - "vendor" : "RTS", - "urn" : "urn:rts:audio:13598744", - "title" : "Les hautes instances européennes expriment leur inquiétude après les accusations de corruption", - "imageUrl" : "https://www.rts.ch/2022/10/18/17/58/13474798.image/16x9", - "imageTitle" : "Ursula von der Leyen au Parlement européen à Strasbourg, 18.10.2022. [Jean-Francois Badias - AP/Keystone]", - "imageCopyright" : "Jean-Francois Badias - AP/Keystone", - "type" : "CLIP", - "date" : "2022-12-12T18:02:00+01:00", - "duration" : 237000, - "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598744/23c3b7cd-a474-3fa6-9961-9019cf0a94e8.mp3", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:audio:13598743", - "position" : 2, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Les hautes instances européennes expriment leur inquiétude après les accusations de corruption", - "media_type" : "Audio", - "media_segment_id" : "13598744", - "media_episode_length" : "237", - "media_segment_length" : "237", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:audio:13598744", - "media_sub_set_id" : "CLIP" - }, - "eventData" : "$ddaa9deb763583f4$71f264168ee2b6b5831574deab90274752575e6b3e61cb945aca3ecdc7ccbfa7d48f87832df12d11e604d7bceeb871dab04acb165b95ea842780df4c7568b481603d3113ff7cdfe19680137b9c06d932c5a4b231bb81f7084de9e10f2a752289ce267e170ee986c43a5cc3841d88a5e6049e4f3d86e209cc9b6c1bdc262b7bd0a3b1db4abd997a9c7bad3c8ae64bd89b7d6d5683b00c4a5760118d8358d85fbd0a89e6a528ab345690c378fec7e87bd9c48408f407663e3f8d19fa80c948c3b4f93f58dab1a6e03b21505c6bca8599acb6ed1ee77a9a83fce9311edb23d47c4b3805a8bf7442f804e1adc3e115772228a7297f7c50406503f4a23c1f0a27f3c1d9093f6773843e37ffd58210088c9c2888af8f2180a4d54ad4d1ab254b91daee6cda26cfc329ee3b5fbc965df3e7da45ca1702e966b6951897fd0aa4c0618feb71143238bebe84812ecb0926a99ad00a27bf71793ff3d5b6537f53a2aab99ac4f02f64710b645c3306dfe53948ec4539", - "resourceList" : [ { - "url" : "https://rts-aod-dd.akamaized.net/ww/13598744/23c3b7cd-a474-3fa6-9961-9019cf0a94e8.mp3", - "quality" : "HQ", - "protocol" : "HTTPS", - "encoding" : "MP3", - "mimeType" : "audio/mpeg", - "presentation" : "DEFAULT", - "streaming" : "PROGRESSIVE", - "dvr" : false, - "live" : false, - "mediaContainer" : "NONE", - "audioCodec" : "MP3", - "videoCodec" : "NONE", - "tokenType" : "NONE", - "analyticsMetadata" : { - "media_streaming_quality" : "HQ", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598744/23c3b7cd-a474-3fa6-9961-9019cf0a94e8.mp3" - } - } ] - }, { - "id" : "13598745", - "mediaType" : "AUDIO", - "vendor" : "RTS", - "urn" : "urn:rts:audio:13598745", - "title" : "Yves Bertoncini s’exprime sur les cas de corruptions présumés au Parlement européen", - "description" : "Interview de Yves Bertoncini, enseignant en affaires européennes à l'école de commerce de Paris, et consultant en affaires européennes.", - "imageUrl" : "https://www.rts.ch/2020/07/19/16/43/11478416.image/16x9", - "imageTitle" : "Yves Bertoncini, président du Mouvement européen en France. [eesc.europa.eu]", - "imageCopyright" : "eesc.europa.eu", - "type" : "CLIP", - "date" : "2022-12-12T18:03:00+01:00", - "duration" : 336000, - "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598745/2da36319-1f31-36af-9f09-e9d86f157e40.mp3", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:audio:13598743", - "position" : 3, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Yves Bertoncini s’exprime sur les cas de corruptions présumés au Parlement européen", - "media_type" : "Audio", - "media_segment_id" : "13598745", - "media_episode_length" : "336", - "media_segment_length" : "336", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:audio:13598745", - "media_sub_set_id" : "CLIP" - }, - "eventData" : "$6affd6172a1fb2b5$298147a56fd0d5dfd2ba9795c9a5c69c8ba397d86fdde5696924cb66475b623869a91bd789ded60b18d93bf43d5f241bfae6d903d3bd3a547c45fcebc0b1a672c3f6088457ad57ed4b312a965c190f9cabe26f33b0bf4e66e7ce2d775adb39bbe0213138ec69e0a35e374f3295700742dd4a4f394811b916dd8825ed91aaa740058954798c880696c944cc25b81e144a9888e68c8d31191cb739b6949079d7c70ac153f2e81cd36782ad53ec72c94c5f277fa69ae077ad79739e0e3bf43207939bf1b960f0d1402598eaedd36e0542cd4c7d370104b08c0305f21541dda6a18c11d8133f2f64026010d5b1ba681dc9fbc07f0b1b9c173459df97488ebe374dc758b9a4828b12456c892259428fd9d04174d7d2ea40a45fb1fb6d85135347b118a7ccfaa99a2262fdc3c7d828562154bf8ff163ac9e9bc9555123d8f1af656bdd98c3cf4aa0e3f2dba4dafd0dc72a285139502b271fbdfd7ea5cf22abfd3265119858ccdb2473b4f49226f02ac93b31e7", - "resourceList" : [ { - "url" : "https://rts-aod-dd.akamaized.net/ww/13598745/2da36319-1f31-36af-9f09-e9d86f157e40.mp3", - "quality" : "HQ", - "protocol" : "HTTPS", - "encoding" : "MP3", - "mimeType" : "audio/mpeg", - "presentation" : "DEFAULT", - "streaming" : "PROGRESSIVE", - "dvr" : false, - "live" : false, - "mediaContainer" : "NONE", - "audioCodec" : "MP3", - "videoCodec" : "NONE", - "tokenType" : "NONE", - "analyticsMetadata" : { - "media_streaming_quality" : "HQ", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598745/2da36319-1f31-36af-9f09-e9d86f157e40.mp3" - } - } ] - }, { - "id" : "13598746", - "mediaType" : "AUDIO", - "vendor" : "RTS", - "urn" : "urn:rts:audio:13598746", - "title" : "Première journée d’audience à Bellinzone pour le terroriste du kebab de Morges", - "imageUrl" : "https://www.rts.ch/2022/12/12/20/04/13622249.image/16x9", - "imageTitle" : "Première journée d’audience à Bellinzone pour le terroriste de Morges. [Linda Graedel - Keystone]", - "imageCopyright" : "Linda Graedel - Keystone", - "type" : "CLIP", - "date" : "2022-12-12T18:04:00+01:00", - "duration" : 161000, - "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598746/ac7ee7e2-5b20-359f-9655-f9e814bee447.mp3", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:audio:13598743", - "position" : 4, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Première journée d’audience à Bellinzone pour le terroriste du kebab de Morges", - "media_type" : "Audio", - "media_segment_id" : "13598746", - "media_episode_length" : "161", - "media_segment_length" : "161", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:audio:13598746", - "media_sub_set_id" : "CLIP" - }, - "eventData" : "$cf0f417e58a57656$75222e3db44c918c724e36471224ff8a7439cde9f3b249451df03ce7df4cb7a89bf6ac20089a37a66981de5c753a64a8fb34ee58db2062215983622a2184a953e6ea811782b27b8ce2f4c49cd73244812108447dda38bc9185cf35bc06895b04015ced91c13bf1e1d9f284e4878d326251a5d5beec438aadc91d460baf68eb333d892e36ffa49bb1cba06fab9d45590bfb5860467b482832760ed71a65c39175c2c24fb3602bc67afb3c9817aad42532caba64b693f5c445de5c8d712ed2a5b81863d00e48c7a50d12845aac4445c41113813db784ed2a6235c4047ae6a4ffd481917e73b1503892c84b63bb813dce3ef6b234ea20c4bbaafe957bbaf284fac50a148911e79093154475c8cbea03b9e74d848d900dac16ade67042c53e8319b7c8e549969f8bf1e306eb2b7cabb0d1df7568f34eafcb4e027fea412b11f99a4515562691cbaab1b9918f205c4a5a92f0265cd5fa6826af40ac88efe4a8b8b56372d9f2f35fe2c20c1c2f4ae844c85b69", - "resourceList" : [ { - "url" : "https://rts-aod-dd.akamaized.net/ww/13598746/ac7ee7e2-5b20-359f-9655-f9e814bee447.mp3", - "quality" : "HQ", - "protocol" : "HTTPS", - "encoding" : "MP3", - "mimeType" : "audio/mpeg", - "presentation" : "DEFAULT", - "streaming" : "PROGRESSIVE", - "dvr" : false, - "live" : false, - "mediaContainer" : "NONE", - "audioCodec" : "MP3", - "videoCodec" : "NONE", - "tokenType" : "NONE", - "analyticsMetadata" : { - "media_streaming_quality" : "HQ", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598746/ac7ee7e2-5b20-359f-9655-f9e814bee447.mp3" - } - } ] - }, { - "id" : "13598747", - "mediaType" : "AUDIO", - "vendor" : "RTS", - "urn" : "urn:rts:audio:13598747", - "title" : "Le nombre de jeunes femmes hospitalisées pour des troubles psychiques a largement augmenté: interview de Anne Edan", - "description" : "Interview de Anne Edan, responsable de Malatavie, Unité de crise-Partenariat Public privé HUG-Children Action", - "imageUrl" : "https://www.rts.ch/2022/12/12/18/43/13622215.image/16x9", - "imageTitle" : "La doctoresse Anne Edan est médecin responsable de Malatavie (HUG). [RTS]", - "imageCopyright" : "RTS", - "type" : "CLIP", - "date" : "2022-12-12T18:05:00+01:00", - "duration" : 306000, - "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598747/603ff318-7020-3b44-8ea8-efc1f500758d.mp3", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:audio:13598743", - "position" : 5, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Le nombre de jeunes femmes hospitalisées pour des troubles psychiques a largement augmenté: interview de Anne Edan", - "media_type" : "Audio", - "media_segment_id" : "13598747", - "media_episode_length" : "306", - "media_segment_length" : "306", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:audio:13598747", - "media_sub_set_id" : "CLIP" - }, - "eventData" : "$ebf7057806b66ff5$20cff69bc7509ca8a40119107dd01414fcac55cf2a2d4aa9bb7e13a8b50745fe7f1c7b6690ff41722a2b227ce4fea003cb98af73d0cda6a5216a2857e56b3df8542ca270e551ccbab0439e0142230090cc385cf6e4eb28bcf25cac1bba91696a9d85c9bceac0919559b07cacc6b584fcb7bc4f1b2b66e64a621d1c43b996a41ce5f89d125d42d251bd808d5f4ee2556c2743944e7caaa5629971a24aa99c6961c15756f77043c5bad622c87083b65decf3a97a80695a13a107fdef6d23d58c2ffa39e53344352734a8c627cad9f72796eb3207d06880bad20c616938c6ae37a9d274c0354776c078e7fc3cd01ee6af765282fbc2d02fafe6b6e6f96ed73f921465dbe73297e63cf496b40e1599ed37f1dab991f1aa9b39a64e68942523eb60f83aed372115739566f13e426d959f9414110ef3a632e4d333bbb3c667c2d7a9661a682d6099dbb53743633b47581d8ec96077ced60f6792d520da15cf1ce0d1c3129dcbafd0980faa1449bfbfa48050e9", - "resourceList" : [ { - "url" : "https://rts-aod-dd.akamaized.net/ww/13598747/603ff318-7020-3b44-8ea8-efc1f500758d.mp3", - "quality" : "HQ", - "protocol" : "HTTPS", - "encoding" : "MP3", - "mimeType" : "audio/mpeg", - "presentation" : "DEFAULT", - "streaming" : "PROGRESSIVE", - "dvr" : false, - "live" : false, - "mediaContainer" : "NONE", - "audioCodec" : "MP3", - "videoCodec" : "NONE", - "tokenType" : "NONE", - "analyticsMetadata" : { - "media_streaming_quality" : "HQ", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598747/603ff318-7020-3b44-8ea8-efc1f500758d.mp3" - } - } ] - }, { - "id" : "13598748", - "mediaType" : "AUDIO", - "vendor" : "RTS", - "urn" : "urn:rts:audio:13598748", - "title" : "Réforme du 2e pilier: la question des compensations financières pour la génération transitoire divise les Chambres fédérales", - "imageUrl" : "https://www.rts.ch/2022/12/05/17/48/13601356.image/16x9", - "imageTitle" : "La transformation numérique de l'administration divise les Chambres fédérales. [RTS]", - "imageCopyright" : "RTS", - "type" : "CLIP", - "date" : "2022-12-12T18:06:00+01:00", - "duration" : 151000, - "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598748/c8364401-7593-3c61-aefb-cd206fcb4101.mp3", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:audio:13598743", - "position" : 6, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Réforme du 2e pilier: la question des compensations financières pour la génération transitoire divise les Chambres fédérales", - "media_type" : "Audio", - "media_segment_id" : "13598748", - "media_episode_length" : "151", - "media_segment_length" : "151", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:audio:13598748", - "media_sub_set_id" : "CLIP" - }, - "eventData" : "$a4182629afa7c2d7$16cd8d9d1c92ece2ffd7db8649edfadfea3904afc9ba1d4acf8ef3bd38f50d80ea9b8b13038e6521e9f2d6949d97e226e9d9beae7c7e2289ccf429b551bc38ebaf7aab6821edc5396ee0547a188641d561cdf82e9c3700ec70be619740ca1493281ead1465c0c49996ff707f3d38bc97a88f14c19aa8e5200153b5e4e898261cd18e1a7e3e61094a134241756a6f8558463477fb51e09598a50bcb8c8b3fbd841170bff51c516ad6ace9d97989a2b78c3705a60ff211606781ecc296d949ace022ddac0e8736f2f499f827cb58469f723a0b708242f33cd5d9bef8ed4d0ae8ddf6f446de47eaf128949ca72970cb7a036996df5d2f43f952f44325c507ae2b9e8e56d468c4361d16e317cc338dd713dcdbae946f9288ec141bdd44f9eb30b325fd71229e850330754a92d0f88309117990bc9d33e4183c82ea103a1cd478b9111aef3db2fad3c7ef50cc4c4cad18a5395d8e547a36dbb70997d1588eac3bfed8c64fce49128575fd2331060377af7625", - "resourceList" : [ { - "url" : "https://rts-aod-dd.akamaized.net/ww/13598748/c8364401-7593-3c61-aefb-cd206fcb4101.mp3", - "quality" : "HQ", - "protocol" : "HTTPS", - "encoding" : "MP3", - "mimeType" : "audio/mpeg", - "presentation" : "DEFAULT", - "streaming" : "PROGRESSIVE", - "dvr" : false, - "live" : false, - "mediaContainer" : "NONE", - "audioCodec" : "MP3", - "videoCodec" : "NONE", - "tokenType" : "NONE", - "analyticsMetadata" : { - "media_streaming_quality" : "HQ", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598748/c8364401-7593-3c61-aefb-cd206fcb4101.mp3" - } - } ] - }, { - "id" : "13598749", - "mediaType" : "AUDIO", - "vendor" : "RTS", - "urn" : "urn:rts:audio:13598749", - "title" : "Retour sur l’élection d’Éric Ciotti à la tête du parti français Les Républicains", - "imageUrl" : "https://www.rts.ch/2022/12/12/17/37/13622160.image/16x9", - "imageTitle" : "Éric Ciotti est élu à la tête du parti français Les Républicains le 11 décembre 2022. [Christophe Petit Tesson - EPA/Keystone]", - "imageCopyright" : "Christophe Petit Tesson - EPA/Keystone", - "type" : "CLIP", - "date" : "2022-12-12T18:07:00+01:00", - "duration" : 166000, - "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598749/8c057540-b74b-35f8-9790-64821c110eb9.mp3", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:audio:13598743", - "position" : 7, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Retour sur l’élection d’Éric Ciotti à la tête du parti français Les Républicains", - "media_type" : "Audio", - "media_segment_id" : "13598749", - "media_episode_length" : "166", - "media_segment_length" : "166", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:audio:13598749", - "media_sub_set_id" : "CLIP" - }, - "eventData" : "$d8edc280c72c858e$645dcf60f54f9a71c47bb237ec2674bcc3713eabb9a5f1f3008fed53e99ccdadeded7697f0ba048e8dccc1367486f718c29c59448c392eeedc4267eb44eb799b5845d710f92771a5e91d59cc2a5dce42b5a91fa430eaaaeecd08e332b8ce5cbfe036afdc99aab2c06c002af485d354fa1a4636126a069cd11cab33e12baad3d762462eaa03a543dda98c7aa52e8d4545378e49dddde527d463f4b3a8ff44ba1c9a399aaa97101f8c341dbda37164c9d43df821681c19abd73e2bc83fcf404dd436d2f8b64503f548ceb42672e58566696da69314cc56462b0818740b47f34079bcb604e12d46dd27e7b4c13b9b1519e9fa228b61973d1604ee3787fe2660fd51ba1c30f79be516edf7355751f1b096e38e425079f998aaad023de2dd8a9a754e4f445752c79dac4545400958de48acd769a2eb64e5579a0cc5bba86752c05f1abcab04d254fe1bbcf1488b66bff49634784ed1ae381c1604e2d612917f5fe58213221fd67378b5dd92d38da9b02c8ac7", - "resourceList" : [ { - "url" : "https://rts-aod-dd.akamaized.net/ww/13598749/8c057540-b74b-35f8-9790-64821c110eb9.mp3", - "quality" : "HQ", - "protocol" : "HTTPS", - "encoding" : "MP3", - "mimeType" : "audio/mpeg", - "presentation" : "DEFAULT", - "streaming" : "PROGRESSIVE", - "dvr" : false, - "live" : false, - "mediaContainer" : "NONE", - "audioCodec" : "MP3", - "videoCodec" : "NONE", - "tokenType" : "NONE", - "analyticsMetadata" : { - "media_streaming_quality" : "HQ", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598749/8c057540-b74b-35f8-9790-64821c110eb9.mp3" - } - } ] - }, { - "id" : "13598750", - "mediaType" : "AUDIO", - "vendor" : "RTS", - "urn" : "urn:rts:audio:13598750", - "title" : "Vincent Baudriller s’exprime sur la nomination de la Française Séverine Chavrier à la tête de la Comédie à Genève", - "description" : "Interview de Vincent Baudriller, directeur du Théâtre de Vidy.", - "imageUrl" : "https://www.rts.ch/2020/04/14/16/05/11246790.image/16x9", - "imageTitle" : "Vincent Baudriller. [Jean-Christophe Bott - Keystone]", - "imageCopyright" : "Jean-Christophe Bott - Keystone", - "type" : "CLIP", - "date" : "2022-12-12T18:08:00+01:00", - "duration" : 402000, - "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598750/4f917b97-c0c7-3775-9564-5fc8cb11a5b8.mp3", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:audio:13598743", - "position" : 8, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Vincent Baudriller s’exprime sur la nomination de la Française Séverine Chavrier à la tête de la Comédie à Genève", - "media_type" : "Audio", - "media_segment_id" : "13598750", - "media_episode_length" : "402", - "media_segment_length" : "402", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:audio:13598750", - "media_sub_set_id" : "CLIP" - }, - "eventData" : "$eae27a0140800d45$b6ba151f5298efd03d42142e78f964c7e451e7511b1f3ff5fff3dd6b4cda7136fc5a1ad7ac52128003d431a94c222d30740781e2bcdd2ced46f84dc828c967ec5fbc3eeb32df9678c036ecdffd97b0ccf581a492e0819c575dedb053c44c45ae824e6035416f975a7ade48492d7c98c469d646f859e27de83c80020de052f5b136539c97b0772adf35f120ff09c3feccb5ac20892ac5002d3c10eb28e6dbfa7f9f598089fa93c004a4939b3d654c71459542a4c8bdf11354c4a762013ad47d3f3bba9dbfece5677d500a2031206c93df103bfb66663499a1f73f38ae449f0871beb73ef585612d016bd881fdc8bb35e0429499b2049dd1ad0880a62239b46f197ade98d6848e8aa2f17797f28b533f60554fa7b062d5b7b47d069bba7fee70687327f27e81d2aed628c09c925e2c5c6e86ccd7cc30158ca20a7a94d12cb0c98e9722d7f350bd8bd18a3f095842bb4d7733be89733d8710608a9bf0f87cdae636f5cfbb33f0d1386a4994938dd5f7d526", - "resourceList" : [ { - "url" : "https://rts-aod-dd.akamaized.net/ww/13598750/4f917b97-c0c7-3775-9564-5fc8cb11a5b8.mp3", - "quality" : "HQ", - "protocol" : "HTTPS", - "encoding" : "MP3", - "mimeType" : "audio/mpeg", - "presentation" : "DEFAULT", - "streaming" : "PROGRESSIVE", - "dvr" : false, - "live" : false, - "mediaContainer" : "NONE", - "audioCodec" : "MP3", - "videoCodec" : "NONE", - "tokenType" : "NONE", - "analyticsMetadata" : { - "media_streaming_quality" : "HQ", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598750/4f917b97-c0c7-3775-9564-5fc8cb11a5b8.mp3" - } - } ] - }, { - "id" : "13598751", - "mediaType" : "AUDIO", - "vendor" : "RTS", - "urn" : "urn:rts:audio:13598751", - "title" : "Forum des idées - Le Campus pour la démocratie veut rendre la démocratie plus accessible aux citoyens suisses", - "description" : "Interview de Catherine Carron, représentante romande du Campus pour la démocratie, plate-forme nationale de l'éducation à la citoyenneté et à la participation politique.", - "imageUrl" : "https://www.rts.ch/2023/05/31/13/06/13531758.image/16x9", - "imageTitle" : "Le Palais fédéral à Berne. [Peter Klaunzer - Keystone]", - "imageCopyright" : "Peter Klaunzer - Keystone", - "type" : "CLIP", - "date" : "2022-12-12T18:09:00+01:00", - "duration" : 379000, - "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598751/c2311297-1727-36ec-87d8-dcf686e443a0.mp3", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:audio:13598743", - "position" : 9, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Forum des idées - Le Campus pour la démocratie veut rendre la démocratie plus accessible aux citoyens suisses", - "media_type" : "Audio", - "media_segment_id" : "13598751", - "media_episode_length" : "379", - "media_segment_length" : "379", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:audio:13598751", - "media_sub_set_id" : "CLIP" - }, - "eventData" : "$cfff2b2de5e31883$8435e642492008527864da938d268db4ff9ad63541b83d0192ac5da8fba432287d015b7f7ee0a9cd15e07c53a739c75e1a75edb0ca8b960f08c0f28c67fce3decbb41279f1966a042f1e211d31087178834bcff57323e5f167b2d401f85116c07881ce0946db23d45d0210ee3fa2ce47d758f1ffd33ace7764de97c0026530c94629151151a21478d9811a1fc40a1480624caafb03b4ffc7f5d60cebae02f1bfde44250293515a95466c093c684e8cae9e17abf7e23d1689dab081d2f2789dc457208dda279b0a0c91455a9f49ffdef96f6a13bd34c41181177737de10ac66672eb546990ec2e5e0a5f59471838e544628660638665621802751beb522788dc0abe15fc010ea722c8671f1bf0fa97ad13fc1bc01a5bbf6b355cce414a04c65f5cf03cd78757e7f43cdda95046b734dab8f169a4e3338ef1e3634064cd20c065a3037ce8a27c54f9cafe95a44608f0d3e014df1a0b5be43ccdea3e5817a85b15759f7c12eece51ade1619eac3c840f62b", - "resourceList" : [ { - "url" : "https://rts-aod-dd.akamaized.net/ww/13598751/c2311297-1727-36ec-87d8-dcf686e443a0.mp3", - "quality" : "HQ", - "protocol" : "HTTPS", - "encoding" : "MP3", - "mimeType" : "audio/mpeg", - "presentation" : "DEFAULT", - "streaming" : "PROGRESSIVE", - "dvr" : false, - "live" : false, - "mediaContainer" : "NONE", - "audioCodec" : "MP3", - "videoCodec" : "NONE", - "tokenType" : "NONE", - "analyticsMetadata" : { - "media_streaming_quality" : "HQ", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598751/c2311297-1727-36ec-87d8-dcf686e443a0.mp3" - } - } ] - }, { - "id" : "13598756", - "mediaType" : "AUDIO", - "vendor" : "RTS", - "urn" : "urn:rts:audio:13598756", - "title" : "Le grand débat - Faut-il se méfier de TikTok?", - "description" : "Débat entre Laurence Allard, maîtresse de conférences en Sciences de la Communication à la Sorbonne-Nouvelle et sociologue des usages numériques, Charles Thibout, politiste, chercheur à l'IRIS et à l'Université Paris 1, spécialiste de la géopolitique des entreprises de nouvelles technologies, et Yaniv Benhamou, professeur en droit du numérique à la Faculté de droit et au Digital Law Center de l’Université de Genève et avocat en droit des nouvelles technologies.", - "imageUrl" : "https://www.rts.ch/2022/12/12/19/02/13622244.image/16x9", - "imageTitle" : "Débat entre Laurence Allard, maîtresse de conférences en Sciences de la Communication à la Sorbonne-Nouvelle et sociologue des usages numériques, Charles Thibout, politiste, chercheur à l'IRIS et à l'Université Paris 1, spécialiste de la géopolitique des entreprises de nouvelles technologies, et Yaniv Benhamou, professeur en droit du numérique à la Faculté de droit et au Digital Law Center de l’Université de Genève et avocat en droit des nouvelles technologies. [RTS - RTS]", - "imageCopyright" : "RTS - RTS", - "type" : "CLIP", - "date" : "2022-12-12T18:14:00+01:00", - "duration" : 1286000, - "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598756/e6723f95-82a7-3a9f-97fd-c2c6a0421d28.mp3", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:audio:13598743", - "position" : 10, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Le grand débat - Faut-il se méfier de TikTok?", - "media_type" : "Audio", - "media_segment_id" : "13598756", - "media_episode_length" : "1286", - "media_segment_length" : "1286", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "long", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:audio:13598756", - "media_sub_set_id" : "CLIP" - }, - "eventData" : "$05d56fc176b428a7$c4d4c80e459350910101e4a51f2103e4d1decd01ac68bf302012719250e82ebba3730b1e526100865258028134005197e2ca53e885fc5c1596af2b03bfa87ab00717fd1e776a2117ff6de14c74135d21a7e75cfd5f78f34fc4bc05c5f6d6e9d4131f83701dba8d5bb403b1f70c0d6844c9ba1143024dfd8dfb51632ca593c515a42885e33e7d5c5a52530d8e728d7af5950bdf2b53be9513a0219ae62ddfb028dd10b9c99a8852d60fad788d02105300a1db890c61f57fa2756de8b1b7d66e20214996f035c7d2777e10f3a565e4bd9baa26431e49f2cf78c359dd46cb20d166fb60ad10a8e865065e4dc6881028931fb21ddc2aa708cfd2c1addf35e1fd54e733da0813c27b1be77aaeed5b08932c3c08a9feddd92cc294f05734b0ea2cc96f6b69d2a4608f90ec41045c560580782548de98c42c39cd7dfb61d1878b927371afc194d99a266b39ff8b1ada6a2e0120076f53bf8ddc4ed33a530c234c63c8c005c76a06888964520e2cef23ef655e8e", - "resourceList" : [ { - "url" : "https://rts-aod-dd.akamaized.net/ww/13598756/e6723f95-82a7-3a9f-97fd-c2c6a0421d28.mp3", - "quality" : "HQ", - "protocol" : "HTTPS", - "encoding" : "MP3", - "mimeType" : "audio/mpeg", - "presentation" : "DEFAULT", - "streaming" : "PROGRESSIVE", - "dvr" : false, - "live" : false, - "mediaContainer" : "NONE", - "audioCodec" : "MP3", - "videoCodec" : "NONE", - "tokenType" : "NONE", - "analyticsMetadata" : { - "media_streaming_quality" : "HQ", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598756/e6723f95-82a7-3a9f-97fd-c2c6a0421d28.mp3" - } - } ] - } ], - "analyticsData" : { - "srg_pr_id" : "13598757", - "srg_plid" : "1784426", - "ns_st_pl" : "Forum", - "ns_st_pr" : "Forum du 12.12.2022", - "ns_st_dt" : "2022-12-12", - "ns_st_ddt" : "2022-12-12", - "ns_st_tdt" : "2022-12-12", - "ns_st_tm" : "18:00", - "ns_st_tep" : "*null", - "ns_st_li" : "0", - "ns_st_stc" : "0867", - "ns_st_st" : "RTS Online", - "ns_st_tpr" : "1784426", - "ns_st_en" : "*null", - "ns_st_ge" : "*null", - "ns_st_ia" : "*null", - "ns_st_ce" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc", - "srg_unit" : "RTS", - "srg_c1" : "full", - "srg_c2" : "la-1ere_programmes_forum", - "srg_c3" : "LA 1ÈRE", - "srg_aod_prid" : "13598757" - }, - "analyticsMetadata" : { - "media_episode_id" : "13598757", - "media_show_id" : "1784426", - "media_show" : "Forum", - "media_episode" : "Forum du 12.12.2022", - "media_is_livestream" : "false", - "media_full_length" : "full", - "media_enterprise_units" : "RTS", - "media_joker1" : "full", - "media_joker2" : "la-1ere_programmes_forum", - "media_joker3" : "LA 1ÈRE", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_thumbnail" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/16x9/scale/width/344", - "media_publication_date" : "2022-12-12", - "media_publication_time" : "18:00:00", - "media_publication_datetime" : "2022-12-12T18:00:00+01:00", - "media_tv_date" : "2022-12-12", - "media_tv_time" : "18:00:00", - "media_tv_datetime" : "2022-12-12T18:00:00+01:00", - "media_content_group" : "Forum,Programmes,La 1ère", - "media_channel_id" : "a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", - "media_channel_cs" : "0867", - "media_channel_name" : "La 1ère", - "media_since_publication_d" : "496", - "media_since_publication_h" : "11919" - } -} \ No newline at end of file diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_drm.dataset/Contents.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_drm.dataset/Contents.json deleted file mode 100644 index d5ec4402..00000000 --- a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_drm.dataset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "data" : [ - { - "filename" : "MediaComposition_drm.json", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_drm.dataset/MediaComposition_drm.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_drm.dataset/MediaComposition_drm.json deleted file mode 100644 index b261eecd..00000000 --- a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_drm.dataset/MediaComposition_drm.json +++ /dev/null @@ -1,315 +0,0 @@ - -{ - "chapterUrn" : "urn:rts:video:13548828", - "episode" : { - "id" : "13435837", - "title" : "Top Models", - "lead" : "8846", - "publishedDate" : "2022-11-18T11:44:09+01:00", - "imageUrl" : "https://www.rts.ch/2022/11/18/08/05/13548823.image/16x9", - "imageTitle" : "Top Models [RTS]" - }, - "show" : { - "id" : "532539", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:show:tv:532539", - "title" : "Top Models", - "lead" : "Drames, paillettes et glamour au coeur de Los Angeles.\n\nDu lundi au vendredi à 11h45 sur RTS Un. L'épisode du jour est disponible en \"preview\" sur Play RTS 24h avant la diffusion antenne, puis à revoir durant 30 jours.", - "description" : "Drames, paillettes et glamour au coeur de Los Angeles. Du lundi au vendredi à 11h45 sur RTS Un. L'épisode du jour est disponible en \"preview\" sur Play RTS 24h avant la diffusion antenne, puis à revoir durant 30 jours.", - "imageUrl" : "https://www.rts.ch/2022/04/26/11/09/11507387.image/16x9", - "imageTitle" : "Top Models. [RTS/Monty Brinton/CBS/Courtesy of Sony Pictures of Televisions]", - "bannerImageUrl" : "https://www.rts.ch/2022/04/26/11/09/11507387.image/3x1", - "posterImageUrl" : "https://www.rts.ch/2022/04/26/10/03/12155676.image/2x3", - "posterImageIsFallbackUrl" : false, - "homepageUrl" : "https://details.rts.ch/emissions/series", - "primaryChannelId" : "143932a79bb5a123a646b68b1d1188d7ae493e5b", - "primaryChannelUrn" : "urn:rts:channel:tv:143932a79bb5a123a646b68b1d1188d7ae493e5b", - "availableAudioLanguageList" : [ { - "locale" : "fr", - "language" : "Français" - }, { - "locale" : "en", - "language" : "English" - } ], - "availableVideoQualityList" : [ "SD" ], - "audioDescriptionAvailable" : false, - "subtitlesAvailable" : true, - "multiAudioLanguagesAvailable" : true, - "topicList" : [ { - "id" : "2386", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:2386", - "title" : "Top Models" - }, { - "id" : "2383", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:2383", - "title" : "Séries" - }, { - "id" : "2026", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:2026", - "title" : "Émissions" - } ], - "allowIndexing" : false - }, - "channel" : { - "id" : "143932a79bb5a123a646b68b1d1188d7ae493e5b", - "vendor" : "RTS", - "urn" : "urn:rts:channel:tv:143932a79bb5a123a646b68b1d1188d7ae493e5b", - "title" : "RTS 1", - "imageUrl" : "https://www.rts.ch/2022/04/26/11/09/11507387.image/16x9", - "imageUrlRaw" : "https://il.srgssr.ch/image-service/dynamic/8eebe5.svg", - "imageTitle" : "Top Models. [RTS/Monty Brinton/CBS/Courtesy of Sony Pictures of Televisions]", - "transmission" : "TV" - }, - "chapterList" : [ { - "id" : "13548828", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13548828", - "title" : "8846", - "imageUrl" : "https://www.rts.ch/2022/11/18/08/05/13548823.image/16x9", - "imageTitle" : "8846 [RTS]", - "type" : "EPISODE", - "date" : "2022-11-18T11:44:09+01:00", - "duration" : 1259200, - "validFrom" : "2022-11-17T12:11:44+01:00", - "validTo" : "2022-12-18T12:11:44+01:00", - "playableAbroad" : false, - "socialCountList" : [ { - "key" : "srgView", - "value" : 4779 - }, { - "key" : "srgLike", - "value" : 0 - }, { - "key" : "fbShare", - "value" : 1 - }, { - "key" : "twitterShare", - "value" : 1 - }, { - "key" : "googleShare", - "value" : 0 - }, { - "key" : "whatsAppShare", - "value" : 3 - } ], - "displayable" : true, - "position" : 0, - "noEmbed" : true, - "analyticsData" : { - "ns_st_ep" : "8846", - "ns_st_ty" : "Video", - "ns_st_ci" : "13548828", - "ns_st_el" : "1259200", - "ns_st_cl" : "1259200", - "ns_st_sl" : "1259200", - "srg_mgeobl" : "true", - "ns_st_tp" : "1", - "ns_st_cn" : "1", - "ns_st_ct" : "vc12", - "ns_st_pn" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc" - }, - "analyticsMetadata" : { - "media_segment" : "8846", - "media_type" : "Video", - "media_segment_id" : "13548828", - "media_episode_length" : "1259", - "media_segment_length" : "1259", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "long", - "media_is_geoblocked" : "true", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:13548828" - }, - "eventData" : "$fbb22ba2ca84dbae$9405a51d787a879d706b2354bbe054bb494dcba5fd4e5769985cb35df800bd3325fe61c69bb52ac217a0e5d5b5f9b679087b77d331f294e663042184283ec77ab2e349cef8cf00412fb057769142e4cfd03afa06e50c39a5d077821cb998a3bd728622d3c9f41f836b9268736db5f9ce11bc5091de7fe319ff4e8e4d1a801c0c9a1ab2f9e435038338d3be4cb1f956ec177901145ae0bc284d967a4c6240d1a70de139c4b06b7bc85b4cafe61ac36f659bca7e579e4be756e89bbab9fb583908afa5f327d5d77e0a6b931ff245fd91c703ff97a02224b62cc09d30d4ec3ef276f994212f5a009521c58b3f9b08b95197d71b3abb0df932802466511d069318acc4205cdc1e73a144ce24caa1becb10acd9afe26c7243e8c8f4ae5bccc28aa813c99a0a8759aa7078f62e4f3c4b8c1722690e48fa1bdc1dab50aaf79d39d9a9b3f035b6d6e2fbc746600fbdbf65a41896dabf07a929fb3182dded8644fa14683a998e9e1b3ce808431d8bceda1bbbd2bf", - "resourceList" : [ { - "url" : "https://rtsvod-euwe.akamaized.net:443/2a438722-f0c3-4653-8528-6d4dc11543e7/RTSVOD-11c75e3a-3091.ism/manifest(format=mpd-time-csf,encryption=cenc)", - "drmList" : [ { - "type" : "PLAYREADY", - "licenseUrl" : "https://srg.live.ott.irdeto.com/licenseServer/playready/v1/SRG/license?contentId=RTSVOD" - }, { - "type" : "WIDEVINE", - "licenseUrl" : "https://srg.live.ott.irdeto.com/licenseServer/widevine/v1/SRG/license?contentId=RTSVOD" - } ], - "quality" : "HD", - "protocol" : "DASH", - "encoding" : "H264", - "mimeType" : "application/dash+xml", - "presentation" : "DEFAULT", - "streaming" : "DASH", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "DASH" - }, { - "locale" : "en", - "language" : "English", - "source" : "DASH" - } ], - "subtitleInformationList" : [ { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "DASH", - "type" : "SDH" - }, { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "DASH", - "type" : "SDH" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rtsvod-euwe.akamaized.net:443/2a438722-f0c3-4653-8528-6d4dc11543e7/RTSVOD-11c75e3a-3091.ism/manifest(format=mpd-time-csf,encryption=cenc)" - } - }, { - "url" : "https://rtsvod-euwe.akamaized.net:443/2a438722-f0c3-4653-8528-6d4dc11543e7/RTSVOD-11c75e3a-3091.ism/manifest(format=m3u8-aapl,encryption=cbcs-aapl)", - "drmList" : [ { - "type" : "FAIRPLAY", - "licenseUrl" : "https://srg.live.ott.irdeto.com/licenseServer/streaming/v1/SRG/getckc?contentId=RTSVOD&keyId=6470ddd4-63ab-4d1c-972a-f91b10278eba", - "certificateUrl" : "https://srg.live.ott.irdeto.com/licenseServer/streaming/v1/SRG/getcertificate?applicationId=live" - } ], - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "MPEG2_TS", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - }, { - "locale" : "en", - "language" : "English", - "source" : "HLS" - } ], - "subtitleInformationList" : [ { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "HLS", - "type" : "SDH" - }, { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "HLS", - "type" : "SDH" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rtsvod-euwe.akamaized.net:443/2a438722-f0c3-4653-8528-6d4dc11543e7/RTSVOD-11c75e3a-3091.ism/manifest(format=m3u8-aapl,encryption=cbcs-aapl)" - } - } ], - "aspectRatio" : "16:9", - "timeIntervalList" : [ { - "type" : "CLOSING_CREDITS", - "markIn" : 1233720, - "markOut" : 1259200 - } ] - } ], - "topicList" : [ { - "id" : "2386", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:2386", - "title" : "Top Models" - }, { - "id" : "2383", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:2383", - "title" : "Séries" - }, { - "id" : "2026", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:2026", - "title" : "Émissions" - } ], - "analyticsData" : { - "srg_pr_id" : "13435837", - "srg_plid" : "532539", - "ns_st_pl" : "Top Models", - "ns_st_pr" : "Top Models du 18.11.2022", - "ns_st_dt" : "2022-11-18", - "ns_st_ddt" : "2022-11-17", - "ns_st_tdt" : "2022-11-18", - "ns_st_tm" : "11:44:09", - "ns_st_tep" : "500386540", - "ns_st_li" : "0", - "ns_st_stc" : "0867", - "ns_st_st" : "RTS Online", - "ns_st_tpr" : "532539", - "ns_st_en" : "*null", - "ns_st_ge" : "*null", - "ns_st_ia" : "*null", - "ns_st_ce" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc", - "srg_unit" : "RTS", - "srg_c1" : "full", - "srg_c2" : "video_plus7_series_top-models", - "srg_c3" : "RTS 1", - "srg_tv_id" : "500386540" - }, - "analyticsMetadata" : { - "media_episode_id" : "13435837", - "media_show_id" : "532539", - "media_show" : "Top Models", - "media_episode" : "Top Models du 18.11.2022", - "media_is_livestream" : "false", - "media_full_length" : "full", - "media_enterprise_units" : "RTS", - "media_joker1" : "full", - "media_joker2" : "video_plus7_series_top-models", - "media_joker3" : "RTS 1", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_tv_id" : "500386540", - "media_thumbnail" : "https://www.rts.ch/2022/11/18/08/05/13548823.image/16x9/scale/width/344", - "media_publication_date" : "2022-11-17", - "media_publication_time" : "12:11:44", - "media_publication_datetime" : "2022-11-17T12:11:44+01:00", - "media_tv_date" : "2022-11-18", - "media_tv_time" : "11:44:09", - "media_tv_datetime" : "2022-11-18T11:44:09+01:00", - "media_content_group" : "Top Models,Séries,Émissions", - "media_channel_id" : "143932a79bb5a123a646b68b1d1188d7ae493e5b", - "media_channel_cs" : "0867", - "media_channel_name" : "RTS 1", - "media_since_publication_d" : "0", - "media_since_publication_h" : "-3" - } -} \ No newline at end of file diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_live.dataset/Contents.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_live.dataset/Contents.json deleted file mode 100644 index 1f3ee82c..00000000 --- a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_live.dataset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "data" : [ - { - "filename" : "urn_rts_audio_3262320.json", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_live.dataset/urn_rts_audio_3262320.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_live.dataset/urn_rts_audio_3262320.json deleted file mode 100644 index 8971ba65..00000000 --- a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_live.dataset/urn_rts_audio_3262320.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "chapterUrn" : "urn:rts:audio:3262320", - "episode" : { - "id" : "3262332", - "title" : "La 1ère en direct", - "publishedDate" : "2011-07-11T14:18:47+02:00", - "imageUrl" : "https://www.rts.ch/2020/05/18/14/24/11333295.image/16x9", - "imageTitle" : "Chaîne La 1ère" - }, - "show" : { - "id" : "3262333", - "vendor" : "RTS", - "transmission" : "RADIO", - "urn" : "urn:rts:show:radio:3262333", - "title" : "La 1ère en direct", - "imageUrl" : "https://www.rts.ch/2020/05/18/14/24/11333295.image/16x9", - "imageTitle" : "Chaîne La 1ère", - "bannerImageUrl" : "https://www.rts.ch/2020/05/18/14/24/11333295.image/3x1", - "posterImageUrl" : "https://ws.srf.ch/asset/image/audio/e0322b37-5697-474d-93ac-19a4044a6a24/POSTER.jpg", - "posterImageIsFallbackUrl" : true, - "primaryChannelId" : "a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", - "primaryChannelUrn" : "urn:rts:channel:radio:a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", - "audioDescriptionAvailable" : false, - "subtitlesAvailable" : false, - "multiAudioLanguagesAvailable" : false, - "allowIndexing" : false - }, - "channel" : { - "id" : "a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", - "vendor" : "RTS", - "urn" : "urn:rts:channel:radio:a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", - "title" : "La 1ere", - "imageUrl" : "https://www.rts.ch/2020/05/18/14/24/11333295.image/16x9", - "imageTitle" : "Chaîne La 1ère", - "transmission" : "RADIO" - }, - "chapterList" : [ { - "id" : "3262320", - "mediaType" : "AUDIO", - "vendor" : "RTS", - "urn" : "urn:rts:audio:3262320", - "title" : "La 1ère en direct", - "imageUrl" : "https://www.rts.ch/2020/05/18/14/24/11333295.image/16x9", - "imageTitle" : "Chaîne La 1ère", - "type" : "LIVESTREAM", - "date" : "2011-07-11T14:18:47+02:00", - "duration" : 0, - "playableAbroad" : true, - "displayable" : true, - "position" : 0, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Livestream", - "media_type" : "Audio", - "media_segment_id" : "3262320", - "media_episode_length" : "0", - "media_segment_length" : "0", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "infinit.livestream", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:audio:3262320" - }, - "fullLengthMarkIn" : 0, - "fullLengthMarkOut" : 0, - "resourceList" : [ { - "url" : "https://lsaplus.swisstxt.ch/audio/la-1ere_96.stream/playlist.m3u8?", - "quality" : "HD", - "protocol" : "HLS-DVR", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : true, - "live" : true, - "mediaContainer" : "MPEG2_TS", - "audioCodec" : "AAC", - "videoCodec" : "NONE", - "tokenType" : "NONE", - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://lsaplus.swisstxt.ch/audio/la-1ere_96.stream/playlist.m3u8?" - }, - "streamOffset" : 55000 - } ] - } ], - "analyticsData" : { - "srg_pr_id" : "3262332", - "srg_plid" : "3262333", - "ns_st_pl" : "Livestream", - "ns_st_pr" : "La 1ère en direct", - "ns_st_dt" : "2011-07-11", - "ns_st_ddt" : "2011-07-11", - "ns_st_tdt" : "2011-07-11", - "ns_st_tm" : "14:18:47", - "ns_st_tep" : "*null", - "ns_st_li" : "1", - "ns_st_stc" : "0867", - "ns_st_st" : "La 1ere", - "ns_st_tpr" : "1423878", - "ns_st_en" : "*null", - "ns_st_ge" : "*null", - "ns_st_ia" : "*null", - "ns_st_ce" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc", - "srg_unit" : "RTS", - "srg_c1" : "live", - "srg_c2" : "rts.ch_audio_la-1ere", - "srg_c3" : "LA 1ÈRE", - "srg_aod_prid" : "3262332" - }, - "analyticsMetadata" : { - "media_episode_id" : "3262332", - "media_show_id" : "1423878", - "media_show" : "On en parle", - "media_episode" : "La 1ère en direct", - "media_is_livestream" : "true", - "media_full_length" : "full", - "media_enterprise_units" : "RTS", - "media_joker1" : "live", - "media_joker2" : "rts.ch_audio_la-1ere", - "media_joker3" : "LA 1ÈRE", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_thumbnail" : "https://www.rts.ch/2020/05/18/14/24/11333295.image/16x9/scale/width/344", - "media_publication_date" : "2011-07-11", - "media_publication_time" : "14:18:47", - "media_publication_datetime" : "2011-07-11T14:18:47+02:00", - "media_tv_date" : "2011-07-11", - "media_tv_time" : "14:18:47", - "media_tv_datetime" : "2011-07-11T14:18:47+02:00", - "media_content_group" : "La 1ère", - "media_channel_id" : "a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", - "media_channel_cs" : "0867", - "media_channel_name" : "La 1ere", - "media_since_publication_d" : "4074", - "media_since_publication_h" : "97795" - } -} \ No newline at end of file diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_missingAnalytics.dataset/Contents.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_missingAnalytics.dataset/Contents.json deleted file mode 100644 index 56c7e33d..00000000 --- a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_missingAnalytics.dataset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "data" : [ - { - "filename" : "urn_rts_video_13360574.json", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_missingAnalytics.dataset/urn_rts_video_13360574.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_missingAnalytics.dataset/urn_rts_video_13360574.json deleted file mode 100644 index 51f9103a..00000000 --- a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_missingAnalytics.dataset/urn_rts_video_13360574.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "chapterUrn" : "urn:rts:video:13360574", - "episode" : { - "id" : "13360565", - "title" : "Yadebat", - "publishedDate" : "2022-09-05T16:30:00+02:00", - "imageUrl" : "https://www.rts.ch/2022/09/05/18/20/13360573.image/16x9", - "imageTitle" : "On réunit des ex après leur rupture [RTS]" - }, - "show" : { - "id" : "10174267", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:show:tv:10174267", - "title" : "Yadebat", - "lead" : "Une série qui te donne la parole, pour laisser entendre ton avis sur les débats de société animée par Melissa.", - "description" : "Une série qui te donne la parole, pour laisser entendre ton avis sur les débats de société animée par Melissa.", - "imageUrl" : "https://www.rts.ch/2020/01/10/11/14/10520588.image/16x9", - "imageTitle" : "Yadebat - Tataki [RTS]", - "posterImageUrl" : "https://ws.srf.ch/asset/image/audio/e0322b37-5697-474d-93ac-19a4044a6a24/POSTER.jpg", - "posterImageIsFallbackUrl" : true, - "audioDescriptionAvailable" : false, - "subtitlesAvailable" : false, - "multiAudioLanguagesAvailable" : false, - "topicList" : [ { - "id" : "59952", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:59952", - "title" : "Yadebat" - }, { - "id" : "54537", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:54537", - "title" : "Tataki" - } ], - "allowIndexing" : false - }, - "chapterList" : [ { - "id" : "13360574", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13360574", - "title" : "On réunit des ex après leur rupture", - "description" : "Dans ce nouvel épisode de YADEBAT, Mélissa réunit 3 couples qui se sont séparés récemment. Elles les a questionné en face à face pour connaître leurs différents ressentis et réactions.", - "imageUrl" : "https://www.rts.ch/2022/09/05/18/20/13360573.image/16x9", - "imageTitle" : "On réunit des ex après leur rupture [RTS]", - "type" : "EPISODE", - "date" : "2022-09-05T16:30:00+02:00", - "duration" : 902360, - "validFrom" : "2022-09-05T16:30:00+02:00", - "validTo" : "2100-01-01T23:59:59+01:00", - "playableAbroad" : true, - "socialCountList" : [ { - "key" : "srgView", - "value" : 17 - }, { - "key" : "srgLike", - "value" : 0 - }, { - "key" : "fbShare", - "value" : 0 - }, { - "key" : "twitterShare", - "value" : 0 - }, { - "key" : "googleShare", - "value" : 0 - }, { - "key" : "whatsAppShare", - "value" : 0 - } ], - "displayable" : true, - "position" : 0, - "noEmbed" : false, - "eventData" : "$27549332a83ca6ac$64b181b51953d6ed48de11986513e2f93922eb3d4315e6d5ad8189e1fe38d933c011ba7ded29e3d757ba1e566e76d65d97c8f0cd0735cc47b1cb3e5cf091c89c8d6c18ff31e19e3d7509cbf826c0c156fd10b8908ebe481aaf7282de102e92342ffb36b52df58453b40d64883f720fb3eddd38b595ddf6961acc4bc33abb3f2c49b7d90b52a35239f0209caa3ebc532e6a95315bd382bc08f2b78af2ec23c3f7e7917de924cb7f85b8aedac2fdafd027fe3880e07f3a0ba05f43d0ce601a1d2c7b756012c8820e12eef32fb9c0e1f532cce31cf1be738a9d6c05555857700fc5e1f0e1bd9886f06c55f5e731a66daa09be035e5ef53a4da159a7d3943a67ebaa1ac1302ad3ff046739eb185d78737e1543e7788d4edd9858af0e6846460106e954e8f1176cf60876aad36646c11a3b3a824ab54433f99c4576accea86e2b853c", - "resourceList" : [ { - "url" : "https://rts-vod-amd.akamaized.net/ww/13360574/447e0958-42a8-3bdd-8365-95d54031e605/master.m3u8", - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-vod-amd.akamaized.net/ww/13360574/447e0958-42a8-3bdd-8365-95d54031e605/master.m3u8" - } - } ], - "aspectRatio" : "16:9", - "spriteSheet" : { - "urn" : "urn:rts:video:13360574", - "rows" : 23, - "columns" : 20, - "thumbnailHeight" : 84, - "thumbnailWidth" : 150, - "interval" : 2000, - "url" : "https://il.srgssr.ch/spritesheet/urn/rts/video/13360574/sprite-13360574.jpeg" - } - } ], - "topicList" : [ { - "id" : "59952", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:59952", - "title" : "Yadebat" - }, { - "id" : "54537", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:54537", - "title" : "Tataki" - } ], - "analyticsData" : { - "srg_pr_id" : "13360565", - "srg_plid" : "10174267", - "ns_st_pl" : "Yadebat", - "ns_st_pr" : "Yadebat du 05.09.2022", - "ns_st_dt" : "2022-09-05", - "ns_st_ddt" : "2022-09-05", - "ns_st_tdt" : "*null", - "ns_st_tm" : "*null", - "ns_st_tep" : "500418168", - "ns_st_li" : "0", - "ns_st_stc" : "0867", - "ns_st_st" : "RTS Online", - "ns_st_tpr" : "10174267", - "ns_st_en" : "*null", - "ns_st_ge" : "*null", - "ns_st_ia" : "*null", - "ns_st_ce" : "1", - "ns_st_cdm" : "eo", - "ns_st_cmt" : "ec", - "srg_unit" : "RTS", - "srg_c1" : "full", - "srg_c2" : "video_tataki_yadebat", - "srg_c3" : "RTS.ch", - "srg_tv_id" : "500418168" - }, - "analyticsMetadata" : { - "media_episode_id" : "13360565", - "media_show_id" : "10174267", - "media_show" : "Yadebat", - "media_episode" : "Yadebat du 05.09.2022", - "media_is_livestream" : "false", - "media_full_length" : "full", - "media_enterprise_units" : "RTS", - "media_joker1" : "full", - "media_joker2" : "video_tataki_yadebat", - "media_joker3" : "RTS.ch", - "media_is_web_only" : "true", - "media_production_source" : "produced.for.web", - "media_tv_id" : "500418168", - "media_thumbnail" : "https://www.rts.ch/2022/09/05/18/20/13360573.image/16x9/scale/width/344", - "media_publication_date" : "2022-09-05", - "media_publication_time" : "16:30:00", - "media_publication_datetime" : "2022-09-05T16:30:00+02:00", - "media_content_group" : "Yadebat,Tataki", - "media_since_publication_d" : "0", - "media_since_publication_h" : "19" - } -} \ No newline at end of file diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_mixed.dataset/Contents.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_mixed.dataset/Contents.json deleted file mode 100644 index be7832a3..00000000 --- a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_mixed.dataset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "data" : [ - { - "filename" : "urn_rts_video_14827796.json", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_mixed.dataset/urn_rts_video_14827796.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_mixed.dataset/urn_rts_video_14827796.json deleted file mode 100644 index be1d38a6..00000000 --- a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_mixed.dataset/urn_rts_video_14827796.json +++ /dev/null @@ -1,1258 +0,0 @@ -{ - "chapterUrn" : "urn:rts:video:14827796", - "episode" : { - "id" : "14718074", - "title" : "Forum", - "lead" : "Forum du 10.04.2024", - "publishedDate" : "2024-04-10T18:00:00+02:00", - "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827795.image/16x9", - "imageTitle" : "Forum [RTS]" - }, - "show" : { - "id" : "9933104", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:show:tv:9933104", - "title" : "Forum", - "lead" : "7 jours sur 7, Forum questionne en direct les acteurs de l’actualité, ouvre le débat sur les controverses qui animent la vie politique, culturelle et économique.", - "description" : "7 jours sur 7, Forum questionne en direct les acteurs de l'actualité, ouvre le débat sur les controverses qui animent la vie politique, culturelle et économique. C'est un lieu d'écoute, d'échanges, de remise en question. Forum propose chaque soir un regard attentif et acéré sur l’actualité suisse et internationale.", - "imageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/16x9", - "imageTitle" : "Logo Forum [RTS]", - "bannerImageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/3x1", - "posterImageUrl" : "https://www.rts.ch/2024/02/22/13/58/12399002.image/2x3", - "posterImageIsFallbackUrl" : false, - "homepageUrl" : "https://details.rts.ch/la-1ere/programmes/forum/", - "primaryChannelId" : "d7dfff28deee44e1d3c49a3d37d36d492b29671b", - "primaryChannelUrn" : "urn:rts:channel:tv:d7dfff28deee44e1d3c49a3d37d36d492b29671b", - "availableAudioLanguageList" : [ { - "locale" : "fr", - "language" : "Français" - } ], - "availableVideoQualityList" : [ "SD", "HD" ], - "audioDescriptionAvailable" : false, - "subtitlesAvailable" : true, - "multiAudioLanguagesAvailable" : false, - "topicList" : [ { - "id" : "49683", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:49683", - "title" : "Forum" - }, { - "id" : "16202", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:16202", - "title" : "La 1ère" - } ], - "allowIndexing" : false - }, - "channel" : { - "id" : "d7dfff28deee44e1d3c49a3d37d36d492b29671b", - "vendor" : "RTS", - "urn" : "urn:rts:channel:tv:d7dfff28deee44e1d3c49a3d37d36d492b29671b", - "title" : "RTS 2", - "imageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/16x9", - "imageUrlRaw" : "https://il.srgssr.ch/image-service/dynamic/c915e35.svg", - "imageTitle" : "Logo Forum [RTS]", - "transmission" : "TV" - }, - "chapterList" : [ { - "id" : "14827796", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:14827796", - "title" : "Forum (vidéo) - Présenté par Thibaut Schaller et Renaud Malik", - "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827795.image/16x9", - "imageTitle" : "Forum (vidéo) - Présenté par Thibaut Schaller et Renaud Malik [RTS]", - "type" : "EPISODE", - "date" : "2024-04-10T18:00:00+02:00", - "duration" : 3599720, - "validFrom" : "2024-04-10T19:00:00+02:00", - "playableAbroad" : true, - "socialCountList" : [ { - "key" : "srgView", - "value" : 1212 - }, { - "key" : "srgLike", - "value" : 0 - }, { - "key" : "fbShare", - "value" : 0 - }, { - "key" : "twitterShare", - "value" : 0 - }, { - "key" : "googleShare", - "value" : 0 - }, { - "key" : "whatsAppShare", - "value" : 2 - } ], - "displayable" : true, - "position" : 0, - "noEmbed" : false, - "analyticsData" : { - "ns_st_ep" : "Forum (vidéo) - Présenté par Thibaut Schaller et Renaud Malik", - "ns_st_ty" : "Video", - "ns_st_ci" : "14827796", - "ns_st_el" : "3599720", - "ns_st_cl" : "3599720", - "ns_st_sl" : "3599720", - "srg_mgeobl" : "false", - "ns_st_tp" : "1", - "ns_st_cn" : "1", - "ns_st_ct" : "vc12", - "ns_st_pn" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc" - }, - "analyticsMetadata" : { - "media_segment" : "Forum (vidéo) - Présenté par Thibaut Schaller et Renaud Malik", - "media_type" : "Video", - "media_segment_id" : "14827796", - "media_episode_length" : "3600", - "media_segment_length" : "3600", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "long", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:14827796", - "media_sub_set_id" : "EPISODE", - "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" - }, - "eventData" : "$639edb61591433fa$39dd7d5589c57c9d9e934d9b277655679f3ccc1188abc26332eb015bf8eb84749d1d4eade6218a91e7fa548fb8de0f5304154a3212400eb6e288c54acb84175d5176a476c815cabd7786be1e6f1fb06a9f6cd699995fb9e0338af76422d6b8cfd56f32634f999dff81476e0eb174d284ea765538b8cd7e9eea02572602725f07a08b873078b5ee9e76231fe34cb6bc2ee20f961289e9f59fb8b87255d04d2938f23e51aa62a340545f28850c9b272644b0de206dd2664284733ce12297efd04c0ce6da56f3cfa50bb82380510ea739d0ccb7a83dcfd5ec198dae564d0ed6c58315c9317342395edade408b0abba2c4935c924663a4ad37e8c606b77bcb9a54ecfa5136f2f86e6e9a0074b18e3cdb0c211a26e8a48fdab82a1fb9d375219f986bbfd51fe1ae40b412b206027e47c0f5c50cf5b1b3bd0607b047b15cc5c75aeb4dc9ba90f24b2e3ae4105d524fbff21c2b4128adeb3d51d27d334a629f75148b8a286714d05e57b742e4eb09b82e126777", - "resourceList" : [ { - "url" : "https://rts-vod-amd.akamaized.net/ww/14827796/a07f41f3-987b-3d1e-af10-b9e7220429db/master.m3u8", - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - } ], - "subtitleInformationList" : [ { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "HLS", - "type" : "SDH" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827796/a07f41f3-987b-3d1e-af10-b9e7220429db/master.m3u8" - } - } ], - "aspectRatio" : "16:9", - "spriteSheet" : { - "urn" : "urn:rts:video:14827796", - "rows" : 30, - "columns" : 20, - "thumbnailHeight" : 84, - "thumbnailWidth" : 150, - "interval" : 6000, - "url" : "https://il.srgssr.ch/spritesheet/urn/rts/video/14827796/sprite-14827796.jpeg" - } - }, { - "id" : "14827774", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:14827774", - "title" : "La Suisse organise une conférence de haut niveau sur la paix en Ukraine qui se déroulera mi-juin", - "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827765.image/16x9", - "imageTitle" : "La Suisse organise une conférence de haut niveau sur la paix en Ukraine qui se déroulera mi-juin [RTS]", - "type" : "CLIP", - "date" : "2024-04-10T18:00:00+02:00", - "duration" : 208800, - "validFrom" : "2024-04-10T19:00:00+02:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:14827796", - "position" : 1, - "noEmbed" : false, - "analyticsData" : { - "ns_st_ep" : "La Suisse organise une conférence de haut niveau sur la paix en Ukraine qui se déroulera mi-juin", - "ns_st_ty" : "Video", - "ns_st_ci" : "14827774", - "ns_st_el" : "208800", - "ns_st_cl" : "208800", - "ns_st_sl" : "208800", - "srg_mgeobl" : "false", - "ns_st_tp" : "1", - "ns_st_cn" : "1", - "ns_st_ct" : "vc11", - "ns_st_pn" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc" - }, - "analyticsMetadata" : { - "media_segment" : "La Suisse organise une conférence de haut niveau sur la paix en Ukraine qui se déroulera mi-juin", - "media_type" : "Video", - "media_segment_id" : "14827774", - "media_episode_length" : "209", - "media_segment_length" : "209", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:14827774", - "media_sub_set_id" : "CLIP", - "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" - }, - "eventData" : "$3d2421a5fd090279$f1b0b1930c0e0152328c4bede0613736bad4bff09afc6b35fbe2824b8dda4b36ceeebde74eb0f673f6bf0c63bb5d8e943014b8f08fb9678e82c47c205aa66ff887c808e5160e93c610e2da6677d4e01daa331a8bac1d47103ab130151385d47c7519b994f643035e58aee846dad0492b95795ebec6a3ddc4b17be7a03b4e894e43662db6784e803aeec556fd5fb329735f6ca1a4a3a645bf5c647854807d1d655a1ab2e49b8461e42df64afe3aa11649074b7eff32f8d9dcd3c24a4b82d67775987485392f460281d789acff0663d3506dc172775ac74b15ca678a3468a93b52a432c287b4c588ab850149210b224a29c55fbfeeda57c5dd258ce3e33cce59770e506ca75b22e57831deb364554977e6ab9d00c7cccd49d6ef7f0e54cd6bfed5c53d3ea030e6b78fd65d65ea88a5dff5a6c34a8f3eed897bd3363cb2f5fda76661f56b311831c4fd3c039f3d6872c02b1c97e61ba312585965f2ff7f11a0a80c3bad513b8a3dc0b2f723700d1adabe86", - "fullLengthMarkIn" : 99000, - "fullLengthMarkOut" : 307800, - "resourceList" : [ { - "url" : "https://rts-vod-amd.akamaized.net/ww/14827774/d39bbda9-5f74-3c41-a670-d55be1a1f0e3/master.m3u8", - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - } ], - "subtitleInformationList" : [ { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "HLS", - "type" : "SDH" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827774/d39bbda9-5f74-3c41-a670-d55be1a1f0e3/master.m3u8" - } - } ], - "aspectRatio" : "16:9" - }, { - "id" : "14827776", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:14827776", - "title" : "Conférence de haut niveau sur la paix en Ukraine: les réactions de Cédric Wermuth et Pascal Broulis", - "description" : "Réactions de Cédric Wermuth, co-président du Parti socialiste suisse, et Pascal Broulis, conseiller aux Etats PLR vaudois.", - "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827775.image/16x9", - "imageTitle" : "Conférence de haut niveau sur la paix en Ukraine: les réactions de Cédric Wermuth et Pascal Broulis [RTS]", - "type" : "CLIP", - "date" : "2024-04-10T18:00:00+02:00", - "duration" : 135200, - "validFrom" : "2024-04-10T19:00:00+02:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:14827796", - "position" : 2, - "noEmbed" : false, - "analyticsData" : { - "ns_st_ep" : "Conférence de haut niveau sur la paix en Ukraine: les réactions de Cédric Wermuth et Pascal Broulis", - "ns_st_ty" : "Video", - "ns_st_ci" : "14827776", - "ns_st_el" : "135200", - "ns_st_cl" : "135200", - "ns_st_sl" : "135200", - "srg_mgeobl" : "false", - "ns_st_tp" : "1", - "ns_st_cn" : "1", - "ns_st_ct" : "vc11", - "ns_st_pn" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc" - }, - "analyticsMetadata" : { - "media_segment" : "Conférence de haut niveau sur la paix en Ukraine: les réactions de Cédric Wermuth et Pascal Broulis", - "media_type" : "Video", - "media_segment_id" : "14827776", - "media_episode_length" : "135", - "media_segment_length" : "135", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:14827776", - "media_sub_set_id" : "CLIP", - "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" - }, - "eventData" : "$210802670c7f1a3d$710d33293247a3c70783e81fd407a86ee614cda7351b3039aa31ed5f0b1384ec1484092aa0c186828ce2022949336c51b4ccc9434526d02e41b6bdc7882a27206b879dbd73c0d18b21ef3ea018f8a9df9613e1980e14d1f079a8379fd4163c7533831c8b4988e078fa50eb54520ac9181ca02acda9be74dcf2d03862bb547aba11389a8025fadc6771a6e13628dcb9a23809d65d8265f33e692724c0af89def4cfb12f67bdc3fb2c48f38c021c26e105d480162e8f7300dfe0d5b8e1945f1a0a8891c1d828b3c9d606763b4137a7536282870c2f297f5b1b12db616226fe42501c1604aef292fa78f7b9856398307174082e84fd5351635a0a824352c73340b53cbc041cd4fda9dfe2da63c0182f4b5e3398ed9debec5f3d369bda7bb58b5123625358ff825d940d823691454ac208ed7fa04a62272d06de690a2a8ba19103ea8a9747a972cc01a17f120c99a6e3a993854ebd513e5e9907255317c242528da31621298ab8d2e725ff49b48be7969324", - "fullLengthMarkIn" : 307800, - "fullLengthMarkOut" : 443000, - "resourceList" : [ { - "url" : "https://rts-vod-amd.akamaized.net/ww/14827776/3ecdae9b-b4f9-3492-9b16-4b2c7fa09706/master.m3u8", - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - } ], - "subtitleInformationList" : [ { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "HLS", - "type" : "SDH" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827776/3ecdae9b-b4f9-3492-9b16-4b2c7fa09706/master.m3u8" - } - } ], - "aspectRatio" : "16:9" - }, { - "id" : "14827778", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:14827778", - "title" : "Le Conseil fédéral propose des mesures pour mieux encadrer les grandes banques", - "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827777.image/16x9", - "imageTitle" : "Le Conseil fédéral propose des mesures pour mieux encadrer les grandes banques [RTS]", - "type" : "CLIP", - "date" : "2024-04-10T18:00:00+02:00", - "duration" : 215680, - "validFrom" : "2024-04-10T19:00:00+02:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:14827796", - "position" : 3, - "noEmbed" : false, - "analyticsData" : { - "ns_st_ep" : "Le Conseil fédéral propose des mesures pour mieux encadrer les grandes banques", - "ns_st_ty" : "Video", - "ns_st_ci" : "14827778", - "ns_st_el" : "215680", - "ns_st_cl" : "215680", - "ns_st_sl" : "215680", - "srg_mgeobl" : "false", - "ns_st_tp" : "1", - "ns_st_cn" : "1", - "ns_st_ct" : "vc11", - "ns_st_pn" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc" - }, - "analyticsMetadata" : { - "media_segment" : "Le Conseil fédéral propose des mesures pour mieux encadrer les grandes banques", - "media_type" : "Video", - "media_segment_id" : "14827778", - "media_episode_length" : "216", - "media_segment_length" : "216", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:14827778", - "media_sub_set_id" : "CLIP", - "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" - }, - "eventData" : "$068c9a1aad2071b1$e23321f435dd224b565339c3e79a5d70bd795bbedbe5d325618e06d353e522e945f9a6e5ae4fe4c9329aed070a53cbf7ef2232c4357149b7049384c2b66f75c9b58605cc32d00fbbbb1505d14cae448a35655e772f00780b67538ac74c37306328a2cbb668663b24f71aff30803bafc8c6aea3028bee42335598ce904bdd084aed55eb69054d933ad063195a40121f833c54248ebeedf027957fb049d72f8817274cb395da9a58c4a74a9cf5ddef3090496ae6afcff92a9a9e0555dd3aba8d4dee4443b34599ca286d5dc788da5ecec07062afac5959bb997833d899b46bdbfd69c7aa1f5010354f223b2cd5afb77c930cafe2d3d9c0e421d2aeb5ed12b96c61e5a7937b40de5b44a0a85e67ceaad94e356ae3847fa4c24e98b915dd43507adc5e50f8885066bb68142b642641ac8e0648aae561a2e2e99d211551063be17003b7a779606668acc98beff509fc82bf1f8356b44c08e480ece9f1613651de6ea2e2e9b93a79b7953df9fd89d0a9f4162b", - "fullLengthMarkIn" : 443000, - "fullLengthMarkOut" : 658680, - "resourceList" : [ { - "url" : "https://rts-vod-amd.akamaized.net/ww/14827778/7b0ecad4-504d-3c45-9639-f5a8d1d6e3b0/master.m3u8", - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - } ], - "subtitleInformationList" : [ { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "HLS", - "type" : "SDH" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827778/7b0ecad4-504d-3c45-9639-f5a8d1d6e3b0/master.m3u8" - } - } ], - "aspectRatio" : "16:9" - }, { - "id" : "14827780", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:14827780", - "title" : "Mesures pour encadrer les grandes banques: réactions de de Cédric Wermuth et Pascal Broulis", - "description" : "Réactions de Cédric Wermuth, co-président du Parti socialiste suisse, et Pascal Broulis, conseiller aux Etats PLR vaudois.", - "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827779.image/16x9", - "imageTitle" : "Mesures pour encadrer les grandes banques: réactions de de Cédric Wermuth et Pascal Broulis [RTS]", - "type" : "CLIP", - "date" : "2024-04-10T18:00:00+02:00", - "duration" : 431720, - "validFrom" : "2024-04-10T19:00:00+02:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:14827796", - "position" : 4, - "noEmbed" : false, - "analyticsData" : { - "ns_st_ep" : "Mesures pour encadrer les grandes banques: réactions de de Cédric Wermuth et Pascal Broulis", - "ns_st_ty" : "Video", - "ns_st_ci" : "14827780", - "ns_st_el" : "431720", - "ns_st_cl" : "431720", - "ns_st_sl" : "431720", - "srg_mgeobl" : "false", - "ns_st_tp" : "1", - "ns_st_cn" : "1", - "ns_st_ct" : "vc11", - "ns_st_pn" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc" - }, - "analyticsMetadata" : { - "media_segment" : "Mesures pour encadrer les grandes banques: réactions de de Cédric Wermuth et Pascal Broulis", - "media_type" : "Video", - "media_segment_id" : "14827780", - "media_episode_length" : "432", - "media_segment_length" : "432", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:14827780", - "media_sub_set_id" : "CLIP", - "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" - }, - "eventData" : "$6e3534588d6725f4$4703f9023b457157ed443aa7b95fa82dd3f7a70d60dabecced85efebda6f83a468e3375da9dae4a8327eedf7f62305e318b50cddb9d92d7d28119086670b06108021e18ce8d37e8e000630c47f9a94e4b601b2917d8693b7d997d15e9948468f97c491375e6cb40dc8409fcff1c60206d03c0397b889260eb9b0b159fdc8baa32e7d654cca711037d7ad651dbd52f22b84faf5d8ea350d53fd9f87490bb41b3aae02cf8f2c84b96bfc1ccbb5efd44fee7940c554539c19aeb08f4a6435b53a217412807d8c78f03c4b322b8586edb5c04f2fd25d7f54eccf063dc1ff02c07c3aef43ca0026718943d25f537b87eb7ff129eb63f1dd1ed271559817e92ef8f6d30074b4dc41dcb86b9699934b371400e70d95f1a144ff80155c73281d238dc95fa2ab7c18c030d3579c7b42b0c635194cc22ee249eea8323c710dcb3c2f12827c3b7d34cfeee716b29f6f603e6f7bc189dc40e620832d7891d7d35f489ea820626cb7ab263c0ca8ceaf0bd94b55910784", - "fullLengthMarkIn" : 658680, - "fullLengthMarkOut" : 1090400, - "resourceList" : [ { - "url" : "https://rts-vod-amd.akamaized.net/ww/14827780/1c34db09-0de5-3c67-acb0-017db627e3a0/master.m3u8", - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - } ], - "subtitleInformationList" : [ { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "HLS", - "type" : "SDH" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827780/1c34db09-0de5-3c67-acb0-017db627e3a0/master.m3u8" - } - } ], - "aspectRatio" : "16:9" - }, { - "id" : "14827782", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:14827782", - "title" : "Le recours à l'intelligence artificielle par l'armée israélienne: interview de Jean-Marc Rickli", - "description" : "Interview de Jean-Marc Rickli, responsable du département des risques mondiaux et émergents au centre genevois de politique de sécurité (GCSP).", - "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827781.image/16x9", - "imageTitle" : "Le recours à l'intelligence artificielle par l'armée israélienne: interview de Jean-Marc Rickli [RTS]", - "type" : "CLIP", - "date" : "2024-04-10T18:00:00+02:00", - "duration" : 389200, - "validFrom" : "2024-04-10T19:00:00+02:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:14827796", - "position" : 5, - "noEmbed" : false, - "analyticsData" : { - "ns_st_ep" : "Le recours à l'intelligence artificielle par l'armée israélienne: interview de Jean-Marc Rickli", - "ns_st_ty" : "Video", - "ns_st_ci" : "14827782", - "ns_st_el" : "389200", - "ns_st_cl" : "389200", - "ns_st_sl" : "389200", - "srg_mgeobl" : "false", - "ns_st_tp" : "1", - "ns_st_cn" : "1", - "ns_st_ct" : "vc11", - "ns_st_pn" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc" - }, - "analyticsMetadata" : { - "media_segment" : "Le recours à l'intelligence artificielle par l'armée israélienne: interview de Jean-Marc Rickli", - "media_type" : "Video", - "media_segment_id" : "14827782", - "media_episode_length" : "389", - "media_segment_length" : "389", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:14827782", - "media_sub_set_id" : "CLIP", - "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" - }, - "eventData" : "$5ddcf9d04706d658$e5ae57d14ede33563e7155b90bdf29a6c962a6722fc91671678a47d0866d80da9a1b8b6d95d78bfaa81e5db63f24f9571b685ba492542696157005d4441dbd494e13bee4d890cccf7ca0758d0420fd9c72dc9570d3c76c68ab73177b640886b68d7ae30ba4718bc74fe08d53fb150b2d4604657cc8b5b5c0f8de1f5d565c6a8ac93c302d01f654a63e9ff2f38b9dde31e253bfa2a8cd32a540236ca0b4ff2dbb26cf4528666e711441b1dd80378263cbf03a096b0b38873988ea33d92fe86ae8b8a096f9e1fbf38d9f92d3b56664787d8d7ece67b8250d8597e111e3640a157e8d983bec6a8e9899ebc8e3e22ba950c0506d36f07fc5dc4e8dcee0a3141f8cbcfdf0c1a87d1e494d2b5942eac62dbd7ef09225761db130b1ded7de4df6bed9ae6f51f90ae2841462c7a15615bce14836d670d01b76e4ab134aacb619f38ad2f5765c49af7cec668978cca36e8dc99ec270921496cfdd879b27696fe348f7874c34887cfe881715a32724d9c9958b372e", - "fullLengthMarkIn" : 1090400, - "fullLengthMarkOut" : 1479600, - "resourceList" : [ { - "url" : "https://rts-vod-amd.akamaized.net/ww/14827782/11ab086f-9d6c-3d0f-9d2d-ab838ad3f19d/master.m3u8", - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - } ], - "subtitleInformationList" : [ { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "HLS", - "type" : "SDH" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827782/11ab086f-9d6c-3d0f-9d2d-ab838ad3f19d/master.m3u8" - } - } ], - "aspectRatio" : "16:9" - }, { - "id" : "14827784", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:14827784", - "title" : "Pacte migratoire européen: les Vingt-Sept durcissent les contrôles des arrivées aux frontières", - "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827783.image/16x9", - "imageTitle" : "Pacte migratoire européen: les Vingt-Sept durcissent les contrôles des arrivées aux frontières [RTS]", - "type" : "CLIP", - "date" : "2024-04-10T18:00:00+02:00", - "duration" : 146640, - "validFrom" : "2024-04-10T19:00:00+02:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:14827796", - "position" : 6, - "noEmbed" : false, - "analyticsData" : { - "ns_st_ep" : "Pacte migratoire européen: les Vingt-Sept durcissent les contrôles des arrivées aux frontières", - "ns_st_ty" : "Video", - "ns_st_ci" : "14827784", - "ns_st_el" : "146640", - "ns_st_cl" : "146640", - "ns_st_sl" : "146640", - "srg_mgeobl" : "false", - "ns_st_tp" : "1", - "ns_st_cn" : "1", - "ns_st_ct" : "vc11", - "ns_st_pn" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc" - }, - "analyticsMetadata" : { - "media_segment" : "Pacte migratoire européen: les Vingt-Sept durcissent les contrôles des arrivées aux frontières", - "media_type" : "Video", - "media_segment_id" : "14827784", - "media_episode_length" : "147", - "media_segment_length" : "147", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:14827784", - "media_sub_set_id" : "CLIP", - "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" - }, - "eventData" : "$7a293f98d6d07f8b$c9d11b497a448ec03e333750802081d0b53616f8e53f7cdd51c6486ac2cba23360936e017cb4347b8bb4d72161855972c603454ddc5b432e8606034ef571d2314eb287246663590cd2c426d8f73732cac751f17d9fea844cb1501b6c503046bb7fef09fee75ba43919313bddd04a3724f21ee832e99193ef90b62fce096fa9906888f02cce712964ec13bf4be2cdb5b31d1dacb665d9cd007e1a3c74be7fd61377bf5fb65459fd3e0ddefd308e669e4f38a50c5bba6c3271d8aa8c618b9fa915fc57d8c8d83333b51866c5feab4344d50b447cfdef60655d506c59f2b496f9415eae94eda6c300f83ef4bdb76257aaac89d8dc50bb63fe06bc6f162bb872e22c97c9b733ce94cb82bfc94d5334e713f9f84cce3a332a8d6a95fec4c7c82cc7581b40c7476d6ea3e0debaf5578c0b460d340caf8f14d57e71c6fbe0e24d14370a8ebd9aa0c14b906a054ffbf70fc81163837f285654e44c708a6cfeb37308fa000db09ee396bfb3e7e1248ee2a97f98af", - "fullLengthMarkIn" : 1479600, - "fullLengthMarkOut" : 1626240, - "resourceList" : [ { - "url" : "https://rts-vod-amd.akamaized.net/ww/14827784/bdcfc85d-6017-3f28-8334-19fd74b1c4e3/master.m3u8", - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - } ], - "subtitleInformationList" : [ { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "HLS", - "type" : "SDH" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827784/bdcfc85d-6017-3f28-8334-19fd74b1c4e3/master.m3u8" - } - } ], - "aspectRatio" : "16:9" - }, { - "id" : "14827786", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:14827786", - "title" : "Jura: un ressortissant français s'est vu refuser la naturalisation suisse car il tondait sa pelouse le dimanche", - "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827785.image/16x9", - "imageTitle" : "Jura: un ressortissant français s'est vu refuser la naturalisation suisse car il tondait sa pelouse le dimanche [RTS]", - "type" : "CLIP", - "date" : "2024-04-10T18:00:00+02:00", - "duration" : 174080, - "validFrom" : "2024-04-10T19:00:00+02:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:14827796", - "position" : 7, - "noEmbed" : false, - "analyticsData" : { - "ns_st_ep" : "Jura: un ressortissant français s'est vu refuser la naturalisation suisse car il tondait sa pelouse le dimanche", - "ns_st_ty" : "Video", - "ns_st_ci" : "14827786", - "ns_st_el" : "174080", - "ns_st_cl" : "174080", - "ns_st_sl" : "174080", - "srg_mgeobl" : "false", - "ns_st_tp" : "1", - "ns_st_cn" : "1", - "ns_st_ct" : "vc11", - "ns_st_pn" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc" - }, - "analyticsMetadata" : { - "media_segment" : "Jura: un ressortissant français s'est vu refuser la naturalisation suisse car il tondait sa pelouse le dimanche", - "media_type" : "Video", - "media_segment_id" : "14827786", - "media_episode_length" : "174", - "media_segment_length" : "174", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:14827786", - "media_sub_set_id" : "CLIP", - "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" - }, - "eventData" : "$b41d27c3cc43eac9$7b12fe99ba916fd8c5531b966fbc6f00cd1bf8601a8e86e4565b07dcfa760a57fed7fb1aba47c5bf171cb3f190976ed1556afdc765e79b6256dcb853289980d4f6b7ab672d940543579492b30ab510141e0060858cad4d9ef72dbc4ec91698013c0ceb9bfa1b57f7a8ce5a1f6102542868a0aaac711b75f8b08aa7760df8cd86dc880d13e4daecd814160711622fe496d2ab8dd0bcda0ed8f3fb1c3b7650ffc6e07e14199e06f3906a1624a81b0f8d607dc6903d2008825e549f086b003a9e25cdb20dd3f509dd2b29de1c1a43210e89fbab98d42a015106f9f45c088dffaf3aaf3b513c2a613ecb4231df79ecceeef35983e3d009bc253b50ab0e91fa46cd24532756471ea1a961db173b171f0b5d3ff59be4a62152804ed3aa866a7586ed6f40d9f02b051d7851e2e7a76677ef642dd29af0207775255393bc9383a5253832ae42afc022533b7eb4c9780d59235a9ed9b04239bdf011102d2735621e81865d0b86d7d700b01ba4fa5291e82e08717e", - "fullLengthMarkIn" : 1626240, - "fullLengthMarkOut" : 1800320, - "resourceList" : [ { - "url" : "https://rts-vod-amd.akamaized.net/ww/14827786/3324a962-78c5-32a3-a8be-6df0ff97db17/master.m3u8", - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - } ], - "subtitleInformationList" : [ { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "HLS", - "type" : "SDH" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827786/3324a962-78c5-32a3-a8be-6df0ff97db17/master.m3u8" - } - } ], - "aspectRatio" : "16:9" - }, { - "id" : "14827788", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:14827788", - "title" : "Copinage, népotisme: quelles règles s'appliquent dans l'administration?", - "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827787.image/16x9", - "imageTitle" : "Copinage, népotisme: quelles règles s'appliquent dans l'administration? [RTS]", - "type" : "CLIP", - "date" : "2024-04-10T18:00:00+02:00", - "duration" : 189960, - "validFrom" : "2024-04-10T19:00:00+02:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:14827796", - "position" : 8, - "noEmbed" : false, - "analyticsData" : { - "ns_st_ep" : "Copinage, népotisme: quelles règles s'appliquent dans l'administration?", - "ns_st_ty" : "Video", - "ns_st_ci" : "14827788", - "ns_st_el" : "189960", - "ns_st_cl" : "189960", - "ns_st_sl" : "189960", - "srg_mgeobl" : "false", - "ns_st_tp" : "1", - "ns_st_cn" : "1", - "ns_st_ct" : "vc11", - "ns_st_pn" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc" - }, - "analyticsMetadata" : { - "media_segment" : "Copinage, népotisme: quelles règles s'appliquent dans l'administration?", - "media_type" : "Video", - "media_segment_id" : "14827788", - "media_episode_length" : "190", - "media_segment_length" : "190", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:14827788", - "media_sub_set_id" : "CLIP", - "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" - }, - "eventData" : "$cebd9d8e209f9f72$8f7e44e8842283b05aecaac5de32c1b31c6b54ccc538ebc1b493a1d30b484f78e50d1bced6c31655a4fbc1b54e9aa65d3171ad3243af84516d5d87405a7f1f7ccf2744bad835d7b15386bffda048b2e9cc2d63b6e091c4e33d71a2e47917403201bfe0d209409bb2bbb6a3536a7f756e95a64b53d2261df72dd2d38bec51691112d9f2b2838a810d6d033e60709f8cb19447f450e21cac720289d700d4ada620469120134802b2421c8c4f48ea06adfbeeff6af6291166f2c21ab253aab3f0195c545d2a456044adf15784b42c990e5ad43b2cfa47007f6e439859ab27ee2c769a92ce1ec02c40f003dc76fa6e16cd1e1a80f220cfbc821ffb8bcee90243b635a8c7c0af1eaf67bf1d34293a3c2247a37c34570e4c671fd80733b9c6e46964fa353117c642ae975db7b31dbaf05dc7f9842f4039e31e7d3a6fc2e3094cf0e0635ec714a84b3636e0bbf99e23493cd86e6696894239f3d32e9b18581d7d84a51d3d9fb8484a369156bc7881a3e14aeb94", - "fullLengthMarkIn" : 1800320, - "fullLengthMarkOut" : 1990280, - "resourceList" : [ { - "url" : "https://rts-vod-amd.akamaized.net/ww/14827788/a7f8c778-9de2-38a5-8b1a-92c668746054/master.m3u8", - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - } ], - "subtitleInformationList" : [ { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "HLS", - "type" : "SDH" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827788/a7f8c778-9de2-38a5-8b1a-92c668746054/master.m3u8" - } - } ], - "aspectRatio" : "16:9" - }, { - "id" : "14827790", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:14827790", - "title" : "Hockey sur glace: fin de saison pour Fribourg-Gottéron ce soir face à Lausanne?", - "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827789.image/16x9", - "imageTitle" : "Hockey sur glace: fin de saison pour Fribourg-Gottéron ce soir face à Lausanne? [RTS]", - "type" : "CLIP", - "date" : "2024-04-10T18:00:00+02:00", - "duration" : 131840, - "validFrom" : "2024-04-10T19:00:00+02:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:14827796", - "position" : 9, - "noEmbed" : false, - "analyticsData" : { - "ns_st_ep" : "Hockey sur glace: fin de saison pour Fribourg-Gottéron ce soir face à Lausanne?", - "ns_st_ty" : "Video", - "ns_st_ci" : "14827790", - "ns_st_el" : "131840", - "ns_st_cl" : "131840", - "ns_st_sl" : "131840", - "srg_mgeobl" : "false", - "ns_st_tp" : "1", - "ns_st_cn" : "1", - "ns_st_ct" : "vc11", - "ns_st_pn" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc" - }, - "analyticsMetadata" : { - "media_segment" : "Hockey sur glace: fin de saison pour Fribourg-Gottéron ce soir face à Lausanne?", - "media_type" : "Video", - "media_segment_id" : "14827790", - "media_episode_length" : "132", - "media_segment_length" : "132", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:14827790", - "media_sub_set_id" : "CLIP", - "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" - }, - "eventData" : "$2d3cf00ff55412b5$2f4336501473190b1fe37356cd47edb03dc54a41f59a9a9459d4046610427b68b36280b927492d45acb239c29e076010beead9d7429ed417b9dc5a003f4b8e6865a8dfe7df6e8419774e038639aa069e0037122289c33e85cd5682eb199f2049d49b7166d6aef9130c8d7bd0bb08d24ddd0d63fe6766468ad28c479c95cc57d66e1a7aa2f58bf2298461f77688345d40d7438779381639a115e1f7ea672ff445c7efeec82f4fd16f4bb161d4f3021d277eb8a788ba7273d7e1a0e5738a87c364a10115395b40236f502e8acb4cf2e2b9817b23b79a15bc7a7ada2934fc23ed7aa7f5e560ad76c751eb409c7606d3467e6a9ff1dea3790170c3d6668330bee3e09f970ebf424697f73200c6f7c2c03ab49aee60d2c7510fb3cfb6070387324d49dceddb4c6b0bb03f8e7fbfde18c987fa2816f0846da671aeafae890e7681ec379afff649cd0b9a9395209b93373edb270b01c38e1f3e9fb180b6a31366042297c4dc1d5ffae5cc8c7a2c48152c8e1e03", - "fullLengthMarkIn" : 2004520, - "fullLengthMarkOut" : 2136360, - "resourceList" : [ { - "url" : "https://rts-vod-amd.akamaized.net/ww/14827790/f3ee717e-1234-339d-b370-2b91e09e33c4/master.m3u8", - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - } ], - "subtitleInformationList" : [ { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "HLS", - "type" : "SDH" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827790/f3ee717e-1234-339d-b370-2b91e09e33c4/master.m3u8" - } - } ], - "aspectRatio" : "16:9" - }, { - "id" : "14827792", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:14827792", - "title" : "Le grand débat - Faut-il sauver les jobs d'été?", - "description" : "Débat entre les députés genevois Caroline Renold (PS) et Jean-Marc Guinchard (Centre), et Sylvain Weber, professeur à la Haute école de gestion de Genève.", - "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827791.image/16x9", - "imageTitle" : "Le grand débat - Faut-il sauver les jobs d'été? [RTS]", - "type" : "CLIP", - "date" : "2024-04-10T18:00:00+02:00", - "duration" : 1071000, - "validFrom" : "2024-04-10T19:00:00+02:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:14827796", - "position" : 10, - "noEmbed" : false, - "analyticsData" : { - "ns_st_ep" : "Le grand débat - Faut-il sauver les jobs d'été?", - "ns_st_ty" : "Video", - "ns_st_ci" : "14827792", - "ns_st_el" : "1071000", - "ns_st_cl" : "1071000", - "ns_st_sl" : "1071000", - "srg_mgeobl" : "false", - "ns_st_tp" : "1", - "ns_st_cn" : "1", - "ns_st_ct" : "vc12", - "ns_st_pn" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc" - }, - "analyticsMetadata" : { - "media_segment" : "Le grand débat - Faut-il sauver les jobs d'été?", - "media_type" : "Video", - "media_segment_id" : "14827792", - "media_episode_length" : "1071", - "media_segment_length" : "1071", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "long", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:14827792", - "media_sub_set_id" : "CLIP", - "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" - }, - "eventData" : "$45a21bdfa3fde5e1$7ad9a53938ab0577fbcca7109ec84f74b0c6990bb249ddab37baf116d840a4b2373efbe794b027213f30a4df44105572d370af89d17416c5693289ce3238e8f81058612a823918ea4bf8c93f364f2f308d841cdfbdc5d7b7e71dc54c84d5e1f115d01a82e9c250623c9cc209b68f9f6aa9afe19a2b35f8641368fc375fb3ba874243da940b0be273d334f290f22d9545d60c9193c00bc5d73e76250f8fe988839d38e5ca596c6c3f4c45d9a6fe1a5a0cd0323d9f1cf19ec14941971abd9346ecc0183ebe4905debd36b9e592383e16209b0fed0f664fc3babdff3d5d3c9d618b2249990e1a3fe5e6b635bf6967c2186e696a44f7b2bf7ea1aa23230cc6c7411e1d477edd85d80a4d6660dabdf9e621960e833c8914694c1b0cc808ec214b7e96b8fd62bd1785c1bb2012635b3bb37185e537400cf4923ec62b41e2507629ef9c35434efc4492be56d7a23325167f83f7895482185662547452c90f20db79b720161a89008effa74ef43b66484797e95e", - "fullLengthMarkIn" : 2142080, - "fullLengthMarkOut" : 3213080, - "resourceList" : [ { - "url" : "https://rts-vod-amd.akamaized.net/ww/14827792/4def5e57-2634-3f0f-97a8-cd19b96bd2ef/master.m3u8", - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - } ], - "subtitleInformationList" : [ { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "HLS", - "type" : "SDH" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827792/4def5e57-2634-3f0f-97a8-cd19b96bd2ef/master.m3u8" - } - } ], - "aspectRatio" : "16:9" - }, { - "id" : "14827794", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:14827794", - "title" : "Forum des idées - L'Université de Lausanne veut rendre la science plus utile aux citoyens", - "description" : "Interview de Cléolia Sabot, coordinatrice d'Interface, le Fonds de soutien à la recherche partenariale de l'Unil.", - "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827793.image/16x9", - "imageTitle" : "Forum des idées - L'Université de Lausanne veut rendre la science plus utile aux citoyens [RTS]", - "type" : "CLIP", - "date" : "2024-04-10T18:00:00+02:00", - "duration" : 335160, - "validFrom" : "2024-04-10T19:00:00+02:00", - "playableAbroad" : true, - "displayable" : true, - "position" : 11, - "noEmbed" : false, - "analyticsData" : { - "ns_st_ep" : "Forum des idées - L'Université de Lausanne veut rendre la science plus utile aux citoyens", - "ns_st_ty" : "Video", - "ns_st_ci" : "14827794", - "ns_st_el" : "335160", - "ns_st_cl" : "335160", - "ns_st_sl" : "335160", - "srg_mgeobl" : "false", - "ns_st_tp" : "1", - "ns_st_cn" : "1", - "ns_st_ct" : "vc11", - "ns_st_pn" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc" - }, - "analyticsMetadata" : { - "media_segment" : "Forum des idées - L'Université de Lausanne veut rendre la science plus utile aux citoyens", - "media_type" : "Video", - "media_segment_id" : "14827794", - "media_episode_length" : "335", - "media_segment_length" : "335", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:14827794", - "media_sub_set_id" : "CLIP", - "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" - }, - "eventData" : "$1be907c9fe4c81d9$1327cda5d0b97592ab053853b4aebb4e163333dffe62d17cbc2dbe50c87fc6c252dca6a57b91d22c9fe05891ebd8e9ac8cfe8f6d906826259f49ef35c7cec418e30e88b451677989f78a15ab3c40c8533a2b9f0abe21a102c653ebc5ac32e1aa7b65b553895923f1c9f6d601a6a9ca50a6ad5b19ff867063b6d1b10746d82d81c215146081c26458572f242ded2833b64dddc959bd9947f0adf80cef893a4fb9046077db73f4d265d713d956d73bfde3cfb5251e41bc38f0f1e61260f4265a3609b52c418ab9c1aaf7e35db1e952b5b33a21ba49d937955056823c2dea32a493cf2df63b348d303d97afde95e7b55a24546102b6341bd4b81bf8183c842c01e6b30532a296a1e8076aadbea22635b29dc37272b3690df4b4253fc70db8015310e8d07a9c03fe6354734fad839bb164ad7d0862b67f048df4244f5084862e5e3ea81949b669e0db43e50b479c7ee8ec10163ed05c88b07d468df463171ce5bc8b24caa0c159587aa47e90e85868b7d092", - "fullLengthMarkIn" : 3218240, - "fullLengthMarkOut" : 3553400, - "resourceList" : [ { - "url" : "https://rts-vod-amd.akamaized.net/ww/14827794/8e8efcc7-37f6-3078-bf7e-5894324b29e6/master.m3u8", - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - } ], - "subtitleInformationList" : [ { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "HLS", - "type" : "SDH" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827794/8e8efcc7-37f6-3078-bf7e-5894324b29e6/master.m3u8" - } - } ], - "aspectRatio" : "16:9" - }, { - "id" : "14812522", - "mediaType" : "AUDIO", - "vendor" : "RTS", - "urn" : "urn:rts:audio:14812522", - "title" : "Forum - Présenté par Thibaut Schaller et Renaud Malik", - "imageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/16x9", - "imageTitle" : "Logo Forum [RTS]", - "type" : "CLIP", - "date" : "2024-04-10T18:00:00+02:00", - "duration" : 3600000, - "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/14812522/b3fde219-dd00-3bfa-a085-a90ee4c9969a.mp3", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:14827796", - "position" : 12, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Forum - Présenté par Thibaut Schaller et Renaud Malik", - "media_type" : "Audio", - "media_segment_id" : "14812522", - "media_episode_length" : "3600", - "media_segment_length" : "3600", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "long", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:audio:14812522", - "media_sub_set_id" : "CLIP", - "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" - }, - "eventData" : "$94d0a22d6ce4f018$cce0e3446e92b4d9024e6a2e2e390087e63c86fd4586474c28ce7ece2806db8ddc68336257833a0c54c98ed43e6e283e8f321b56b0ee6bd47f56acfe2f12ffc438ca2c71ba9406d02c1a0be9c0f4f929f54107d9065f94b78b464aa90711ef056b97eebad09da193c1f16040d7df5d7a0983166d7139e57dbfed5951dd29c5761969b9412266cd2baa7f8fd0efbedb05ed2489d61dc08ed1c51768e997c989e69d055a645d3ed277c2a2837d7317141bb6e1bb2a2c204d4574a0b99be034c7af81e7331c459f48b17725aca6ad1fe179035ce3d61853623a06306b30b89f48f69932de9a0e61829d9b5b678ce0e662a9378a9a2fa10914586793b6efbabb9bc91863306cc39b1534ce39fc17632f59c2461d1ccfded9504fa7810e6c2b4f2cc3c3932fa54d8c4e262ac6fa82b358542e55b07302655a89e6eb5485a5b6dcf2e9febc6167095269892f133b74bea6596c173fdc8cfef634e6e8d0fb1f4982e4c81aded9d224122ded855676dbe6d59206", - "fullLengthMarkIn" : 0, - "fullLengthMarkOut" : 0, - "resourceList" : [ { - "url" : "https://rts-aod-dd.akamaized.net/ww/14812522/b3fde219-dd00-3bfa-a085-a90ee4c9969a.mp3", - "quality" : "HQ", - "protocol" : "HTTPS", - "encoding" : "MP3", - "mimeType" : "audio/mpeg", - "presentation" : "DEFAULT", - "streaming" : "PROGRESSIVE", - "dvr" : false, - "live" : false, - "mediaContainer" : "NONE", - "audioCodec" : "MP3", - "videoCodec" : "NONE", - "tokenType" : "NONE", - "analyticsMetadata" : { - "media_streaming_quality" : "HQ", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-aod-dd.akamaized.net/ww/14812522/b3fde219-dd00-3bfa-a085-a90ee4c9969a.mp3" - } - } ] - } ], - "topicList" : [ { - "id" : "49683", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:49683", - "title" : "Forum" - }, { - "id" : "16202", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:16202", - "title" : "La 1ère" - } ], - "analyticsData" : { - "srg_pr_id" : "14718074", - "srg_plid" : "9933104", - "ns_st_pl" : "Forum", - "ns_st_pr" : "Forum du 10.04.2024", - "ns_st_dt" : "2024-04-10", - "ns_st_ddt" : "2024-04-10", - "ns_st_tdt" : "2024-04-10", - "ns_st_tm" : "18:00", - "ns_st_tep" : "500531747", - "ns_st_li" : "0", - "ns_st_stc" : "0867", - "ns_st_st" : "RTS Online", - "ns_st_tpr" : "9933104", - "ns_st_en" : "*null", - "ns_st_ge" : "*null", - "ns_st_ia" : "*null", - "ns_st_ce" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc", - "srg_unit" : "RTS", - "srg_c1" : "full", - "srg_c2" : "video_la-1ere_forum", - "srg_c3" : "RTS 2", - "srg_tv_id" : "500531747", - "srg_aod_prid" : "14718074" - }, - "analyticsMetadata" : { - "media_episode_id" : "14718074", - "media_show_id" : "9933104", - "media_show" : "Forum", - "media_episode" : "Forum du 10.04.2024", - "media_is_livestream" : "false", - "media_full_length" : "full", - "media_enterprise_units" : "RTS", - "media_joker1" : "full", - "media_joker2" : "video_la-1ere_forum", - "media_joker3" : "RTS 2", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_tv_id" : "500531747", - "media_thumbnail" : "https://www.rts.ch/2024/04/10/19/51/14827795.image/16x9/scale/width/344", - "media_publication_date" : "2024-04-10", - "media_publication_time" : "19:00:00", - "media_publication_datetime" : "2024-04-10T19:00:00+02:00", - "media_tv_date" : "2024-04-10", - "media_tv_time" : "18:00:00", - "media_tv_datetime" : "2024-04-10T18:00:00+02:00", - "media_content_group" : "Forum,La 1ère", - "media_channel_id" : "d7dfff28deee44e1d3c49a3d37d36d492b29671b", - "media_channel_cs" : "0867", - "media_channel_name" : "RTS 2", - "media_since_publication_d" : "11", - "media_since_publication_h" : "278" - } -} \ No newline at end of file diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_onDemand.dataset/Contents.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_onDemand.dataset/Contents.json deleted file mode 100644 index 56c7e33d..00000000 --- a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_onDemand.dataset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "data" : [ - { - "filename" : "urn_rts_video_13360574.json", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_onDemand.dataset/urn_rts_video_13360574.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_onDemand.dataset/urn_rts_video_13360574.json deleted file mode 100644 index 7fe752b9..00000000 --- a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_onDemand.dataset/urn_rts_video_13360574.json +++ /dev/null @@ -1,210 +0,0 @@ -{ - "chapterUrn" : "urn:rts:video:13360574", - "episode" : { - "number": 12, - "seasonNumber": 2, - "id" : "13360565", - "title" : "Yadebat", - "publishedDate" : "2022-09-05T16:30:00+02:00", - "imageUrl" : "https://www.rts.ch/2022/09/05/18/20/13360573.image/16x9", - "imageTitle" : "On réunit des ex après leur rupture [RTS]" - }, - "show" : { - "id" : "10174267", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:show:tv:10174267", - "title" : "Yadebat", - "lead" : "Une série qui te donne la parole, pour laisser entendre ton avis sur les débats de société animée par Melissa.", - "description" : "Une série qui te donne la parole, pour laisser entendre ton avis sur les débats de société animée par Melissa.", - "imageUrl" : "https://www.rts.ch/2020/01/10/11/14/10520588.image/16x9", - "imageTitle" : "Yadebat - Tataki [RTS]", - "posterImageUrl" : "https://ws.srf.ch/asset/image/audio/e0322b37-5697-474d-93ac-19a4044a6a24/POSTER.jpg", - "posterImageIsFallbackUrl" : true, - "audioDescriptionAvailable" : false, - "subtitlesAvailable" : false, - "multiAudioLanguagesAvailable" : false, - "topicList" : [ { - "id" : "59952", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:59952", - "title" : "Yadebat" - }, { - "id" : "54537", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:54537", - "title" : "Tataki" - } ], - "allowIndexing" : false - }, - "chapterList" : [ { - "id" : "13360574", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13360574", - "title" : "On réunit des ex après leur rupture", - "description" : "Dans ce nouvel épisode de YADEBAT, Mélissa réunit 3 couples qui se sont séparés récemment. Elles les a questionné en face à face pour connaître leurs différents ressentis et réactions.", - "imageUrl" : "https://www.rts.ch/2022/09/05/18/20/13360573.image/16x9", - "imageTitle" : "On réunit des ex après leur rupture [RTS]", - "type" : "EPISODE", - "date" : "2022-09-05T16:30:00+02:00", - "duration" : 902360, - "validFrom" : "2022-09-05T16:30:00+02:00", - "validTo" : "2100-01-01T23:59:59+01:00", - "playableAbroad" : true, - "socialCountList" : [ { - "key" : "srgView", - "value" : 17 - }, { - "key" : "srgLike", - "value" : 0 - }, { - "key" : "fbShare", - "value" : 0 - }, { - "key" : "twitterShare", - "value" : 0 - }, { - "key" : "googleShare", - "value" : 0 - }, { - "key" : "whatsAppShare", - "value" : 0 - } ], - "displayable" : true, - "position" : 0, - "noEmbed" : false, - "analyticsData" : { - "ns_st_ep" : "On réunit des ex après leur rupture", - "ns_st_ty" : "Video", - "ns_st_ci" : "13360574", - "ns_st_el" : "902360", - "ns_st_cl" : "902360", - "ns_st_sl" : "902360", - "srg_mgeobl" : "false", - "ns_st_tp" : "1", - "ns_st_cn" : "1", - "ns_st_ct" : "vc12", - "ns_st_pn" : "1", - "ns_st_cdm" : "eo", - "ns_st_cmt" : "ec" - }, - "analyticsMetadata" : { - "media_segment" : "On réunit des ex après leur rupture", - "media_type" : "Video", - "media_segment_id" : "13360574", - "media_episode_length" : "902", - "media_segment_length" : "902", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "1", - "media_duration_category" : "long", - "media_is_geoblocked" : "false", - "media_is_web_only" : "true", - "media_production_source" : "produced.for.web", - "media_urn" : "urn:rts:video:13360574" - }, - "eventData" : "$27549332a83ca6ac$64b181b51953d6ed48de11986513e2f93922eb3d4315e6d5ad8189e1fe38d933c011ba7ded29e3d757ba1e566e76d65d97c8f0cd0735cc47b1cb3e5cf091c89c8d6c18ff31e19e3d7509cbf826c0c156fd10b8908ebe481aaf7282de102e92342ffb36b52df58453b40d64883f720fb3eddd38b595ddf6961acc4bc33abb3f2c49b7d90b52a35239f0209caa3ebc532e6a95315bd382bc08f2b78af2ec23c3f7e7917de924cb7f85b8aedac2fdafd027fe3880e07f3a0ba05f43d0ce601a1d2c7b756012c8820e12eef32fb9c0e1f532cce31cf1be738a9d6c05555857700fc5e1f0e1bd9886f06c55f5e731a66daa09be035e5ef53a4da159a7d3943a67ebaa1ac1302ad3ff046739eb185d78737e1543e7788d4edd9858af0e6846460106e954e8f1176cf60876aad36646c11a3b3a824ab54433f99c4576accea86e2b853c", - "resourceList" : [ { - "url" : "https://rts-vod-amd.akamaized.net/ww/13360574/447e0958-42a8-3bdd-8365-95d54031e605/master.m3u8", - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-vod-amd.akamaized.net/ww/13360574/447e0958-42a8-3bdd-8365-95d54031e605/master.m3u8" - } - } ], - "aspectRatio" : "16:9", - "spriteSheet" : { - "urn" : "urn:rts:video:13360574", - "rows" : 23, - "columns" : 20, - "thumbnailHeight" : 84, - "thumbnailWidth" : 150, - "interval" : 2000, - "url" : "https://il.srgssr.ch/spritesheet/urn/rts/video/13360574/sprite-13360574.jpeg" - } - } ], - "topicList" : [ { - "id" : "59952", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:59952", - "title" : "Yadebat" - }, { - "id" : "54537", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:54537", - "title" : "Tataki" - } ], - "analyticsData" : { - "srg_pr_id" : "13360565", - "srg_plid" : "10174267", - "ns_st_pl" : "Yadebat", - "ns_st_pr" : "Yadebat du 05.09.2022", - "ns_st_dt" : "2022-09-05", - "ns_st_ddt" : "2022-09-05", - "ns_st_tdt" : "*null", - "ns_st_tm" : "*null", - "ns_st_tep" : "500418168", - "ns_st_li" : "0", - "ns_st_stc" : "0867", - "ns_st_st" : "RTS Online", - "ns_st_tpr" : "10174267", - "ns_st_en" : "*null", - "ns_st_ge" : "*null", - "ns_st_ia" : "*null", - "ns_st_ce" : "1", - "ns_st_cdm" : "eo", - "ns_st_cmt" : "ec", - "srg_unit" : "RTS", - "srg_c1" : "full", - "srg_c2" : "video_tataki_yadebat", - "srg_c3" : "RTS.ch", - "srg_tv_id" : "500418168" - }, - "analyticsMetadata" : { - "media_episode_id" : "13360565", - "media_show_id" : "10174267", - "media_show" : "Yadebat", - "media_episode" : "Yadebat du 05.09.2022", - "media_is_livestream" : "false", - "media_full_length" : "full", - "media_enterprise_units" : "RTS", - "media_joker1" : "full", - "media_joker2" : "video_tataki_yadebat", - "media_joker3" : "RTS.ch", - "media_is_web_only" : "true", - "media_production_source" : "produced.for.web", - "media_tv_id" : "500418168", - "media_thumbnail" : "https://www.rts.ch/2022/09/05/18/20/13360573.image/16x9/scale/width/344", - "media_publication_date" : "2022-09-05", - "media_publication_time" : "16:30:00", - "media_publication_datetime" : "2022-09-05T16:30:00+02:00", - "media_content_group" : "Yadebat,Tataki", - "media_since_publication_d" : "0", - "media_since_publication_h" : "19" - } -} \ No newline at end of file diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_redundant.dataset/Contents.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_redundant.dataset/Contents.json deleted file mode 100644 index 414c11c3..00000000 --- a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_redundant.dataset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "data" : [ - { - "filename" : "urn_rts_video_13763072.json", - "idiom" : "universal", - "universal-type-identifier" : "public.json" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_redundant.dataset/urn_rts_video_13763072.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_redundant.dataset/urn_rts_video_13763072.json deleted file mode 100644 index c0f2de89..00000000 --- a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_redundant.dataset/urn_rts_video_13763072.json +++ /dev/null @@ -1,690 +0,0 @@ -{ - "chapterUrn" : "urn:rts:video:13763072", - "episode" : { - "id" : "13646015", - "title" : "19h30", - "publishedDate" : "2023-02-06T19:30:00+01:00", - "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763071.image/16x9", - "imageTitle" : "19h30 [RTS]" - }, - "show" : { - "id" : "105932", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:show:tv:105932", - "title" : "19h30", - "lead" : "L'édition du soir du téléjournal.", - "imageUrl" : "https://www.rts.ch/2019/08/28/11/33/10667272.image/16x9", - "imageTitle" : "RTS Info - Le 19h30, avec nouveau logo RTS Info (la mise en ligne le lundi 26 août 2019) [RTS]", - "bannerImageUrl" : "https://www.rts.ch/2019/08/28/11/33/10667272.image/3x1", - "posterImageUrl" : "https://www.rts.ch/2021/08/05/18/12/12396566.image/2x3", - "posterImageIsFallbackUrl" : false, - "primaryChannelId" : "143932a79bb5a123a646b68b1d1188d7ae493e5b", - "primaryChannelUrn" : "urn:rts:channel:tv:143932a79bb5a123a646b68b1d1188d7ae493e5b", - "availableAudioLanguageList" : [ { - "locale" : "fr", - "language" : "Français" - } ], - "availableVideoQualityList" : [ "SD", "HD" ], - "audioDescriptionAvailable" : false, - "subtitlesAvailable" : true, - "multiAudioLanguagesAvailable" : false, - "topicList" : [ { - "id" : "908", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:908", - "title" : "19h30" - }, { - "id" : "904", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:904", - "title" : "Vidéos" - }, { - "id" : "665", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:665", - "title" : "Info" - } ], - "allowIndexing" : true - }, - "channel" : { - "id" : "143932a79bb5a123a646b68b1d1188d7ae493e5b", - "vendor" : "RTS", - "urn" : "urn:rts:channel:tv:143932a79bb5a123a646b68b1d1188d7ae493e5b", - "title" : "RTS 1", - "imageUrl" : "https://www.rts.ch/2019/08/28/11/33/10667272.image/16x9", - "imageUrlRaw" : "https://il.srgssr.ch/image-service/dynamic/8eebe5.svg", - "imageTitle" : "RTS Info - Le 19h30, avec nouveau logo RTS Info (la mise en ligne le lundi 26 août 2019) [RTS]", - "transmission" : "TV" - }, - "chapterList" : [ { - "id" : "13763072", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13763072", - "title" : "19h30", - "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763071.image/16x9", - "imageTitle" : "19h30 [RTS]", - "type" : "EPISODE", - "date" : "2023-02-06T19:30:00+01:00", - "duration" : 1857560, - "validFrom" : "2023-02-06T20:05:19+01:00", - "playableAbroad" : true, - "socialCountList" : [ { - "key" : "srgView", - "value" : 13340 - }, { - "key" : "srgLike", - "value" : 0 - }, { - "key" : "fbShare", - "value" : 1 - }, { - "key" : "twitterShare", - "value" : 0 - }, { - "key" : "googleShare", - "value" : 0 - }, { - "key" : "whatsAppShare", - "value" : 34 - } ], - "displayable" : true, - "position" : 0, - "noEmbed" : false, - "analyticsData" : { - "ns_st_ep" : "19h30", - "ns_st_ty" : "Video", - "ns_st_ci" : "13763072", - "ns_st_el" : "1857560", - "ns_st_cl" : "1857560", - "ns_st_sl" : "1857560", - "srg_mgeobl" : "false", - "ns_st_tp" : "13", - "ns_st_cn" : "1", - "ns_st_ct" : "vc12", - "ns_st_pn" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc" - }, - "analyticsMetadata" : { - "media_segment" : "19h30", - "media_type" : "Video", - "media_segment_id" : "13763072", - "media_episode_length" : "1858", - "media_segment_length" : "1858", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "13", - "media_duration_category" : "long", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:13763072" - }, - "eventData" : "$9a6505bcccb854bf$a0ec7b2518b7ce6260492f932bbe32902090d850691731a263a076b740edfd09021e43de81ef4cbec8fd112788116eec867927a2eaea7aed9f5df592d48fed9209a004578d1192ac68df0f063ca108cee4c7e783890e1c1af04fdc95de08a3515919f0910f4804d6d0f6d90182f46894a40c6f254b132655c1d4e7c2a532312a9999a945b0d5edb4f21bbe1f7e7f12cc7a484b000984d395b8ac3f3222433004536c0a7233874ef4ae80cbc4d6f5dc3e9952a8ad986666021bb3b9849ae83b86b163cc7e0ef8617f7cfabac9d12e649ad4dc395a4f8c5e12ec9b865d7d1ae28802977ff0d268032cf7ef7209711b75459705353edf342f05f01a5dcf3853dfb2e46bb7adb5852fc6d9ca115877b3d08a22b3a822c751ee88b0c279dcdadf16604b3c7c73cb2f8e58156cd2de4b78cd1d6fe0f57b400088a5a892d365086f75e3ce0dcf35fe7af7bf6221a679b639ec8141ff5d019cbf4cb520663f95fd1d93c52c4edab3440fdfcb1d4a27b4d442f8cf", - "resourceList" : [ { - "url" : "https://rts-vod-amd.akamaized.net/ww/13763072/11def5d2-733d-3f82-bb0a-90492ff637d2/master.m3u8", - "quality" : "HD", - "protocol" : "HLS", - "encoding" : "H264", - "mimeType" : "application/x-mpegURL", - "presentation" : "DEFAULT", - "streaming" : "HLS", - "dvr" : false, - "live" : false, - "mediaContainer" : "FMP4", - "audioCodec" : "AAC", - "videoCodec" : "H264", - "tokenType" : "NONE", - "audioTrackList" : [ { - "locale" : "fr", - "language" : "Français", - "source" : "HLS" - } ], - "subtitleInformationList" : [ { - "locale" : "fr", - "language" : "Français (SDH)", - "source" : "HLS", - "type" : "SDH" - } ], - "analyticsData" : { - "srg_mqual" : "HD", - "srg_mpres" : "DEFAULT" - }, - "analyticsMetadata" : { - "media_streaming_quality" : "HD", - "media_special_format" : "DEFAULT", - "media_url" : "https://rts-vod-amd.akamaized.net/ww/13763072/11def5d2-733d-3f82-bb0a-90492ff637d2/master.m3u8" - } - } ], - "segmentList" : [ { - "id" : "13763046", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13763046", - "title" : "L'est de la Turquie dévasté par un séisme de 7,8 Le bilan pourrait atteindre plusieurs milliers de morts.", - "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763036.image/16x9", - "imageTitle" : "L'est de la Turquie dévasté par un séisme de 7,8 Le bilan pourrait atteindre plusieurs milliers de morts. [RTS]", - "type" : "CLIP", - "date" : "2023-02-06T19:30:00+01:00", - "duration" : 131840, - "validFrom" : "2023-02-06T20:05:19+01:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:13763072", - "position" : 1, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "L'est de la Turquie dévasté par un séisme de 7,8 Le bilan pourrait atteindre plusieurs milliers de morts.", - "media_type" : "Video", - "media_segment_id" : "13763046", - "media_episode_length" : "1858", - "media_segment_length" : "132", - "media_number_of_segment_selected" : "1", - "media_number_of_segments_total" : "13", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:13763046" - }, - "eventData" : "$b6e9570e7c7c008b$b4b2b89162a5acc3d5e1a659fcdc259315f6fd1b5a19421d31a9a21dc075474b61de92c4cfca39722266b88e2925cb5eb10604e19c9d21dd67ff275340d75dd0a18c55c053ab20150494704581ca4b4b6368f51a8abd08dd432b1a088564873db897ffc9309bec2b48278d935942394d1ac3cc7b08a8a447ec96de6ad790dfaf6473b176e75df4b2f7fd5c32fbc9a5edb0422be37476f62c8d842980a63b7555a1414fdec97a2a1f3617b4ac1fe37b071e01fc1f5056f6b2e8744155f50e46fa0cfe5c0e14a3d121e2da0f152ef9d69eb575d3785858bc207d9082e6dec5e7dfa0ac4c11602a275b6f9e4b5a74a813f2e1796915e45b75e37c1a88c5037e4f5a3b40781b5daac89cfe065b4b167f43068d5bcc1116cc39ec76beaa314ac488d3818e4d4589bbb13796cd7e981e240bcfd7db0cfe9cbb3272e0a3a0190f06356546f1e695f64ede1f7daac3ca0128895bf02bd1cfc576b71db7dcbc403b5cdee266fdc579a35ee688770323980a976e9a", - "markIn" : 101360, - "markOut" : 233200 - }, { - "id" : "13763048", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13763048", - "title" : "Tremblement de terre en Turquie et en Syrie : les explications de Mayalen de Castelbajac, correspondante en Turquie", - "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763047.image/16x9", - "imageTitle" : "Tremblement de terre en Turquie et en Syrie : les explications de Mayalen de Castelbajac, correspondante en Turquie [RTS]", - "type" : "CLIP", - "date" : "2023-02-06T19:30:00+01:00", - "duration" : 156840, - "validFrom" : "2023-02-06T20:05:19+01:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:13763072", - "position" : 2, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Tremblement de terre en Turquie et en Syrie : les explications de Mayalen de Castelbajac, correspondante en Turquie", - "media_type" : "Video", - "media_segment_id" : "13763048", - "media_episode_length" : "1858", - "media_segment_length" : "157", - "media_number_of_segment_selected" : "2", - "media_number_of_segments_total" : "13", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:13763048" - }, - "eventData" : "$16a882befb748245$5aee998923fd67269f5f08d0d1fadd76d80802de2d0a37a81b9a6717c04d4c0f3abe45a31eec7fd840e415b4f7754c8fa9cc6c9d2b151c54d4339fa875f8d9825111ae41e913a219f1d52a27e817949c4bef16015726f7e14940f1756909338ac60c9fe606a8fc03ea972d111a45c3c7ef29474d1d9a9d1804be0d79ca4aedbd50b7dfcadbca48e82eed99a5d8d7059d46c49f6fda33bb8aa075d11aaf1e76c8cfff482715b0804b6dafaa97e871a5ab8bbc1e9a00c0aa0b6048a739544ab53710afc9bad895836701d9cb63ee0f7c38b23a0a74ed545cfd91d1e925296c4e93ac6d9d5ef4f266491e660e036efd5e5da1b25e4bbd1d0f7e2e7cbdaae0dae09999c5df2cc2c5afa232ced5c3cf924a909e0703e5ae8fe1737cdb5e1b2a119c772b433e00a328b5f3896f67a127a2a16167b0ced53377fdeaca1e9d7738ee7d2aeaaf5a1d63e4960efa45823452ffbe44d8aa77d5bb9369b45ae3e4a584b30be93a13139d069bfe017e0ddf93463b0c4b", - "markIn" : 233200, - "markOut" : 390040 - }, { - "id" : "13763050", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13763050", - "title" : "En Syrie, le tremblement de terre qui a frappé le pays lundi a touché une région déjà ravagée par la guerre civile.", - "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763049.image/16x9", - "imageTitle" : "En Syrie, le tremblement de terre qui a frappé le pays lundi a touché une région déjà ravagée par la guerre civile. [RTS]", - "type" : "CLIP", - "date" : "2023-02-06T19:30:00+01:00", - "duration" : 119160, - "validFrom" : "2023-02-06T20:05:19+01:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:13763072", - "position" : 3, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "En Syrie, le tremblement de terre qui a frappé le pays lundi a touché une région déjà ravagée par la guerre civile.", - "media_type" : "Video", - "media_segment_id" : "13763050", - "media_episode_length" : "1858", - "media_segment_length" : "119", - "media_number_of_segment_selected" : "3", - "media_number_of_segments_total" : "13", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:13763050" - }, - "eventData" : "$00cf3e37b9871701$24ad456ebd36455798644ce1372a18e8d12d9d9b5accfa63ace5251bee2c67447b0aad1a07d0c881363adaf5ada12655ed9d8f36824338a57262908f068dfcbc58825092f9cf528fe435a7bc531700aab0215d3f59609e1217682de7fa93830afa0b82c561cec4ee006793772e56c18dbf64aa3009059dd1fcb0ae390a298d9b48ddb0995bf36aa7fa5a540eab7a81d814a9d6c35e4e2eaff212e6b32a16d2787d6d0a7ef8fb3c1b24d7f7039426b336e85ace49b973d37df9db3e4aa995f04ee5988b0dcae50fafe0cd567d0a5acbb37bd7697141013597f1edbb296ee9902faccb91d820b2c42411beae9d31158ade8999e129a3211951de6787ee88f123c9d4fc6ca6479700a60c53720f36d20234f69321813935acf74137b5fde0838a8552b46b03c5e7e6926f268c71f278a8b70725cdc491948de9200b0de5893df4da7a964ab8465b68847ab01b80fc06e120cd8d87250d62ebc9f71e20b880fe3fe4365029cd0677ee7aec2281b3cfeee538", - "markIn" : 390040, - "markOut" : 509200 - }, { - "id" : "13763052", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13763052", - "title" : "Tremblement de terre en Turquie et en Syrie : La Suisse a annoncé qu’elle allait envoyer des équipes et des chiens de sauvetage. Ils doivent s’envoler lundi soir pour la Turquie.", - "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763051.image/16x9", - "imageTitle" : "Tremblement de terre en Turquie et en Syrie : La Suisse a annoncé qu’elle allait envoyer des équipes et des chiens de sauvetage. Ils doivent s’envoler lundi soir pour la Turquie. [RTS]", - "type" : "CLIP", - "date" : "2023-02-06T19:30:00+01:00", - "duration" : 105080, - "validFrom" : "2023-02-06T20:05:19+01:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:13763072", - "position" : 4, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Tremblement de terre en Turquie et en Syrie : La Suisse a annoncé qu’elle allait envoyer des équipes et des chiens de sauvetage. Ils doivent s’envoler lundi soir pour la Turquie.", - "media_type" : "Video", - "media_segment_id" : "13763052", - "media_episode_length" : "1858", - "media_segment_length" : "105", - "media_number_of_segment_selected" : "4", - "media_number_of_segments_total" : "13", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:13763052" - }, - "eventData" : "$459879157b1c695b$dcdf6794f1f24da7e77c37e9debba2aa96d38384400cbf1db6a9b821d960c3b40b183a3392ccfd2860649c70e37dfc0feb55db43644a75386ffca3a115815e924028dcccbabfefa4722c3e20ac42f57dd7b6dfab5f6e630cd97c268d682a4fc10a2cd6ff00eba8e3da8488ca7fac957dd2f90e1c2ca93ec7fcbaa9e083026b50a1c3fbcf1751f0968ddc5f831c6ec638f8bf18fb8c33b325cf8953d2df6f2176cfa86323c79687955c6bc8ea1fd082a81f883d574f8a37d5d6442a754821bf1a31f97e8494c93ed8a4e03c21a1019988e282e64ad98eca4e1d2a49993817df6c99027de09f26eaa49faf8995c7358bbc9307a1a021ca2646f4748f442d33da0c5020fd19cc7f17defeef18becadbd8aafcffefc94747cd11fa9fce0fc96f0f7f251fea090c54b581a6547cb9d0c1509a2cc2b5ab402946d5f5cce429e2d0399c8e8f91b5f658914cf1e994549a3b1cc0856c165e2eb93050cdda86cafa42a31c9a12437f528fe6cfc81b9dc396b214a9", - "markIn" : 509200, - "markOut" : 614280 - }, { - "id" : "13763054", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13763054", - "title" : "Séisme en Turquie et en Syrie : Olivier Hagon, chef du groupe \"Santé\" du corps suisse d'aide humanitaire, revient sur l'importance d'une aide internationale immédiate", - "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763053.image/16x9", - "imageTitle" : "Séisme en Turquie et en Syrie : Olivier Hagon, chef du groupe \"Santé\" du corps suisse d'aide humanitaire, revient sur l'importance d'une aide internationale immédiate [RTS]", - "type" : "CLIP", - "date" : "2023-02-06T19:30:00+01:00", - "duration" : 149480, - "validFrom" : "2023-02-06T20:05:19+01:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:13763072", - "position" : 5, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Séisme en Turquie et en Syrie : Olivier Hagon, chef du groupe \"Santé\" du corps suisse d'aide humanitaire, revient sur l'importance d'une aide internationale immédiate", - "media_type" : "Video", - "media_segment_id" : "13763054", - "media_episode_length" : "1858", - "media_segment_length" : "149", - "media_number_of_segment_selected" : "5", - "media_number_of_segments_total" : "13", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:13763054" - }, - "eventData" : "$cb47ec31639040f6$4e51360d4cbc0a2dd71b960ed6935727f221e27fda235c10f508a6be94a473a66fa15f3e037a5ba3a5e5e99bad571dc30773c88fce53a5e8da1bb794e127230881e5beb9e4d012b4ee423f9722013f8d58574deaaf30c82e9549f8bfc757a9b613b1f573ab2b0984b37e08d975032ed117af8765a317a491e6df21efe2b3a120cc1f7759130afbea7aae2fc3a0bb1b273068fb1d33ab1271c03441518129b8f7861aad4ee68b19719c1553a01dc3d83ab8dbd18eea5578280b5833324f196e49f9897c8512071493e5c01b7c67a42079cb40a13a8c3d87ad6935840388391444b059a1891f9c0bee6565aebac761000a05fc4a6063a1aff5b3072e64744a547203fb859ada45fc65579657fba1336155ecb295d7a38a137756bbba1f4451eb4b4499d7806741e8ebcd15e4681f65f760b07b05c85490293d3063fa4aaa9a5ace0c879509669996155fe2bcd8544b05ebde74a2bc1a3cd9bf3de38d7ef073afe35feeab42c8c7eaf53c7cc447bce133e8", - "markIn" : 614280, - "markOut" : 763760 - }, { - "id" : "13763056", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13763056", - "title" : "Une délégation de parlementaires suisses a rendu visite à la Présidente taiwanaise.", - "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763055.image/16x9", - "imageTitle" : "Une délégation de parlementaires suisses a rendu visite à la Présidente taiwanaise. [RTS]", - "type" : "CLIP", - "date" : "2023-02-06T19:30:00+01:00", - "duration" : 110360, - "validFrom" : "2023-02-06T20:05:19+01:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:13763072", - "position" : 6, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Une délégation de parlementaires suisses a rendu visite à la Présidente taiwanaise.", - "media_type" : "Video", - "media_segment_id" : "13763056", - "media_episode_length" : "1858", - "media_segment_length" : "110", - "media_number_of_segment_selected" : "6", - "media_number_of_segments_total" : "13", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:13763056" - }, - "eventData" : "$57ef956cdc01fb68$1e76173b238e6d873218d416f667a7d312b45a5dea058751206f20fc6db32f6d02ac1a5fecfb6ddeaa7bac331fbb48e37a1b59dca16ddf947ded0e9fe0b7c77a50e5235f61a15e149457f2765284302a87b1379ee40e386e3a52aba94dc80ef80f7e421187c414eeb2a2a87cc4a880e5370e95098b822083d6d0371bc7d1cf27171490d0fb55fec43807fb0dc5b65bea0ff42f862f5475824d5df8443b1604e9e115bc6b3c6972733498579f641461c5f6c70d45d0cac604989623e105c981a359e509832bd307f21cc7bab262922fdb487160aa9a372138fdcada9c2bfcd95ecb4e30a0f10c9fb7c41da2767f54a0f0909804e76696739587371b77bc7c63d32bb820d47414adbc194441d38fc65e8072e90ec9b5d3500d6a7eb5767de11ba18554008cf9fe8c31710e9d03b6848e5ff4b7e92619433bea9a65f000ac3067b97e0554d975a863e0241c21fa730c721dbcaa9397f36f3e88e9eba30607bcf53bc6c1c0a8cefd6908382113552fd4b21c", - "markIn" : 776200, - "markOut" : 886560 - }, { - "id" : "13763058", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13763058", - "title" : "À Hong-Kong, alors que la plupart des opposants à Pékin ont été réduits au silence, quelques-uns résistent encore", - "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763057.image/16x9", - "imageTitle" : "À Hong-Kong, alors que la plupart des opposants à Pékin ont été réduits au silence, quelques-uns résistent encore [RTS]", - "type" : "CLIP", - "date" : "2023-02-06T19:30:00+01:00", - "duration" : 177720, - "validFrom" : "2023-02-06T20:05:19+01:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:13763072", - "position" : 7, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "À Hong-Kong, alors que la plupart des opposants à Pékin ont été réduits au silence, quelques-uns résistent encore", - "media_type" : "Video", - "media_segment_id" : "13763058", - "media_episode_length" : "1858", - "media_segment_length" : "178", - "media_number_of_segment_selected" : "7", - "media_number_of_segments_total" : "13", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:13763058" - }, - "eventData" : "$1a93330bbc15fad9$a604aa17bcf32efc7fb057fd07f1f8c7bac3b6925cd34e46a03f3f895b025dd320d415f97a6392be0d4e95d7f7351310c07f19a343834504ad59d1ec82c85905b0fd0c83185ce812bfe40d582ef9de50da2a2b747a3617052f1121181336eae0b6df069741128683556f64465c7515849ae1a705fe5e37bc912fad9c336fdf7c652c4f6a4d64c9010e259df08b6f605ff475f84c929933bff673dae4b72004c760409b3a5066145a338897c751f12678820b1876376b80c9eb005abb3062d696b1f5d856c8000fb2b7feb6439d047decf0ecedde3d74fcc684cb44de45b067b0196b99917164f8439c4d606b2d7e78a035a08b2c1b3b8d30915054f973e27df03abc3112769fffb0057145033738dc1dacda9fdc0a9fa0c59941e7955f2936b6557ddf05570312b43658a5254ad322401817f2023bc407297cfcf2c1c697fa72f995d005dbc2d71949edab436df50faa3a4c74b7c961ad7322dcdbac76587244f228965f6b4154ca0e821b842b8bc54a", - "markIn" : 916160, - "markOut" : 1093880 - }, { - "id" : "13763060", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13763060", - "title" : "La nouvelle carte d'identité suisse sera disponible dès le 3 mars prochain.", - "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763059.image/16x9", - "imageTitle" : "La nouvelle carte d'identité suisse sera disponible dès le 3 mars prochain. [RTS]", - "type" : "CLIP", - "date" : "2023-02-06T19:30:00+01:00", - "duration" : 79280, - "validFrom" : "2023-02-06T20:05:19+01:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:13763072", - "position" : 8, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "La nouvelle carte d'identité suisse sera disponible dès le 3 mars prochain.", - "media_type" : "Video", - "media_segment_id" : "13763060", - "media_episode_length" : "1858", - "media_segment_length" : "79", - "media_number_of_segment_selected" : "8", - "media_number_of_segments_total" : "13", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:13763060" - }, - "eventData" : "$c95a60ec09271449$74c6da37289f167a2751a1955cbc5d68462366528ebaae5f2d716c8e6809d3a93cff003b8dc4f5e7d931dc6d54b4ce33c65395cefbe99eed9483c6c48f84fb3ea69b83158653bb88bd229d5e816400a949aad113bbe7e0f0b7b4b35a5b50ad29be6f9fec4feaac7278ff990a0b9f234980096272cf7ce165c22ff17444012c6807326b97e9eb257dd962c0b0b6b547b2c50b2412506ace70e230e8dcb29607cab31a6640a8c2ff4493c8cc7e8eaf7332c54dd6a42dcbb825328e630eb1967bd62f36d0f13608f2ffd729feb62f1ca22fd416e32ee12d60e2ff73f3bd3f3dd64f49dd769d04e2d8bb8b0227723552d1d1275a26095ece3acc543f383700ab0806c24447d862ace9dee6eeb9228a5bb70998a486f2581b6b40e6c9f785ff68d9ee504223bae916d24ec87d204cb9e281d76897859a8c1207fc0aa8572adfceca3979abf4834605dada7950598df31045f6cad6b3780e83dafa51f4624f24d6552291351108201a93d38b739c21fa8ce717", - "markIn" : 1093880, - "markOut" : 1173160 - }, { - "id" : "13763062", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13763062", - "title" : "Vive controverse à Neuchâtel. L'Eglise réformée ne veut plus de cérémonies laïques dans ses temples.", - "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763061.image/16x9", - "imageTitle" : "Vive controverse à Neuchâtel. L'Eglise réformée ne veut plus de cérémonies laïques dans ses temples. [RTS]", - "type" : "CLIP", - "date" : "2023-02-06T19:30:00+01:00", - "duration" : 131520, - "validFrom" : "2023-02-06T20:05:19+01:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:13763072", - "position" : 9, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Vive controverse à Neuchâtel. L'Eglise réformée ne veut plus de cérémonies laïques dans ses temples.", - "media_type" : "Video", - "media_segment_id" : "13763062", - "media_episode_length" : "1858", - "media_segment_length" : "132", - "media_number_of_segment_selected" : "9", - "media_number_of_segments_total" : "13", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:13763062" - }, - "eventData" : "$fe5ff11ee266bee8$617478588c4826d351ae27d63c295aa6ecb7098c5aede3501f3eb710ce7338119fdb9fc91b88dd26503c23ee5decdc1752f22c695968dffcec3db8034f7b3a552a9f371800d6d1ec7926a834dae805ff90911adace6c570443209ed1d0baf130c98573c7c7235b1da9786928263a5ba5558ee76d21f5529d296491c4e3bfc9c0488dd9098000dbc492453b95da65d64ac87d589f0960d2810cabe922ed9bda080e5c2b13fbfea894dc0ba460172e4a6b21dd1ca17427b63f0f41156d7ffe1091636ecc1d162fff2888a7212cb424e4aeab7097681a4e7b61cd522f60fad3b740e38e5b52b4450fccca9539eb57d666ffabd5bf0fe1dcf99b0e15032d18f8fe27935a19679e8f5a6426af9fad82ff081943616fd8483e81dd17ae31a797dd6fa9554a52eec6793b222f9220e342651c64df65f4b50b17348233257a94832a33e7a0d502881f2ee1dadfc58ae584ef6c95c0116971aa91588d1992c89ab67caecde0989a4273aa5e1a49ef3329908904bf", - "markIn" : 1173160, - "markOut" : 1304680 - }, { - "id" : "13763064", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13763064", - "title" : "Michel Kocher, journaliste à RTSreligion, commente la décision de l'Église réformée neuchâteloise de ne plus accueillir de cérémonies laïques dans ses temples.", - "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763063.image/16x9", - "imageTitle" : "Michel Kocher, journaliste à RTSreligion, commente la décision de l'Église réformée neuchâteloise de ne plus accueillir de cérémonies laïques dans ses temples. [RTS]", - "type" : "CLIP", - "date" : "2023-02-06T19:30:00+01:00", - "duration" : 142600, - "validFrom" : "2023-02-06T20:05:19+01:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:13763072", - "position" : 10, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Michel Kocher, journaliste à RTSreligion, commente la décision de l'Église réformée neuchâteloise de ne plus accueillir de cérémonies laïques dans ses temples.", - "media_type" : "Video", - "media_segment_id" : "13763064", - "media_episode_length" : "1858", - "media_segment_length" : "143", - "media_number_of_segment_selected" : "10", - "media_number_of_segments_total" : "13", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:13763064" - }, - "eventData" : "$b4e9b17cb149766c$104065454cabe642af9c753e7e05443dfea2ca49820e1dc89e286604535af353ebe1c52da3eb2fef21689581e9727c535cd70b6842488e8f785ce2927465f966d73120a483c5e03e304dc7dcf91dfa23831085d275f49fe0139166e070ce6957c8eebf6f0767eb9449b6b5e6a6ae469f6a2c493dfaf176ef343819d62de8861183bedc43521ddb3497798f2fce203121aeab9d56ded99e8879f03e4de082786ec8b9209d7eb2e4f07123f610e226b380f39a83db742a78ac270ef27534e55adf294f436aee1e4b6de7164a48d12d6addff6f07ed5a714f66f00ea6efd6ca97171ecaf0a6bb0ae78f31dee52480ad8afad009ad1df0b99ee06571656bbb7f4c21469c874d941f8326579d9f9de593f09a490e6a4c57dbd0f9cf3a991533f6b0aa7371203e7263d4a07a1bfe151562ce9a48664548a74fb3d33bd42b08093a3b12ae53f1b0d390ed2ab26027346551ea4e03663069fe23363968350bc309070723f4146d626cfac7399940fea3f22282e1", - "markIn" : 1304680, - "markOut" : 1447280 - }, { - "id" : "13763066", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13763066", - "title" : "2 mois après l'entrée en vigueur de la \"Lex Booking\", notre enquête montre qu'il est avantageux de réserver son hôtel en direct.", - "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763065.image/16x9", - "imageTitle" : "2 mois après l'entrée en vigueur de la \"Lex Booking\", notre enquête montre qu'il est avantageux de réserver son hôtel en direct. [RTS]", - "type" : "CLIP", - "date" : "2023-02-06T19:30:00+01:00", - "duration" : 130560, - "validFrom" : "2023-02-06T20:05:19+01:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:13763072", - "position" : 11, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "2 mois après l'entrée en vigueur de la \"Lex Booking\", notre enquête montre qu'il est avantageux de réserver son hôtel en direct.", - "media_type" : "Video", - "media_segment_id" : "13763066", - "media_episode_length" : "1858", - "media_segment_length" : "131", - "media_number_of_segment_selected" : "11", - "media_number_of_segments_total" : "13", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:13763066" - }, - "eventData" : "$2ece6a538f114427$0e716d1d8e4a1cdd5fc88b1400fd4317d5674af7d51adc5ad25446c6953385c3799d71633e54d4a6b010c46e253fb4af972e56f993e9fced0362f7ea4fc1a32a892624bcb5ede4db30f4e18de68cde5267345a301e324aaa5f6fc606cf18c60f935a29d5d71a21845cb331ba9e434eff25e23830f367f3e5f7a12d55e2c3e9cada84b7689d7b4bb3326d781882f7e21d79a706a4ee07719aa2d3ddd415725dea049f630093af6ce1d68ff536418c36907f4ad57f631ef9ba9b1829f11f6bc8919ec133aedea7187e757adb3a96cdd05726cf705d622613413458e47f21dbcc491b5f82589135ecdd98de7565cc7912d25d1778299b50550f7670ac38740c71f3d69286f30a6d1e241e1183235a134e8a6816b4a9face4c1c72c81694225a708c8a6f7404ca6ceaf8782f97b548cac69dfb5e30075f5774d2e6133f5ae142dad60fe4cf6b2cf10bc5bfb786eba33b02e1a08578f0353ff44d5cbf6e9c71ca9e9807ca5b63cd11ca493eaaefbc3d197ccc", - "markIn" : 1447280, - "markOut" : 1577840 - }, { - "id" : "13763068", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13763068", - "title" : "Première journée au championnats du monde de ski et première médaille suisse avec l’argent de Wendy Holdener en combiné.", - "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763067.image/16x9", - "imageTitle" : "Première journée au championnats du monde de ski et première médaille suisse avec l’argent de Wendy Holdener en combiné. [RTS]", - "type" : "CLIP", - "date" : "2023-02-06T19:30:00+01:00", - "duration" : 119760, - "validFrom" : "2023-02-06T20:05:19+01:00", - "playableAbroad" : false, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:13763072", - "position" : 12, - "noEmbed" : true, - "analyticsMetadata" : { - "media_segment" : "Première journée au championnats du monde de ski et première médaille suisse avec l’argent de Wendy Holdener en combiné.", - "media_type" : "Video", - "media_segment_id" : "13763068", - "media_episode_length" : "1858", - "media_segment_length" : "120", - "media_number_of_segment_selected" : "12", - "media_number_of_segments_total" : "13", - "media_duration_category" : "short", - "media_is_geoblocked" : "true", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:13763068" - }, - "eventData" : "$b0e5010f0d20637f$3e77d084182f46934ad9ee16de6e15e80d37d5b528ed652e9b894d96a68b663e98eff6318fe88f9dbb8f52011572a40379bef3dcec053b35b0595bc91f75375c046fdca30575850a7fbca5d4221bd1ff95547d0ae81a058182d277c3704d03e72a7b6e9a022b9a246433341f3b31dd50d1304515ab0c5afc4e5c68d98566ec1c2522a629253f6294ef89db7eb28ac6c21bc88b19affb75ff5fd35a519fbb0bc5aab8236776203b200e9ea78b657500a6203fefb750e2f307fb0197b6fa40288361d44686aa7543bee9ba3471d9fd9ecd4dd80067108ab7ac943b8be9e4e210100c991334c68837e365bcb3b086cf063cd9db9eec0180c64bc6e834436f9367ae85ccf512cab3df1a61ccd9dffb72d9654b19b7ea6e375eda7fa25af868a9ef9d2d592fea7b04c13f0ee2e721c3756f80d887640505f383436f1c84680430ac3a92d5a68fc9a059eea55f2d130bf94d87e7b5b083e4c5494e5bf64a0fcb48c92ab49e892c01d95cd726cd7e08ffaca8a0", - "markIn" : 1581720, - "markOut" : 1701480 - }, { - "id" : "13763070", - "mediaType" : "VIDEO", - "vendor" : "RTS", - "urn" : "urn:rts:video:13763070", - "title" : "Michel Simonet, le célèbre balayeur fribourgeois, cumule 4 millions de vue sur Instagram avec une vidéo racontant son quotidien.", - "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763069.image/16x9", - "imageTitle" : "Michel Simonet, le célèbre balayeur fribourgeois, cumule 4 millions de vue sur Instagram avec une vidéo racontant son quotidien. [RTS]", - "type" : "CLIP", - "date" : "2023-02-06T19:30:00+01:00", - "duration" : 128800, - "validFrom" : "2023-02-06T20:05:19+01:00", - "playableAbroad" : true, - "displayable" : true, - "fullLengthUrn" : "urn:rts:video:13763072", - "position" : 13, - "noEmbed" : false, - "analyticsMetadata" : { - "media_segment" : "Michel Simonet, le célèbre balayeur fribourgeois, cumule 4 millions de vue sur Instagram avec une vidéo racontant son quotidien.", - "media_type" : "Video", - "media_segment_id" : "13763070", - "media_episode_length" : "1858", - "media_segment_length" : "129", - "media_number_of_segment_selected" : "13", - "media_number_of_segments_total" : "13", - "media_duration_category" : "short", - "media_is_geoblocked" : "false", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_urn" : "urn:rts:video:13763070" - }, - "eventData" : "$4c2cb6787916e0cf$7656f4a41f1c6fa32d08561727846b3bb5351df71cbb9ee961ae30c670a6a17cabe57da7e6f183f391290366827ce367208076430e6de3593c8ef9a39588edc4d7faaac91cc9d7433f01a8e3e98366a39b4f312fb237046101672c15d441d82b798a5ea698d2358485def04e8e66e32dd835bfcba7ce04611b1787e29e7bb0f2f0f15ee75b5d6ba756154913c2a50752d89d2bacdfa85398f694031cd39549c74db85fed1562d9b86daf0c8da686a56f89782b4d7786631cbb5993a45aab579fa2b0b5f3fb7de747dcfffefcc544cb3bf3d147861ed8d026262b07810b775603c7040b6521fdc5b33fbc5a9ec5e078e6112272ae73f4bd97c9a7f367bc6ec718239994397aabac1787800819a8adefc349e4b92438fcaf316959f8e8dcdde47cae695cfdc3ae7d124d2de58925eb313894d1acf5ef693b4627e588f4bd494ce7ece42436cce6b5de66dfdb234389d5743a9b843bc25ff8234f80ad7d15646e066c4f26f76a097137d3b0f9fd137365c6", - "markIn" : 1701480, - "markOut" : 1830280 - } ], - "aspectRatio" : "16:9", - "spriteSheet" : { - "urn" : "urn:rts:video:13763072", - "rows" : 24, - "columns" : 20, - "thumbnailHeight" : 84, - "thumbnailWidth" : 150, - "interval" : 4000, - "url" : "https://il.srgssr.ch/spritesheet/urn/rts/video/13763072/sprite-13763072.jpeg" - } - } ], - "topicList" : [ { - "id" : "908", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:908", - "title" : "19h30" - }, { - "id" : "904", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:904", - "title" : "Vidéos" - }, { - "id" : "665", - "vendor" : "RTS", - "transmission" : "TV", - "urn" : "urn:rts:topic:tv:665", - "title" : "Info" - } ], - "analyticsData" : { - "srg_pr_id" : "13646015", - "srg_plid" : "105932", - "ns_st_pl" : "19h30", - "ns_st_pr" : "19h30 du 06.02.2023", - "ns_st_dt" : "2023-02-06", - "ns_st_ddt" : "2023-02-06", - "ns_st_tdt" : "2023-02-06", - "ns_st_tm" : "19:30:00", - "ns_st_tep" : "500434867", - "ns_st_li" : "0", - "ns_st_stc" : "0867", - "ns_st_st" : "RTS Online", - "ns_st_tpr" : "105932", - "ns_st_en" : "*null", - "ns_st_ge" : "*null", - "ns_st_ia" : "*null", - "ns_st_ce" : "1", - "ns_st_cdm" : "to", - "ns_st_cmt" : "fc", - "srg_unit" : "RTS", - "srg_c1" : "full", - "srg_c2" : "video_info_journal-19h30", - "srg_c3" : "RTS 1", - "srg_tv_id" : "500434867" - }, - "analyticsMetadata" : { - "media_episode_id" : "13646015", - "media_show_id" : "105932", - "media_show" : "19h30", - "media_episode" : "19h30 du 06.02.2023", - "media_is_livestream" : "false", - "media_full_length" : "full", - "media_enterprise_units" : "RTS", - "media_joker1" : "full", - "media_joker2" : "video_info_journal-19h30", - "media_joker3" : "RTS 1", - "media_is_web_only" : "false", - "media_production_source" : "produced.for.broadcasting", - "media_tv_id" : "500434867", - "media_thumbnail" : "https://www.rts.ch/2023/02/06/21/06/13763071.image/16x9/scale/width/344", - "media_publication_date" : "2023-02-06", - "media_publication_time" : "20:05:19", - "media_publication_datetime" : "2023-02-06T20:05:19+01:00", - "media_tv_date" : "2023-02-06", - "media_tv_time" : "19:30:00", - "media_tv_datetime" : "2023-02-06T19:30:00+01:00", - "media_content_group" : "19h30,Vidéos,Info", - "media_channel_id" : "143932a79bb5a123a646b68b1d1188d7ae493e5b", - "media_channel_cs" : "0867", - "media_channel_name" : "RTS 1", - "media_since_publication_d" : "6", - "media_since_publication_h" : "163" - } -} \ No newline at end of file diff --git a/Tests/CoreTests/AccumulatePublisherTests.swift b/Tests/CoreTests/AccumulatePublisherTests.swift deleted file mode 100644 index c32a22ec..00000000 --- a/Tests/CoreTests/AccumulatePublisherTests.swift +++ /dev/null @@ -1,186 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Combine -import PillarboxCircumspect -import XCTest - -final class AccumulatePublisherTests: XCTestCase { - private static func publisher(at index: Int) -> AnyPublisher { - precondition(index > 0) - return Just(index) - .delay(for: .seconds(Double(index) * 0.1), scheduler: DispatchQueue.main) - .eraseToAnyPublisher() - } - - private static func prependedPublisher(at index: Int) -> AnyPublisher { - precondition(index > 0) - return Just(index) - .delay(for: .seconds(Double(index) * 0.1), scheduler: DispatchQueue.main) - .prepend(0) - .eraseToAnyPublisher() - } - - func testSyntaxWithoutTypeErasure() { - expectOnlyEqualPublished( - values: [ - [1, 2, 3] - ], - from: Publishers.AccumulateLatestMany( - Just(1), - Just(2), - Just(3) - ) - ) - } - - func testAccumulateOne() { - expectOnlyEqualPublished( - values: [ - [1] - ], - from: Publishers.AccumulateLatestMany( - Self.publisher(at: 1) - ) - ) - } - - func testAccumulateTwo() { - expectOnlyEqualPublished( - values: [ - [1, 2] - ], - from: Publishers.AccumulateLatestMany( - Self.publisher(at: 1), - Self.publisher(at: 2) - ) - ) - } - - func testAccumulateThree() { - expectOnlyEqualPublished( - values: [ - [1, 2, 3] - ], - from: Publishers.AccumulateLatestMany( - Self.publisher(at: 1), - Self.publisher(at: 2), - Self.publisher(at: 3) - ) - ) - } - - func testAccumulateFour() { - expectOnlyEqualPublished( - values: [ - [1, 2, 3, 4] - ], - from: Publishers.AccumulateLatestMany( - Self.publisher(at: 1), - Self.publisher(at: 2), - Self.publisher(at: 3), - Self.publisher(at: 4) - ) - ) - } - - func testAccumulateFive() { - expectOnlyEqualPublished( - values: [ - [1, 2, 3, 4, 5] - ], - from: Publishers.AccumulateLatestMany( - Self.publisher(at: 1), - Self.publisher(at: 2), - Self.publisher(at: 3), - Self.publisher(at: 4), - Self.publisher(at: 5) - ) - ) - } - - func testAccumulateOnePrepended() { - expectOnlyEqualPublished( - values: [ - [0], - [1] - ], - from: Publishers.AccumulateLatestMany( - Self.prependedPublisher(at: 1) - ) - ) - } - - func testAccumulateTwoPrepended() { - expectOnlyEqualPublished( - values: [ - [0, 0], - [1, 0], - [1, 2] - ], - from: Publishers.AccumulateLatestMany( - Self.prependedPublisher(at: 1), - Self.prependedPublisher(at: 2) - ) - ) - } - - func testAccumulateThreePrepended() { - expectOnlyEqualPublished( - values: [ - [0, 0, 0], - [1, 0, 0], - [1, 2, 0], - [1, 2, 3] - ], - from: Publishers.AccumulateLatestMany( - Self.prependedPublisher(at: 1), - Self.prependedPublisher(at: 2), - Self.prependedPublisher(at: 3) - ) - ) - } - - func testAccumulateFourPrepended() { - expectOnlyEqualPublished( - values: [ - [0, 0, 0, 0], - [1, 0, 0, 0], - [1, 2, 0, 0], - [1, 2, 3, 0], - [1, 2, 3, 4] - ], - from: Publishers.AccumulateLatestMany( - Self.prependedPublisher(at: 1), - Self.prependedPublisher(at: 2), - Self.prependedPublisher(at: 3), - Self.prependedPublisher(at: 4) - ) - ) - } - - func testAccumulateFivePrepended() { - expectOnlyEqualPublished( - values: [ - [0, 0, 0, 0, 0], - [1, 0, 0, 0, 0], - [1, 2, 0, 0, 0], - [1, 2, 3, 0, 0], - [1, 2, 3, 4, 0], - [1, 2, 3, 4, 5] - ], - from: Publishers.AccumulateLatestMany( - Self.prependedPublisher(at: 1), - Self.prependedPublisher(at: 2), - Self.prependedPublisher(at: 3), - Self.prependedPublisher(at: 4), - Self.prependedPublisher(at: 5) - ) - ) - } -} diff --git a/Tests/CoreTests/ArrayTests.swift b/Tests/CoreTests/ArrayTests.swift deleted file mode 100644 index d840ea30..00000000 --- a/Tests/CoreTests/ArrayTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Nimble -import PillarboxCircumspect -import XCTest - -final class ArrayTests: XCTestCase { - func testRemoveDuplicates() { - expect([1, 2, 3, 4].removeDuplicates()).to(equalDiff([1, 2, 3, 4])) - expect([1, 2, 1, 4].removeDuplicates()).to(equalDiff([1, 2, 4])) - } - - func testSafeIndex() { - expect([1, 2, 3][safeIndex: 0]).to(equal(1)) - expect([1, 2, 3][safeIndex: -1]).to(beNil()) - expect([1, 2, 3][safeIndex: 3]).to(beNil()) - } -} diff --git a/Tests/CoreTests/CombineLatestTests.swift b/Tests/CoreTests/CombineLatestTests.swift deleted file mode 100644 index b6f83f66..00000000 --- a/Tests/CoreTests/CombineLatestTests.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Combine -import PillarboxCircumspect -import XCTest - -final class CombineLatestPublisherTests: XCTestCase { - func testOutput5() { - expectOnlyEqualPublished( - values: [ - [1, 2, 3, 4, 5] - ], - from: Publishers.CombineLatest5( - Just(1), - Just(2), - Just(3), - Just(4), - Just(5) - ) - .map { [$0.0, $0.1, $0.2, $0.3, $0.4] } - ) - } - - func testOutput6() { - expectOnlyEqualPublished( - values: [ - [1, 2, 3, 4, 5, 6] - ], - from: Publishers.CombineLatest6( - Just(1), - Just(2), - Just(3), - Just(4), - Just(5), - Just(6) - ) - .map { [$0.0, $0.1, $0.2, $0.3, $0.4, $0.5] } - ) - } - - func testOutput7() { - expectOnlyEqualPublished( - values: [ - [1, 2, 3, 4, 5, 6, 7] - ], - from: Publishers.CombineLatest7( - Just(1), - Just(2), - Just(3), - Just(4), - Just(5), - Just(6), - Just(7) - ) - .map { [$0.0, $0.1, $0.2, $0.3, $0.4, $0.5, $0.6] } - ) - } -} diff --git a/Tests/CoreTests/ComparableTests.swift b/Tests/CoreTests/ComparableTests.swift deleted file mode 100644 index 6546fc69..00000000 --- a/Tests/CoreTests/ComparableTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Nimble -import XCTest - -final class ComparableTests: XCTestCase { - func testClamped() { - expect((-1).clamped(to: 0...1)).to(equal(0)) - expect(0.clamped(to: 0...1)).to(equal(0)) - expect(0.5.clamped(to: 0...1)).to(equal(0.5)) - expect(1.clamped(to: 0...1)).to(equal(1)) - expect(2.clamped(to: 0...1)).to(equal(1)) - } -} diff --git a/Tests/CoreTests/DemandBufferTests.swift b/Tests/CoreTests/DemandBufferTests.swift deleted file mode 100644 index 7b8386c6..00000000 --- a/Tests/CoreTests/DemandBufferTests.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Combine -import Nimble -import PillarboxCircumspect -import XCTest - -final class DemandBufferTests: XCTestCase { - func testEmptyBuffer() { - let buffer = DemandBuffer() - expect(buffer.values).to(beEmpty()) - expect(buffer.requested).to(equal(Subscribers.Demand.none)) - } - - func testPrefilledBuffer() { - let buffer: DemandBuffer = [1, 2] - expect(buffer.values).to(equalDiff([1, 2])) - } - - func testLimitedRequestWithEmptyBuffer() { - let buffer = DemandBuffer() - expect(buffer.request(.max(2))).to(beEmpty()) - expect(buffer.requested).to(equal(.max(2))) - } - - func testLimitedRequestWithPartiallyFilledBuffer() { - let buffer: DemandBuffer = [1, 2] - expect(buffer.request(.max(10))).to(equalDiff([1, 2])) - expect(buffer.requested).to(equal(.max(8))) - } - - func testLimitedRequestWithFullyFilledBuffer() { - let buffer: DemandBuffer = [1, 2, 3, 4] - expect(buffer.request(.max(2))).to(equalDiff([1, 2])) - expect(buffer.requested).to(equal(.max(0))) - expect(buffer.append(5)).to(beEmpty()) - } - - func testUnlimitedRequestWithEmptyBuffer() { - let buffer = DemandBuffer() - expect(buffer.request(.unlimited)).to(beEmpty()) - expect(buffer.requested).to(equal(.unlimited)) - } - - func testUnlimitedRequestWithFilledBuffer() { - let buffer: DemandBuffer = [1, 2] - expect(buffer.request(.unlimited)).to(equalDiff([1, 2])) - expect(buffer.requested).to(equal(.unlimited)) - } - - func testAppendWithPendingLimitedRequest() { - let buffer = DemandBuffer() - expect(buffer.request(.max(2))).to(beEmpty()) - expect(buffer.append(1)).to(equalDiff([1])) - expect(buffer.append(2)).to(equalDiff([2])) - expect(buffer.requested).to(equal(.max(0))) - expect(buffer.append(3)).to(beEmpty()) - } - - func testAppendWithPendingUnlimitedRequest() { - let buffer = DemandBuffer() - expect(buffer.request(.unlimited)).to(beEmpty()) - expect(buffer.append(1)).to(equalDiff([1])) - expect(buffer.append(2)).to(equalDiff([2])) - } - - func testThreadSafety() { - let buffer = DemandBuffer([0...1000]) - for _ in 0..<100 { - DispatchQueue.global().async { - _ = buffer.request(.unlimited) - } - } - } -} diff --git a/Tests/CoreTests/DispatchPublisherTests.swift b/Tests/CoreTests/DispatchPublisherTests.swift deleted file mode 100644 index 0cfd45a6..00000000 --- a/Tests/CoreTests/DispatchPublisherTests.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Combine -import Nimble -import PillarboxCircumspect -import XCTest - -final class DispatchPublisherTests: XCTestCase { - private var cancellables = Set() - - func testReceiveOnMainThreadFromMainThread() { - var value = 0 - Just(3) - .receiveOnMainThread() - .sink { i in - expect(Thread.isMainThread).to(beTrue()) - value = i - } - .store(in: &cancellables) - expect(value).to(equal(3)) - } - - func testReceiveOnMainThreadFromBackgroundThread() { - var value = 0 - Just(3) - .receive(on: DispatchQueue(label: "com.srgssr.pillarbox-tests")) - .receiveOnMainThread() - .sink { i in - expect(Thread.isMainThread).to(beTrue()) - value = i - } - .store(in: &cancellables) - expect(value).to(equal(0)) - } - - func testStandardReceiveOnMainThreadFromMainThread() { - var value = 0 - Just(3) - .receive(on: DispatchQueue.main) - .sink { i in - expect(Thread.isMainThread).to(beTrue()) - value = i - } - .store(in: &cancellables) - expect(value).to(equal(0)) - } - - func testStandardReceiveOnMainThreadFromBackgroundThread() { - var value = 0 - Just(3) - .receive(on: DispatchQueue(label: "com.srgssr.pillarbox-tests")) - .receive(on: DispatchQueue.main) - .sink { i in - expect(Thread.isMainThread).to(beTrue()) - value = i - } - .store(in: &cancellables) - expect(value).to(equal(0)) - } - - func testReceiveOnMainThreadReceivesAllOutputFromMainThread() { - let publisher = [1, 2, 3].publisher - .receiveOnMainThread() - expectOnlyEqualPublished(values: [1, 2, 3], from: publisher) - } - - func testReceiveOnMainThreadReceivesAllOutputFromBackgroundThreads() { - let publisher = [1, 2, 3].publisher - .receive(on: DispatchQueue(label: "com.srgssr.pillarbox-tests")) - .receiveOnMainThread() - expectOnlyEqualPublished(values: [1, 2, 3], from: publisher) - } - - func testDelayIfNeededOutputOrderingWithNonZeroDelay() { - let delayedPublisher = [1, 2, 3].publisher - .delayIfNeeded(for: 0.1, scheduler: DispatchQueue.main) - let subject = CurrentValueSubject(0) - expectEqualPublished(values: [0, 1, 2, 3], from: Publishers.Merge(delayedPublisher, subject), during: .milliseconds(100)) - } - - func testDelayIfNeededOutputOrderingWithZeroDelay() { - let delayedPublisher = [1, 2, 3].publisher - .delayIfNeeded(for: 0, scheduler: DispatchQueue.main) - let subject = CurrentValueSubject(0) - expectEqualPublished(values: [1, 2, 3, 0], from: Publishers.Merge(delayedPublisher, subject), during: .milliseconds(100)) - } -} diff --git a/Tests/CoreTests/LimitedBufferTests.swift b/Tests/CoreTests/LimitedBufferTests.swift deleted file mode 100644 index 0267be95..00000000 --- a/Tests/CoreTests/LimitedBufferTests.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Nimble -import PillarboxCircumspect -import XCTest - -final class LimitedBufferTests: XCTestCase { - func testBufferWithZeroSize() { - let buffer = LimitedBuffer(size: 0) - expect(buffer.values).to(beEmpty()) - buffer.append(1) - expect(buffer.values).to(beEmpty()) - } - - func testBufferWithFiniteSize() { - let buffer = LimitedBuffer(size: 2) - expect(buffer.values).to(beEmpty()) - buffer.append(1) - expect(buffer.values).to(equalDiff([1])) - buffer.append(2) - expect(buffer.values).to(equalDiff([1, 2])) - buffer.append(3) - expect(buffer.values).to(equalDiff([2, 3])) - } -} diff --git a/Tests/CoreTests/MeasurePublisherTests.swift b/Tests/CoreTests/MeasurePublisherTests.swift deleted file mode 100644 index 505ff21f..00000000 --- a/Tests/CoreTests/MeasurePublisherTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Combine -import PillarboxCircumspect -import XCTest - -final class MeasurePublisherTests: XCTestCase { - func testWithSingleEvent() { - let publisher = Just(1) - .delay(for: .milliseconds(500), scheduler: DispatchQueue.main) - .measureDateInterval() - .map(\.duration) - expectPublished(values: [0.5], from: publisher, to: beClose(within: 0.1), during: .seconds(1)) - } - - func testWithMultipleEvents() { - let publisher = [1, 2].publisher - .delay(for: .milliseconds(500), scheduler: DispatchQueue.main) - .measureDateInterval() - .map(\.duration) - expectPublished(values: [0.5, 0], from: publisher, to: beClose(within: 0.1), during: .seconds(1)) - } - - func testWithoutEvents() { - let publisher = Empty() - .delay(for: .milliseconds(500), scheduler: DispatchQueue.main) - .measureDateInterval() - expectNothingPublished(from: publisher, during: .seconds(1)) - } -} diff --git a/Tests/CoreTests/NotificationPublisherDeallocationTests.swift b/Tests/CoreTests/NotificationPublisherDeallocationTests.swift deleted file mode 100644 index 9621f133..00000000 --- a/Tests/CoreTests/NotificationPublisherDeallocationTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Nimble -import PillarboxCircumspect -import XCTest - -final class NotificationPublisherDeallocationTests: XCTestCase { - func testReleaseWithObject() throws { - let notificationCenter = NotificationCenter.default - var object: TestObject? = TestObject() - let publisher = notificationCenter.weakPublisher(for: .testNotification, object: object).first() - - weak var weakObject = object - try autoreleasepool { - try waitForOutput(from: publisher) { - notificationCenter.post(name: .testNotification, object: object) - } - object = nil - } - expect(weakObject).to(beNil()) - } - - func testReleaseWithNSObject() throws { - let notificationCenter = NotificationCenter.default - var object: TestNSObject? = TestNSObject() - let publisher = notificationCenter.weakPublisher(for: .testNotification, object: object).first() - - weak var weakObject = object - try autoreleasepool { - try waitForOutput(from: publisher) { - notificationCenter.post(name: .testNotification, object: object) - } - object = nil - } - expect(weakObject).to(beNil()) - } -} diff --git a/Tests/CoreTests/NotificationPublisherTests.swift b/Tests/CoreTests/NotificationPublisherTests.swift deleted file mode 100644 index 83236880..00000000 --- a/Tests/CoreTests/NotificationPublisherTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Nimble -import PillarboxCircumspect -import XCTest - -final class NotificationPublisherTests: XCTestCase { - func testWithObject() throws { - let object = TestObject() - let notificationCenter = NotificationCenter.default - try waitForOutput(from: notificationCenter.weakPublisher(for: .testNotification, object: object).first()) { - notificationCenter.post(name: .testNotification, object: object) - } - } - - func testWithNSObject() throws { - let object = TestNSObject() - let notificationCenter = NotificationCenter.default - try waitForOutput(from: notificationCenter.weakPublisher(for: .testNotification, object: object).first()) { - notificationCenter.post(name: .testNotification, object: object) - } - } - - func testAfterObjectRelease() { - let notificationCenter = NotificationCenter.default - var object: TestObject? = TestObject() - let publisher = notificationCenter.weakPublisher(for: .testNotification, object: object).first() - - weak var weakObject = object - autoreleasepool { - object = nil - } - expect(weakObject).to(beNil()) - - // We were interested in notifications from `object` only. After its release we should not receive other - // notifications from any other source anymore. - expectNothingPublished(from: publisher, during: .seconds(1)) { - notificationCenter.post(name: .testNotification, object: nil) - } - } -} diff --git a/Tests/CoreTests/PublishAndRepeatOnOutputFromTests.swift b/Tests/CoreTests/PublishAndRepeatOnOutputFromTests.swift deleted file mode 100644 index 1f5a2aae..00000000 --- a/Tests/CoreTests/PublishAndRepeatOnOutputFromTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Combine -import PillarboxCircumspect -import XCTest - -final class PublishAndRepeatOnOutputFromTests: XCTestCase { - private let trigger = Trigger() - - func testNoSignal() { - let publisher = Publishers.PublishAndRepeat(onOutputFrom: Optional>.none) { - Just("out") - } - expectAtLeastEqualPublished(values: ["out"], from: publisher) - } - - func testInactiveSignal() { - let publisher = Publishers.PublishAndRepeat(onOutputFrom: trigger.signal(activatedBy: 1)) { - Just("out") - } - expectAtLeastEqualPublished(values: ["out"], from: publisher) - } - - func testActiveSignal() { - let publisher = Publishers.PublishAndRepeat(onOutputFrom: trigger.signal(activatedBy: 1)) { - Just("out") - } - expectAtLeastEqualPublished(values: ["out", "out"], from: publisher) { [trigger] in - trigger.activate(for: 1) - } - } -} diff --git a/Tests/CoreTests/PublishOnOutputFromTests.swift b/Tests/CoreTests/PublishOnOutputFromTests.swift deleted file mode 100644 index 68e5963e..00000000 --- a/Tests/CoreTests/PublishOnOutputFromTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Combine -import PillarboxCircumspect -import XCTest - -final class PublishOnOutputFromTests: XCTestCase { - private let trigger = Trigger() - - func testNoSignal() { - let publisher = Publishers.Publish(onOutputFrom: Optional>.none) { - Just("out") - } - expectNothingPublished(from: publisher, during: .seconds(1)) - } - - func testInactiveSignal() { - let publisher = Publishers.Publish(onOutputFrom: trigger.signal(activatedBy: 1)) { - Just("out") - } - expectNothingPublished(from: publisher, during: .seconds(1)) - } - - func testActiveSignal() { - let publisher = Publishers.Publish(onOutputFrom: trigger.signal(activatedBy: 1)) { - Just("out") - } - expectAtLeastEqualPublished(values: ["out"], from: publisher) { [trigger] in - trigger.activate(for: 1) - } - } -} diff --git a/Tests/CoreTests/RangeReplaceableCollectionTests.swift b/Tests/CoreTests/RangeReplaceableCollectionTests.swift deleted file mode 100644 index 4d23c50f..00000000 --- a/Tests/CoreTests/RangeReplaceableCollectionTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Nimble -import PillarboxCircumspect -import XCTest - -final class RangeReplaceableCollectionTests: XCTestCase { - func testMoveForward() { - var array = [1, 2, 3, 4, 5, 6, 7] - array.move(from: 2, to: 5) - expect(array).to(equalDiff([1, 2, 4, 5, 3, 6, 7])) - } - - func testMoveBackward() { - var array = [1, 2, 3, 4, 5, 6, 7] - array.move(from: 5, to: 2) - expect(array).to(equalDiff([1, 2, 6, 3, 4, 5, 7])) - } - - func testMoveToEnd() { - var array = [1, 2, 3, 4, 5, 6, 7] - array.move(from: 2, to: 7) - expect(array).to(equalDiff([1, 2, 4, 5, 6, 7, 3])) - } - - func testMoveSameItem() { - var array = [1, 2, 3, 4, 5, 6, 7] - array.move(from: 2, to: 2) - expect(array).to(equalDiff([1, 2, 3, 4, 5, 6, 7])) - } - - func testMoveFromInvalidIndex() { - guard nimbleThrowAssertionsAvailable() else { return } - var array = [1, 2, 3, 4, 5, 6, 7] - expect(array.move(from: -1, to: 2)).to(throwAssertion()) - expect(array.move(from: 8, to: 2)).to(throwAssertion()) - } - - func testMoveToInvalidIndex() { - guard nimbleThrowAssertionsAvailable() else { return } - var array = [1, 2, 3, 4, 5, 6, 7] - expect(array.move(from: 2, to: -1)).to(throwAssertion()) - expect(array.move(from: 2, to: 8)).to(throwAssertion()) - } -} diff --git a/Tests/CoreTests/ReplaySubjectTests.swift b/Tests/CoreTests/ReplaySubjectTests.swift deleted file mode 100644 index 6395e55c..00000000 --- a/Tests/CoreTests/ReplaySubjectTests.swift +++ /dev/null @@ -1,170 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Combine -import Nimble -import PillarboxCircumspect -import XCTest - -final class ReplaySubjectTests: XCTestCase { - func testEmptyBufferOfZero() { - let subject = ReplaySubject(bufferSize: 0) - expectNothingPublished(from: subject, during: .milliseconds(100)) - } - - func testEmptyBufferOfTwo() { - let subject = ReplaySubject(bufferSize: 2) - expectNothingPublished(from: subject, during: .milliseconds(100)) - } - - func testFilledBufferOfZero() { - let subject = ReplaySubject(bufferSize: 0) - subject.send(1) - expectNothingPublished(from: subject, during: .milliseconds(100)) - } - - func testFilledBufferOfTwo() { - let subject = ReplaySubject(bufferSize: 2) - subject.send(1) - subject.send(2) - subject.send(3) - expectEqualPublished(values: [2, 3], from: subject, during: .milliseconds(100)) - } - - func testNewValuesWithBufferOfZero() { - let subject = ReplaySubject(bufferSize: 0) - subject.send(1) - expectEqualPublished(values: [2, 3], from: subject, during: .milliseconds(100)) { - subject.send(2) - subject.send(3) - } - } - - func testNewValuesWithBufferOfTwo() { - let subject = ReplaySubject(bufferSize: 2) - subject.send(1) - subject.send(2) - subject.send(3) - expectEqualPublished(values: [2, 3, 4, 5], from: subject, during: .milliseconds(100)) { - subject.send(4) - subject.send(5) - } - } - - func testMultipleSubscribers() { - let subject = ReplaySubject(bufferSize: 2) - subject.send(1) - subject.send(2) - subject.send(3) - expectEqualPublished(values: [2, 3], from: subject, during: .milliseconds(100)) - expectEqualPublished(values: [2, 3], from: subject, during: .milliseconds(100)) - } - - func testSubscriptionRelease() { - let subject = ReplaySubject(bufferSize: 1) - subject.send(1) - - _ = subject.sink { _ in } - - expect(subject.subscriptions).to(beEmpty()) - } - - func testNewValuesWithMultipleSubscribers() { - let subject = ReplaySubject(bufferSize: 2) - subject.send(1) - subject.send(2) - subject.send(3) - expectEqualPublished(values: [2, 3, 4], from: subject, during: .milliseconds(100)) { - subject.send(4) - } - expectEqualPublished(values: [3, 4], from: subject, during: .milliseconds(100)) - } - - func testCompletion() { - let subject = ReplaySubject(bufferSize: 2) - expectOnlyEqualPublished(values: [1], from: subject) { - subject.send(1) - subject.send(completion: .finished) - } - } - - func testNoValueAfterCompletion() { - let subject = ReplaySubject(bufferSize: 2) - subject.send(1) - subject.send(completion: .finished) - subject.send(2) - expectEqualPublished(values: [1], from: subject, during: .milliseconds(100)) - } - - func testCompletionWithMultipleSubscribers() { - let subject = ReplaySubject(bufferSize: 2) - expectOnlyEqualPublished(values: [1], from: subject) { - subject.send(1) - subject.send(completion: .finished) - } - expectOnlyEqualPublished(values: [1], from: subject) - } - - func testRequestLessValuesThanAvailable() { - let subject = ReplaySubject(bufferSize: 3) - subject.send(1) - subject.send(2) - subject.send(3) - - var results = [Int]() - subject - .subscribe(AnySubscriber( - receiveSubscription: { subscription in - subscription.request(.max(2)) - }, - receiveValue: { value in - results.append(value) - return .none - }, - receiveCompletion: { _ in } - )) - expect(results).to(equalDiff([1, 2])) - } - - func testThreadSafety() { - let replaySubject = ReplaySubject(bufferSize: 3) - for i in 0..<100 { - DispatchQueue.global().async { - replaySubject.send(i) - } - } - } - - func testDeliveryOrderInRecursiveScenario() { - let subject = ReplaySubject(bufferSize: 1) - var cancellables = Set() - - var values: [String] = [] - - subject.sink { i in - values.append("A\(i)") - } - .store(in: &cancellables) - - subject.sink { i in - values.append("B\(i)") - if i == 1 { - subject.send(2) - } - } - .store(in: &cancellables) - - subject.sink { i in - values.append("C\(i)") - } - .store(in: &cancellables) - - subject.send(1) - expect(values).to(equalDiff(["A1", "B1", "A2", "B2", "C1", "C2"])) - } -} diff --git a/Tests/CoreTests/SlicePublisherTests.swift b/Tests/CoreTests/SlicePublisherTests.swift deleted file mode 100644 index 30caeca8..00000000 --- a/Tests/CoreTests/SlicePublisherTests.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Combine -import PillarboxCircumspect -import XCTest - -private struct Person: Equatable { - let firstName: String - let lastName: String -} - -final class SlicePublisherTests: XCTestCase { - func testDelivery() { - let publisher = [ - Person(firstName: "Jane", lastName: "Doe"), - Person(firstName: "Jane", lastName: "Smith"), - Person(firstName: "John", lastName: "Bridges") - ].publisher.slice(at: \.firstName) - expectEqualPublished(values: ["Jane", "John"], from: publisher) - } -} diff --git a/Tests/CoreTests/StopwatchTests.swift b/Tests/CoreTests/StopwatchTests.swift deleted file mode 100644 index 4da359cf..00000000 --- a/Tests/CoreTests/StopwatchTests.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Nimble -import XCTest - -final class StopwatchTests: XCTestCase { - func testCreation() { - let stopwatch = Stopwatch() - wait(for: .milliseconds(500)) - expect(stopwatch.time()).to(equal(0)) - } - - func testStart() { - let stopwatch = Stopwatch() - stopwatch.start() - wait(for: .milliseconds(500)) - expect(stopwatch.time() * 1000).to(beCloseTo(500, within: 100)) - } - - func testStartAndStop() { - let stopwatch = Stopwatch() - stopwatch.start() - wait(for: .milliseconds(200)) - stopwatch.stop() - wait(for: .milliseconds(200)) - expect(stopwatch.time() * 1000).to(beCloseTo(200, within: 100)) - } - - func testStopWithoutStart() { - let stopwatch = Stopwatch() - stopwatch.stop() - wait(for: .milliseconds(200)) - expect(stopwatch.time() * 1000).to(beCloseTo(0, within: 100)) - } - - func testReset() { - let stopwatch = Stopwatch() - stopwatch.start() - wait(for: .milliseconds(200)) - stopwatch.reset() - wait(for: .milliseconds(100)) - expect(stopwatch.time()).to(equal(0)) - } - - func testMultipleStarts() { - let stopwatch = Stopwatch() - stopwatch.start() - wait(for: .milliseconds(200)) - stopwatch.start() - wait(for: .milliseconds(200)) - expect(stopwatch.time() * 1000).to(beCloseTo(400, within: 100)) - } - - func testAccumulation() { - let stopwatch = Stopwatch() - stopwatch.start() - wait(for: .milliseconds(200)) - stopwatch.stop() - wait(for: .milliseconds(200)) - stopwatch.start() - wait(for: .milliseconds(200)) - expect(stopwatch.time() * 1000).to(beCloseTo(400, within: 100)) - } -} diff --git a/Tests/CoreTests/TimeTests.swift b/Tests/CoreTests/TimeTests.swift deleted file mode 100644 index f1c1ca18..00000000 --- a/Tests/CoreTests/TimeTests.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import CoreMedia -import Nimble -import XCTest - -final class TimeTests: XCTestCase { - func testCloseWithFiniteTimes() { - expect(CMTime.close(within: 0)(CMTime.zero, .zero)).to(beTrue()) - expect(CMTime.close(within: 0.5)(CMTime.zero, .zero)).to(beTrue()) - - expect(CMTime.close(within: 0.5)(CMTime(value: 2, timescale: 1), CMTime(value: 2, timescale: 1))).to(beTrue()) - expect(CMTime.close(within: 0.5)(CMTime(value: 2, timescale: 1), CMTime(value: 200, timescale: 100))).to(beTrue()) - expect(CMTime.close(within: 0.5)(CMTime.zero, CMTime(value: 1, timescale: 2))).to(beTrue()) - expect(CMTime.close(within: 0.5)(CMTime.zero, CMTime(value: 2, timescale: 1))).to(beFalse()) - - expect(CMTime.close(within: 0)(CMTime.zero, CMTime(value: 1, timescale: 10000))).to(beFalse()) - } - - func testCloseWithPositiveInfiniteValues() { - expect(CMTime.close(within: 0)(CMTime.positiveInfinity, .positiveInfinity)).to(beTrue()) - expect(CMTime.close(within: 0.5)(CMTime.positiveInfinity, .positiveInfinity)).to(beTrue()) - - expect(CMTime.close(within: 10000)(CMTime.positiveInfinity, .zero)).to(beFalse()) - expect(CMTime.close(within: 10000)(CMTime.positiveInfinity, .negativeInfinity)).to(beFalse()) - expect(CMTime.close(within: 10000)(CMTime.positiveInfinity, .indefinite)).to(beFalse()) - expect(CMTime.close(within: 10000)(CMTime.positiveInfinity, .invalid)).to(beFalse()) - } - - func testCloseWithMinusInfiniteValues() { - expect(CMTime.close(within: 0)(CMTime.negativeInfinity, .negativeInfinity)).to(beTrue()) - expect(CMTime.close(within: 0.5)(CMTime.negativeInfinity, .negativeInfinity)).to(beTrue()) - - expect(CMTime.close(within: 10000)(CMTime.negativeInfinity, .zero)).to(beFalse()) - expect(CMTime.close(within: 10000)(CMTime.negativeInfinity, .positiveInfinity)).to(beFalse()) - expect(CMTime.close(within: 10000)(CMTime.negativeInfinity, .indefinite)).to(beFalse()) - expect(CMTime.close(within: 10000)(CMTime.negativeInfinity, .invalid)).to(beFalse()) - } - - func testCloseWithIndefiniteValues() { - expect(CMTime.close(within: 0)(CMTime.indefinite, .indefinite)).to(beTrue()) - expect(CMTime.close(within: 0.5)(CMTime.indefinite, .indefinite)).to(beTrue()) - - expect(CMTime.close(within: 10000)(CMTime.indefinite, .zero)).to(beFalse()) - expect(CMTime.close(within: 10000)(CMTime.indefinite, .positiveInfinity)).to(beFalse()) - expect(CMTime.close(within: 10000)(CMTime.indefinite, .negativeInfinity)).to(beFalse()) - expect(CMTime.close(within: 10000)(CMTime.indefinite, .invalid)).to(beFalse()) - } - - func testCloseWithInvalidValues() { - expect(CMTime.close(within: 0)(CMTime.invalid, .invalid)).to(beTrue()) - expect(CMTime.close(within: 0.5)(CMTime.invalid, .invalid)).to(beTrue()) - - expect(CMTime.close(within: 10000)(CMTime.invalid, .zero)).to(beFalse()) - expect(CMTime.close(within: 10000)(CMTime.invalid, .positiveInfinity)).to(beFalse()) - expect(CMTime.close(within: 10000)(CMTime.invalid, .negativeInfinity)).to(beFalse()) - expect(CMTime.close(within: 10000)(CMTime.invalid, .indefinite)).to(beFalse()) - } - - func testTimeRangeIsValidAndNotEmpty() { - expect(CMTimeRange.invalid.isValidAndNotEmpty).to(beFalse()) - expect(CMTimeRange.zero.isValidAndNotEmpty).to(beFalse()) - expect(CMTimeRange( - start: CMTime(value: 1, timescale: 1), - end: CMTime(value: 1, timescale: 1) - ).isValidAndNotEmpty).to(beFalse()) - expect(CMTimeRange( - start: CMTime(value: 0, timescale: 1), - end: CMTime(value: 1, timescale: 1) - ).isValidAndNotEmpty).to(beTrue()) - } -} diff --git a/Tests/CoreTests/Tools.swift b/Tests/CoreTests/Tools.swift deleted file mode 100644 index 53b84ec1..00000000 --- a/Tests/CoreTests/Tools.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import Foundation - -final class TestNSObject: NSObject {} - -final class TestObject { - let identifier: String - - init(identifier: String = UUID().uuidString) { - self.identifier = identifier - } -} - -extension Notification.Name { - static let testNotification = Notification.Name("TestNotification") -} diff --git a/Tests/CoreTests/TriggerTests.swift b/Tests/CoreTests/TriggerTests.swift deleted file mode 100644 index 493f225d..00000000 --- a/Tests/CoreTests/TriggerTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Combine -import PillarboxCircumspect -import XCTest - -final class TriggerTests: XCTestCase { - func testInactive() { - let trigger = Trigger() - expectNothingPublished(from: trigger.signal(activatedBy: 1), during: .seconds(1)) - } - - func testActiveWithSignal() { - let trigger = Trigger() - expectAtLeastEqualPublished(values: ["out"], from: trigger.signal(activatedBy: 1).map { _ in "out" }) { - trigger.activate(for: 1) - } - } - - func testMultipleActivations() { - let trigger = Trigger() - expectAtLeastEqualPublished(values: ["out", "out"], from: trigger.signal(activatedBy: 1).map { _ in "out" }) { - trigger.activate(for: 1) - trigger.activate(for: 1) - } - } - - func testDifferentActivationIndex() { - let trigger = Trigger() - expectNothingPublished(from: trigger.signal(activatedBy: 1), during: .seconds(1)) { - trigger.activate(for: 2) - } - } - - func testHashableActivationIndex() { - let trigger = Trigger() - expectEqualPublished(values: ["out"], from: trigger.signal(activatedBy: "index").map { _ in "out" }, during: .seconds(1)) { - trigger.activate(for: "index") - } - } -} diff --git a/Tests/CoreTests/WaitPublisherTests.swift b/Tests/CoreTests/WaitPublisherTests.swift deleted file mode 100644 index d5fe9b8b..00000000 --- a/Tests/CoreTests/WaitPublisherTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Combine -import PillarboxCircumspect -import XCTest - -final class WaitPublisherTests: XCTestCase { - func testWait() { - let signal = PassthroughSubject() - - let publisher = Just("Received") - .wait(untilOutputFrom: signal) - expectNothingPublished(from: publisher, during: .milliseconds(100)) - - expectEqualPublished(values: ["Received"], from: publisher, during: .milliseconds(100)) { - signal.send(()) - } - } -} diff --git a/Tests/CoreTests/WeakCapturePublisherTests.swift b/Tests/CoreTests/WeakCapturePublisherTests.swift deleted file mode 100644 index 49b55c97..00000000 --- a/Tests/CoreTests/WeakCapturePublisherTests.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Combine -import Nimble -import XCTest - -final class WeakCapturePublisherTests: XCTestCase { - func testDeallocation() { - var object: TestObject? = TestObject() - let publisher = Just("output") - .weakCapture(object) - - weak var weakObject = object - autoreleasepool { - object = nil - } - expect(weakObject).to(beNil()) - - expectNothingPublished(from: publisher, during: .seconds(1)) - } - - func testDelivery() { - let object = TestObject(identifier: "weak_capture") - let publisher = Just("output") - .weakCapture(object, at: \.identifier) - expectAtLeastPublished( - values: [("output", "weak_capture")], - from: publisher - ) { output1, output2 in - output1.0 == output2.0 && output1.1 == output2.1 - } - } -} diff --git a/Tests/CoreTests/WithPreviousPublisherTests.swift b/Tests/CoreTests/WithPreviousPublisherTests.swift deleted file mode 100644 index 7cd92f10..00000000 --- a/Tests/CoreTests/WithPreviousPublisherTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxCore - -import Combine -import PillarboxCircumspect -import XCTest - -final class WithPreviousPublisherTests: XCTestCase { - func testEmpty() { - expectNothingPublished(from: Empty().withPrevious(), during: .seconds(1)) - } - - func testPreviousValues() { - expectAtLeastEqualPublished( - values: [nil, 1, 2, 3, 4], - from: (1...5).publisher.withPrevious().map(\.previous) - ) - } - - func testCurrentValues() { - expectAtLeastEqualPublished( - values: [1, 2, 3, 4, 5], - from: (1...5).publisher.withPrevious().map(\.current) - ) - } - - func testOptionalPreviousValues() { - expectAtLeastEqualPublished( - values: [-1, 1, 2, 3, 4], - from: (1...5).publisher.withPrevious(-1).map(\.previous) - ) - } - - func testOptionalCurrentValues() { - expectAtLeastEqualPublished( - values: [1, 2, 3, 4, 5], - from: (1...5).publisher.withPrevious(-1).map(\.current) - ) - } -} diff --git a/Tests/MonitoringTests/MetricHitExpectation.swift b/Tests/MonitoringTests/MetricHitExpectation.swift deleted file mode 100644 index 9b6f33ce..00000000 --- a/Tests/MonitoringTests/MetricHitExpectation.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxMonitoring - -private struct _MetricHitExpectation: MetricHitExpectation where Data: Encodable { - let eventName: EventName - private let evaluate: (MetricPayload) -> Void - - init(eventName: EventName, evaluate: @escaping (MetricPayload) -> Void) { - self.eventName = eventName - self.evaluate = evaluate - } - - func evaluate(_ data: MetricPayload) { - evaluate(data) - } -} - -protocol MetricHitExpectation { - associatedtype Data: Encodable - - var eventName: EventName { get } - - func evaluate(_ data: MetricPayload) -} - -private extension MetricHitExpectation { - func match(payload: any Encodable, with expectation: any MetricHitExpectation) -> Bool { - guard let payload = payload as? MetricPayload, payload.eventName == expectation.eventName else { - return false - } - evaluate(payload) - return true - } -} - -extension _MetricHitExpectation: CustomDebugStringConvertible { - var debugDescription: String { - eventName.rawValue - } -} - -func match(payload: any Encodable, with expectation: any MetricHitExpectation) -> Bool { - expectation.match(payload: payload, with: expectation) -} - -extension MonitoringTestCase { - func error(evaluate: @escaping (MetricPayload) -> Void = { _ in }) -> some MetricHitExpectation { - _MetricHitExpectation(eventName: .error, evaluate: evaluate) - } - - func heartbeat(evaluate: @escaping (MetricPayload) -> Void = { _ in }) -> some MetricHitExpectation { - _MetricHitExpectation(eventName: .heartbeat, evaluate: evaluate) - } - - func start(evaluate: @escaping (MetricPayload) -> Void = { _ in }) -> some MetricHitExpectation { - _MetricHitExpectation(eventName: .start, evaluate: evaluate) - } - - func stop(evaluate: @escaping (MetricPayload) -> Void = { _ in }) -> some MetricHitExpectation { - _MetricHitExpectation(eventName: .stop, evaluate: evaluate) - } -} diff --git a/Tests/MonitoringTests/MetricPayload.swift b/Tests/MonitoringTests/MetricPayload.swift deleted file mode 100644 index d18f3a8d..00000000 --- a/Tests/MonitoringTests/MetricPayload.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxMonitoring - -extension MetricPayload: CustomDebugStringConvertible { - public var debugDescription: String { - eventName.rawValue - } -} diff --git a/Tests/MonitoringTests/MetricsTracker.swift b/Tests/MonitoringTests/MetricsTracker.swift deleted file mode 100644 index 0a7a580d..00000000 --- a/Tests/MonitoringTests/MetricsTracker.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import Foundation -import PillarboxMonitoring - -extension MetricsTracker.Configuration { - static let test = MetricsTracker.Configuration( - serviceUrl: URL(string: "https://localhost/ingest")! - ) - - static let heartbeatTest = MetricsTracker.Configuration( - serviceUrl: URL(string: "https://localhost/ingest")!, - heartbeatInterval: 1 - ) -} - -extension MetricsTracker.Metadata { - static let test = MetricsTracker.Metadata( - identifier: "identifier", - metadataUrl: URL(string: "https://localhost/metadata.json"), - assetUrl: URL(string: "https://localhost/asset.m3u8") - ) -} diff --git a/Tests/MonitoringTests/MetricsTrackerTests.swift b/Tests/MonitoringTests/MetricsTrackerTests.swift deleted file mode 100644 index aa549333..00000000 --- a/Tests/MonitoringTests/MetricsTrackerTests.swift +++ /dev/null @@ -1,225 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxMonitoring - -import Nimble -import PillarboxCircumspect -import PillarboxPlayer -import PillarboxStreams -import XCTest - -final class MetricsTrackerTests: MonitoringTestCase { - func testEntirePlayback() { - let player = Player(item: .simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [ - MetricsTracker.adapter(configuration: .test) { _ in .test } - ] - )) - expectAtLeastHits( - start(), - heartbeat(), - stop { payload in - expect(payload.data.position).to(beCloseTo(1000, within: 100)) - } - ) { - player.play() - } - } - - func testError() { - let player = Player(item: .simple( - url: Stream.unavailable.url, - trackerAdapters: [ - MetricsTracker.adapter(configuration: .test) { _ in .test } - ] - )) - expectAtLeastHits( - start(), - error { payload in - let data = payload.data - expect(data.name).to(equal("NSURLErrorDomain(-1100)")) - expect(data.message).to(equal("The requested URL was not found on this server.")) - expect(data.position).to(beNil()) - expect(data.vpn).to(beFalse()) - } - ) { - player.play() - } - } - - func testNoStopWithoutStart() { - var player: Player? = Player(item: .simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [ - MetricsTracker.adapter(configuration: .test) { _ in .test } - ] - )) - _ = player - expectNoHits(during: .milliseconds(500)) { - player = nil - } - } - - func testHeartbeats() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - MetricsTracker.adapter(configuration: .heartbeatTest) { _ in .test } - ] - )) - expectAtLeastHits(start(), heartbeat(), heartbeat()) { - player.play() - } - } - - func testSessionIdentifierRenewalWhenReplayingAfterEnd() { - let player = Player(item: .simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [ - MetricsTracker.adapter(configuration: .test) { _ in .test } - ] - )) - var sessionId: String? - expectAtLeastHits( - start { payload in - sessionId = payload.sessionId - }, - heartbeat { payload in - expect(payload.sessionId).to(equal(sessionId)) - }, - stop { payload in - expect(payload.sessionId).to(equal(sessionId)) - } - ) { - player.play() - } - expectAtLeastHits( - start { payload in - expect(payload.sessionId).notTo(equal(sessionId)) - } - ) { - player.replay() - } - } - - func testSessionIdentifierRenewalWhenReplayingAfterFatalError() { - let player = Player(item: .simple( - url: Stream.unavailable.url, - trackerAdapters: [ - MetricsTracker.adapter(configuration: .test) { _ in .test } - ] - )) - var sessionId: String? - expectAtLeastHits( - start { payload in - sessionId = payload.sessionId - }, - error { payload in - expect(payload.sessionId).to(equal(sessionId)) - } - ) - expectAtLeastHits( - start { payload in - expect(payload.sessionId).notTo(equal(sessionId)) - } - ) { - player.replay() - } - } - - func testSessionIdentifierClearedAfterPlaybackEnd() { - let player = Player(item: .simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [ - MetricsTracker.adapter(configuration: .test) { _ in .test } - ] - )) - expectAtLeastHits( - start(), - heartbeat(), - stop() - ) { - player.play() - } - expect(player.currentSessionIdentifiers(trackedBy: MetricsTracker.self)).to(beEmpty()) - } - - func testSessionIdentifierPersistenceAfterFatalError() { - let player = Player(item: .simple( - url: Stream.unavailable.url, - trackerAdapters: [ - MetricsTracker.adapter(configuration: .test) { _ in .test } - ] - )) - var sessionId: String? - expectAtLeastHits( - start { payload in - sessionId = payload.sessionId - }, - error() - ) - expect(player.currentSessionIdentifiers(trackedBy: MetricsTracker.self)).to(equalDiff([sessionId!])) - } - - func testPayloads() { - let player = Player(item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - MetricsTracker.adapter(configuration: .test) { .test } - ] - )) - expectAtLeastHits( - start { payload in - expect(payload.version).to(equal(1)) - - let data = payload.data - - let device = data.device - expect(device.id).notTo(beNil()) - expect(device.model).notTo(beNil()) - expect(device.type).notTo(beNil()) - - let media = data.media - expect(media.assetUrl).to(equal(URL(string: "https://localhost/asset.m3u8"))) - expect(media.id).to(equal("identifier")) - expect(media.metadataUrl).to(equal(URL(string: "https://localhost/metadata.json"))) - expect(media.origin).notTo(beNil()) - - let os = data.os - expect(os.name).notTo(beNil()) - expect(os.version).notTo(beNil()) - - let player = data.player - expect(player.name).to(equal("Pillarbox")) - expect(player.version).to(equal(Player.version)) - }, - heartbeat { payload in - expect(payload.version).to(equal(1)) - - let data = payload.data - expect(data.airplay).to(beFalse()) - expect(data.streamType).to(equal("On-demand")) - } - ) { - player.play() - } - } - - func testRepeatOne() { - let player = Player(item: .simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [ - MetricsTracker.adapter(configuration: .test) { _ in .test } - ] - )) - player.repeatMode = .one - expectAtLeastHits(start(), heartbeat(), stop(), start(), heartbeat(), stop()) { - player.play() - } - } -} diff --git a/Tests/MonitoringTests/MonitoringTestCase.swift b/Tests/MonitoringTests/MonitoringTestCase.swift deleted file mode 100644 index c219fe26..00000000 --- a/Tests/MonitoringTests/MonitoringTestCase.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxMonitoring - -import Dispatch -import PillarboxCircumspect -import XCTest - -class MonitoringTestCase: XCTestCase {} - -extension MonitoringTestCase { - /// Collects metric hits during some time interval and matches them against expectations. - func expectHits( - _ expectations: any MetricHitExpectation..., - during interval: DispatchTimeInterval = .seconds(20), - file: StaticString = #file, - line: UInt = #line, - while executing: (() -> Void)? = nil - ) { - MetricHitListener.captureMetricHits { publisher in - expectPublished( - values: expectations, - from: publisher, - to: match(payload:with:), - during: interval, - file: file, - line: line, - while: executing - ) - } - } - - /// Expects metric hits during some time interval and matches them against expectations. - func expectAtLeastHits( - _ expectations: any MetricHitExpectation..., - timeout: DispatchTimeInterval = .seconds(20), - file: StaticString = #file, - line: UInt = #line, - while executing: (() -> Void)? = nil - ) { - MetricHitListener.captureMetricHits { publisher in - expectAtLeastPublished( - values: expectations, - from: publisher, - to: match(payload:with:), - timeout: timeout, - file: file, - line: line, - while: executing - ) - } - } - - /// Expects no metric hits during some time interval. - func expectNoHits( - during interval: DispatchTimeInterval = .seconds(20), - file: StaticString = #file, - line: UInt = #line, - while executing: (() -> Void)? = nil - ) { - MetricHitListener.captureMetricHits { publisher in - expectNothingPublished( - from: publisher, - during: interval, - file: file, - line: line, - while: executing - ) - } - } -} diff --git a/Tests/MonitoringTests/TrackingSessionTests.swift b/Tests/MonitoringTests/TrackingSessionTests.swift deleted file mode 100644 index 2032f791..00000000 --- a/Tests/MonitoringTests/TrackingSessionTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxMonitoring - -import Nimble -import XCTest - -final class TrackingSessionTests: XCTestCase { - func testEmpty() { - let session = TrackingSession() - expect(session.id).to(beNil()) - expect(session.isStarted).to(beFalse()) - } - - func testStart() { - var session = TrackingSession() - session.start() - expect(session.id).notTo(beNil()) - expect(session.isStarted).to(beTrue()) - } - - func testStop() { - var session = TrackingSession() - session.start() - session.stop() - expect(session.id).notTo(beNil()) - expect(session.isStarted).to(beFalse()) - } - - func testReset() { - var session = TrackingSession() - session.start() - session.reset() - expect(session.id).to(beNil()) - expect(session.isStarted).to(beFalse()) - } -} diff --git a/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatAllUpdateTests.swift b/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatAllUpdateTests.swift deleted file mode 100644 index e8f55d8d..00000000 --- a/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatAllUpdateTests.swift +++ /dev/null @@ -1,189 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble -import PillarboxCircumspect - -final class AVPlayerItemRepeatAllUpdateTests: TestCase { - func testPlayerItemsWithoutCurrentItem() { - let previousContents: [AssetContent] = [ - .test(id: "1"), - .test(id: "2"), - .test(id: "3"), - .test(id: "4"), - .test(id: "5") - ] - let currentContents: [AssetContent] = [ - .test(id: "A"), - .test(id: "B"), - .test(id: "C") - ] - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: nil, - repeatMode: .all, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("B"), UUID("C"), UUID("A")])) - } - - func testPlayerItemsWithPreservedCurrentItem() { - let currentItemContent = AssetContent.test(id: "3") - let previousContents: [AssetContent] = [ - .test(id: "1"), - .test(id: "2"), - currentItemContent, - .test(id: "4"), - .test(id: "5") - ] - let currentContents = [ - .test(id: "A"), - currentItemContent, - .test(id: "B"), - .test(id: "C") - ] - let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: currentItem, - repeatMode: .all, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("3"), UUID("B"), UUID("C"), UUID("A")])) - expect(items.first).to(equal(currentItem)) - } - - func testPlayerItemsWithPreservedCurrentItemAtEnd() { - let currentItemContent = AssetContent.test(id: "3") - let previousContents = [ - .test(id: "1"), - .test(id: "2"), - currentItemContent, - .test(id: "4"), - .test(id: "5") - ] - let currentContents = [ - .test(id: "A"), - .test(id: "B"), - .test(id: "C"), - currentItemContent - ] - let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: currentItem, - repeatMode: .all, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("3"), UUID("A")])) - expect(items.first).to(equal(currentItem)) - } - - func testPlayerItemsWithUnknownCurrentItem() { - let previousContents: [AssetContent] = [ - .test(id: "1"), - .test(id: "2") - ] - let currentContents: [AssetContent] = [ - .test(id: "A"), - .test(id: "B") - ] - let unknownItem = AssetContent.test(id: "1").playerItem(configuration: .default, limits: .none) - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: unknownItem, - repeatMode: .all, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("B"), UUID("A")])) - } - - func testPlayerItemsWithCurrentItemReplacedByAnotherItem() { - let currentItemContent = AssetContent.test(id: "1") - let otherContent = AssetContent.test(id: "2") - let previousContents = [ - currentItemContent, - otherContent, - .test(id: "3") - ] - let currentContents = [ - .test(id: "3"), - otherContent, - .test(id: "C") - ] - let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: currentItem, - repeatMode: .all, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("2"), UUID("C"), UUID("3")])) - } - - func testPlayerItemsWithUpdatedCurrentItem() { - let currentItemContent = AssetContent.test(id: "1") - let previousContents: [AssetContent] = [ - .test(id: "1"), - .test(id: "2"), - .test(id: "3") - ] - let currentContents = [ - currentItemContent, - .test(id: "2"), - .test(id: "3") - ] - let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: currentItem, - repeatMode: .all, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("1"), UUID("2"), UUID("3"), UUID("1")])) - expect(items.first).to(equal(currentItem)) - } - - func testPlayerItemsLength() { - let currentContents: [AssetContent] = [ - .test(id: "A"), - .test(id: "B"), - .test(id: "C"), - .test(id: "D") - ] - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: [], - currentItem: nil, - repeatMode: .all, - length: 2, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("B")])) - } -} diff --git a/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatOffUpdateTests.swift b/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatOffUpdateTests.swift deleted file mode 100644 index 8930e0bf..00000000 --- a/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatOffUpdateTests.swift +++ /dev/null @@ -1,189 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble -import PillarboxCircumspect - -final class AVPlayerItemRepeatOffUpdateTests: TestCase { - func testPlayerItemsWithoutCurrentItem() { - let previousContents: [AssetContent] = [ - .test(id: "1"), - .test(id: "2"), - .test(id: "3"), - .test(id: "4"), - .test(id: "5") - ] - let currentContents: [AssetContent] = [ - .test(id: "A"), - .test(id: "B"), - .test(id: "C") - ] - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: nil, - repeatMode: .off, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("B"), UUID("C")])) - } - - func testPlayerItemsWithPreservedCurrentItem() { - let currentItemContent = AssetContent.test(id: "3") - let previousContents: [AssetContent] = [ - .test(id: "1"), - .test(id: "2"), - currentItemContent, - .test(id: "4"), - .test(id: "5") - ] - let currentContents = [ - .test(id: "A"), - currentItemContent, - .test(id: "B"), - .test(id: "C") - ] - let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: currentItem, - repeatMode: .off, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("3"), UUID("B"), UUID("C")])) - expect(items.first).to(equal(currentItem)) - } - - func testPlayerItemsWithPreservedCurrentItemAtEnd() { - let currentItemContent = AssetContent.test(id: "3") - let previousContents = [ - .test(id: "1"), - .test(id: "2"), - currentItemContent, - .test(id: "4"), - .test(id: "5") - ] - let currentContents = [ - .test(id: "A"), - .test(id: "B"), - .test(id: "C"), - currentItemContent - ] - let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: currentItem, - repeatMode: .off, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("3")])) - expect(items.first).to(equal(currentItem)) - } - - func testPlayerItemsWithUnknownCurrentItem() { - let previousContents: [AssetContent] = [ - .test(id: "1"), - .test(id: "2") - ] - let currentContents: [AssetContent] = [ - .test(id: "A"), - .test(id: "B") - ] - let unknownItem = AssetContent.test(id: "1").playerItem(configuration: .default, limits: .none) - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: unknownItem, - repeatMode: .off, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("B")])) - } - - func testPlayerItemsWithCurrentItemReplacedByAnotherItem() { - let currentItemContent = AssetContent.test(id: "1") - let otherContent = AssetContent.test(id: "2") - let previousContents = [ - currentItemContent, - otherContent, - .test(id: "3") - ] - let currentContents = [ - .test(id: "3"), - otherContent, - .test(id: "C") - ] - let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: currentItem, - repeatMode: .off, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("2"), UUID("C")])) - } - - func testPlayerItemsWithUpdatedCurrentItem() { - let currentItemContent = AssetContent.test(id: "1") - let previousContents: [AssetContent] = [ - .test(id: "1"), - .test(id: "2"), - .test(id: "3") - ] - let currentContents = [ - currentItemContent, - .test(id: "2"), - .test(id: "3") - ] - let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: currentItem, - repeatMode: .off, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("1"), UUID("2"), UUID("3")])) - expect(items.first).to(equal(currentItem)) - } - - func testPlayerItemsLength() { - let currentContents: [AssetContent] = [ - .test(id: "A"), - .test(id: "B"), - .test(id: "C"), - .test(id: "D") - ] - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: [], - currentItem: nil, - repeatMode: .off, - length: 2, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("B")])) - } -} diff --git a/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatOneUpdateTests.swift b/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatOneUpdateTests.swift deleted file mode 100644 index 3c863d83..00000000 --- a/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatOneUpdateTests.swift +++ /dev/null @@ -1,189 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble -import PillarboxCircumspect - -final class AVPlayerItemRepeatOneUpdateTests: TestCase { - func testPlayerItemsWithoutCurrentItem() { - let previousContents: [AssetContent] = [ - .test(id: "1"), - .test(id: "2"), - .test(id: "3"), - .test(id: "4"), - .test(id: "5") - ] - let currentContents: [AssetContent] = [ - .test(id: "A"), - .test(id: "B"), - .test(id: "C") - ] - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: nil, - repeatMode: .one, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("A"), UUID("B"), UUID("C")])) - } - - func testPlayerItemsWithPreservedCurrentItem() { - let currentItemContent = AssetContent.test(id: "3") - let previousContents: [AssetContent] = [ - .test(id: "1"), - .test(id: "2"), - currentItemContent, - .test(id: "4"), - .test(id: "5") - ] - let currentContents = [ - .test(id: "A"), - currentItemContent, - .test(id: "B"), - .test(id: "C") - ] - let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: currentItem, - repeatMode: .one, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("3"), UUID("3"), UUID("B"), UUID("C")])) - expect(items.first).to(equal(currentItem)) - } - - func testPlayerItemsWithPreservedCurrentItemAtEnd() { - let currentItemContent = AssetContent.test(id: "3") - let previousContents = [ - .test(id: "1"), - .test(id: "2"), - currentItemContent, - .test(id: "4"), - .test(id: "5") - ] - let currentContents = [ - .test(id: "A"), - .test(id: "B"), - .test(id: "C"), - currentItemContent - ] - let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: currentItem, - repeatMode: .one, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("3"), UUID("3")])) - expect(items.first).to(equal(currentItem)) - } - - func testPlayerItemsWithUnknownCurrentItem() { - let previousContents: [AssetContent] = [ - .test(id: "1"), - .test(id: "2") - ] - let currentContents: [AssetContent] = [ - .test(id: "A"), - .test(id: "B") - ] - let unknownItem = AssetContent.test(id: "1").playerItem(configuration: .default, limits: .none) - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: unknownItem, - repeatMode: .one, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("A"), UUID("B")])) - } - - func testPlayerItemsWithCurrentItemReplacedByAnotherItem() { - let currentItemContent = AssetContent.test(id: "1") - let otherContent = AssetContent.test(id: "2") - let previousContents = [ - currentItemContent, - otherContent, - .test(id: "3") - ] - let currentContents = [ - .test(id: "3"), - otherContent, - .test(id: "C") - ] - let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: currentItem, - repeatMode: .one, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("2"), UUID("2"), UUID("C")])) - } - - func testPlayerItemsWithUpdatedCurrentItem() { - let currentItemContent = AssetContent.test(id: "1") - let previousContents: [AssetContent] = [ - .test(id: "1"), - .test(id: "2"), - .test(id: "3") - ] - let currentContents = [ - currentItemContent, - .test(id: "2"), - .test(id: "3") - ] - let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: previousContents, - currentItem: currentItem, - repeatMode: .one, - length: .max, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("1"), UUID("1"), UUID("2"), UUID("3")])) - expect(items.first).to(equal(currentItem)) - } - - func testPlayerItemsLength() { - let currentContents: [AssetContent] = [ - .test(id: "A"), - .test(id: "B"), - .test(id: "C"), - .test(id: "D") - ] - let items = AVPlayerItem.playerItems( - for: currentContents, - replacing: [], - currentItem: nil, - repeatMode: .one, - length: 2, - configuration: .default, - limits: .none - ) - expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("A")])) - } -} diff --git a/Tests/PlayerTests/AVPlayer/AVPlayerItemTests.swift b/Tests/PlayerTests/AVPlayer/AVPlayerItemTests.swift deleted file mode 100644 index 07de1710..00000000 --- a/Tests/PlayerTests/AVPlayer/AVPlayerItemTests.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble -import PillarboxStreams - -final class AVPlayerItemTests: TestCase { - func testNonLoadedItem() { - let item = AVPlayerItem(url: Stream.onDemand.url) - expect(item.timeRange).toAlways(equal(.invalid), until: .seconds(1)) - } - - func testOnDemand() { - let item = AVPlayerItem(url: Stream.onDemand.url) - _ = AVPlayer(playerItem: item) - expect(item.timeRange).toEventuallyNot(equal(.invalid)) - } - - func testPlayerItemsWithRepeatOff() { - let items = [ - PlayerItem.simple(url: Stream.onDemand.url), - PlayerItem.simple(url: Stream.shortOnDemand.url), - PlayerItem.simple(url: Stream.live.url) - ] - expect { - AVPlayerItem.playerItems( - from: items, - after: 0, - repeatMode: .off, - length: .max, - reload: false, - configuration: .default, - limits: .none - ) - .compactMap(\.url) - } - .toEventually(equal([ - Stream.onDemand.url, - Stream.shortOnDemand.url, - Stream.live.url - ])) - } - - func testPlayerItemsWithRepeatOne() { - let items = [ - PlayerItem.simple(url: Stream.onDemand.url), - PlayerItem.simple(url: Stream.shortOnDemand.url), - PlayerItem.simple(url: Stream.live.url) - ] - expect { - AVPlayerItem.playerItems( - from: items, - after: 0, - repeatMode: .one, - length: .max, - reload: false, - configuration: .default, - limits: .none - ) - .compactMap(\.url) - } - .toEventually(equal([ - Stream.onDemand.url, - Stream.onDemand.url, - Stream.shortOnDemand.url, - Stream.live.url - ])) - } - - func testPlayerItemsWithRepeatAll() { - let items = [ - PlayerItem.simple(url: Stream.onDemand.url), - PlayerItem.simple(url: Stream.shortOnDemand.url), - PlayerItem.simple(url: Stream.live.url) - ] - expect { - AVPlayerItem.playerItems( - from: items, - after: 0, - repeatMode: .all, - length: .max, - reload: false, - configuration: .default, - limits: .none - ) - .compactMap(\.url) - } - .toEventually(equal([ - Stream.onDemand.url, - Stream.shortOnDemand.url, - Stream.live.url, - Stream.onDemand.url - ])) - } -} diff --git a/Tests/PlayerTests/AVPlayer/AVPlayerTests.swift b/Tests/PlayerTests/AVPlayer/AVPlayerTests.swift deleted file mode 100644 index d7a7351e..00000000 --- a/Tests/PlayerTests/AVPlayer/AVPlayerTests.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble -import PillarboxStreams - -final class AVPlayerTests: TestCase { - func testTimeRangeWhenEmpty() { - let player = AVPlayer() - expect(player.timeRange).to(equal(.invalid)) - } - - func testTimeRangeForOnDemand() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = AVPlayer(playerItem: item) - expect(player.timeRange).toEventually(equal(CMTimeRange(start: .zero, duration: Stream.onDemand.duration))) - } - - func testDurationWhenEmpty() { - let player = AVPlayer() - expect(player.duration).to(equal(.invalid)) - } - - func testDurationForOnDemand() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = AVPlayer(playerItem: item) - expect(player.duration).to(equal(.invalid)) - expect(player.duration).toEventually(equal(Stream.onDemand.duration)) - } - - func testDurationForLive() { - let item = AVPlayerItem(url: Stream.live.url) - let player = AVPlayer(playerItem: item) - expect(player.duration).to(equal(.invalid)) - expect(player.duration).toEventually(equal(.indefinite)) - } - - func testDurationForDvr() { - let item = AVPlayerItem(url: Stream.dvr.url) - let player = AVPlayer(playerItem: item) - expect(player.duration).to(equal(.invalid)) - expect(player.duration).toEventually(equal(.indefinite)) - } -} diff --git a/Tests/PlayerTests/Asset/AssetCreationTests.swift b/Tests/PlayerTests/Asset/AssetCreationTests.swift deleted file mode 100644 index 6e6def6b..00000000 --- a/Tests/PlayerTests/Asset/AssetCreationTests.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class AssetCreationTests: TestCase { - func testSimpleAsset() { - let asset = Asset.simple(url: Stream.onDemand.url) - expect(asset.resource).to(equal(.simple(url: Stream.onDemand.url))) - } - - func testCustomAsset() { - let delegate = ResourceLoaderDelegateMock() - let asset = Asset.custom(url: Stream.onDemand.url, delegate: delegate) - expect(asset.resource).to(equal(.custom(url: Stream.onDemand.url, delegate: delegate))) - } - - func testEncryptedAsset() { - let delegate = ContentKeySessionDelegateMock() - let asset = Asset.encrypted(url: Stream.onDemand.url, delegate: delegate) - expect(asset.resource).to(equal(.encrypted(url: Stream.onDemand.url, delegate: delegate))) - } -} diff --git a/Tests/PlayerTests/Asset/AssetMetadataMock.swift b/Tests/PlayerTests/Asset/AssetMetadataMock.swift deleted file mode 100644 index 37943c2f..00000000 --- a/Tests/PlayerTests/Asset/AssetMetadataMock.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import PillarboxPlayer - -struct AssetMetadataMock: Decodable { - let title: String - let subtitle: String? - - init(title: String, subtitle: String? = nil) { - self.title = title - self.subtitle = subtitle - } -} - -extension AssetMetadataMock: AssetMetadata { - var playerMetadata: PlayerMetadata { - .init(title: title, subtitle: subtitle) - } -} diff --git a/Tests/PlayerTests/Asset/ResourceItemTests.swift b/Tests/PlayerTests/Asset/ResourceItemTests.swift deleted file mode 100644 index 78521d4c..00000000 --- a/Tests/PlayerTests/Asset/ResourceItemTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import PillarboxCircumspect -import PillarboxStreams - -final class ResourceItemTests: TestCase { - func testNativePlayerItem() { - let item = Resource.simple(url: Stream.onDemand.url).playerItem(configuration: .default, limits: .none) - _ = AVPlayer(playerItem: item) - expectAtLeastEqualPublished( - values: [false, true], - from: item.publisher(for: \.isPlaybackLikelyToKeepUp) - ) - } - - func testLoadingPlayerItem() { - let item = Resource.loading.playerItem(configuration: .default, limits: .none) - _ = AVPlayer(playerItem: item) - expectAtLeastEqualPublished( - values: [false], - from: item.publisher(for: \.isPlaybackLikelyToKeepUp) - ) - } - - func testFailingPlayerItem() { - let item = Resource.failing(error: StructError()).playerItem(configuration: .default, limits: .none) - _ = AVPlayer(playerItem: item) - expectEqualPublished( - values: [.unknown], - from: item.statusPublisher(), - during: .seconds(1) - ) - } -} diff --git a/Tests/PlayerTests/AudioSession/AVAudioSessionNotificationTests.swift b/Tests/PlayerTests/AudioSession/AVAudioSessionNotificationTests.swift deleted file mode 100644 index b0e5489c..00000000 --- a/Tests/PlayerTests/AudioSession/AVAudioSessionNotificationTests.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFAudio -import Nimble - -final class AVAudioSessionNotificationTests: TestCase { - override func setUp() { - AVAudioSession.enableUpdateNotifications() - try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, policy: .default, options: []) - } - - func testUpdateWithSetCategoryModePolicyOptions() throws { - let audioSession = AVAudioSession.sharedInstance() - expect { - try audioSession.setCategory(.playback, mode: .default, policy: .default, options: [.duckOthers]) - }.to(postNotifications(equal([ - Notification(name: .didUpdateAudioSessionOptions, object: audioSession) - ]))) - } - - func testNoUpdateWithSetCategoryModePolicyOptions() throws { - let audioSession = AVAudioSession.sharedInstance() - expect { - try audioSession.setCategory(.playback, mode: .default, policy: .default, options: []) - }.notTo(postNotifications(equal([ - Notification(name: .didUpdateAudioSessionOptions, object: audioSession) - ]))) - } - - func testUpdateWithSetCategoryModeOptions() throws { - let audioSession = AVAudioSession.sharedInstance() - expect { - try audioSession.setCategory(.playback, mode: .default, options: [.duckOthers]) - }.to(postNotifications(equal([ - Notification(name: .didUpdateAudioSessionOptions, object: audioSession) - ]))) - } - - func testNoUpdateWithSetCategoryModeOptions() throws { - let audioSession = AVAudioSession.sharedInstance() - expect { - try audioSession.setCategory(.playback, mode: .default, options: []) - }.notTo(postNotifications(equal([ - Notification(name: .didUpdateAudioSessionOptions, object: audioSession) - ]))) - } - - func testUpdateWithSetCategoryOptions() throws { - let audioSession = AVAudioSession.sharedInstance() - expect { - try audioSession.setCategory(.playback, options: [.duckOthers]) - }.to(postNotifications(equal([ - Notification(name: .didUpdateAudioSessionOptions, object: audioSession) - ]))) - } - - func testNoUpdateWithSetCategoryOptions() throws { - let audioSession = AVAudioSession.sharedInstance() - expect { - try audioSession.setCategory(.playback, options: []) - }.notTo(postNotifications(equal([ - Notification(name: .didUpdateAudioSessionOptions, object: audioSession) - ]))) - } -} diff --git a/Tests/PlayerTests/Extensions/AVPlayerItem.swift b/Tests/PlayerTests/Extensions/AVPlayerItem.swift deleted file mode 100644 index 31d5751d..00000000 --- a/Tests/PlayerTests/Extensions/AVPlayerItem.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import AVFoundation - -extension AVPlayerItem { - var url: URL? { - (asset as? AVURLAsset)?.url - } -} diff --git a/Tests/PlayerTests/Extensions/AssetContent.swift b/Tests/PlayerTests/Extensions/AssetContent.swift deleted file mode 100644 index 51d39a87..00000000 --- a/Tests/PlayerTests/Extensions/AssetContent.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Foundation -import PillarboxStreams - -extension AssetContent { - static func test(id: Character) -> Self { - AssetContent(id: UUID(id), resource: .simple(url: Stream.onDemand.url), metadata: .empty, configuration: .default, dateInterval: nil) - } -} diff --git a/Tests/PlayerTests/Extensions/MetricEvent.swift b/Tests/PlayerTests/Extensions/MetricEvent.swift deleted file mode 100644 index 8294d7ff..00000000 --- a/Tests/PlayerTests/Extensions/MetricEvent.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -private struct AnyError: Error {} - -extension MetricEvent { - static let anyMetadata = Self(kind: .metadata(experience: .init(), service: .init())) - static let anyAsset = Self(kind: .asset(experience: .init())) - static let anyFailure = Self(kind: .failure(AnyError())) - static let anyWarning = Self(kind: .warning(AnyError())) -} diff --git a/Tests/PlayerTests/Extensions/Player.swift b/Tests/PlayerTests/Extensions/Player.swift deleted file mode 100644 index 667f9715..00000000 --- a/Tests/PlayerTests/Extensions/Player.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Foundation - -extension Player { - var urls: [URL] { - queuePlayer.items().compactMap(\.url) - } -} diff --git a/Tests/PlayerTests/Extensions/UUID.swift b/Tests/PlayerTests/Extensions/UUID.swift deleted file mode 100644 index 5c30bf11..00000000 --- a/Tests/PlayerTests/Extensions/UUID.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import Foundation - -extension UUID { - init(_ char: Character) { - self.init( - uuidString: """ - \(String(repeating: char, count: 8))\ - -\(String(repeating: char, count: 4))\ - -\(String(repeating: char, count: 4))\ - -\(String(repeating: char, count: 4))\ - -\(String(repeating: char, count: 12)) - """ - )! - } -} diff --git a/Tests/PlayerTests/MediaSelection/AVMediaSelectionGroupTests.swift b/Tests/PlayerTests/MediaSelection/AVMediaSelectionGroupTests.swift deleted file mode 100644 index d6f2d98e..00000000 --- a/Tests/PlayerTests/MediaSelection/AVMediaSelectionGroupTests.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble - -final class AVMediaSelectionGroupTests: TestCase { - func testPreferredMediaSelectionOptionsWithCharacteristics() { - let options: [AVMediaSelectionOptionMock] = [ - .init(displayName: "Option 1 (music)", languageCode: "fr", characteristics: [.describesMusicAndSoundForAccessibility]), - .init(displayName: "Option 2", languageCode: "fr", characteristics: []), - .init(displayName: "Option 3 (music)", languageCode: "en", characteristics: [.describesMusicAndSoundForAccessibility]), - .init(displayName: "Option 4", languageCode: "it", characteristics: []) - ] - - expect( - AVMediaSelectionGroup.preferredMediaSelectionOptions( - from: options, - withMediaCharacteristics: [.describesMusicAndSoundForAccessibility] - ) - .map(\.displayName) - .sorted() - ) - .to(equal([ - "Option 1 (music)", - "Option 3 (music)", - "Option 4" - ])) - } - - func testPreferredMediaSelectionOptionsWithoutCharacteristics() { - let options: [AVMediaSelectionOptionMock] = [ - .init(displayName: "Option 1 (music)", languageCode: "fr", characteristics: [.describesMusicAndSoundForAccessibility]), - .init(displayName: "Option 2", languageCode: "fr", characteristics: []), - .init(displayName: "Option 3 (music)", languageCode: "en", characteristics: [.describesMusicAndSoundForAccessibility]), - .init(displayName: "Option 4", languageCode: "it", characteristics: []) - ] - - expect( - AVMediaSelectionGroup.preferredMediaSelectionOptions( - from: options, - withoutMediaCharacteristics: [.describesMusicAndSoundForAccessibility] - ) - .map(\.displayName) - .sorted() - ) - .to(equal([ - "Option 2", - "Option 3 (music)", - "Option 4" - ])) - } -} diff --git a/Tests/PlayerTests/MediaSelection/AVMediaSelectionOptionTests.swift b/Tests/PlayerTests/MediaSelection/AVMediaSelectionOptionTests.swift deleted file mode 100644 index 870c8b51..00000000 --- a/Tests/PlayerTests/MediaSelection/AVMediaSelectionOptionTests.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble - -final class AVMediaSelectionOptionTests: TestCase { - func testSortedOptions() { - let option1 = AVMediaSelectionOptionMock(displayName: "English") - let option2 = AVMediaSelectionOptionMock(displayName: "French") - expect(option1 < option2).to(beTrue()) - } - - func testEqualOptions() { - let option1 = AVMediaSelectionOptionMock(displayName: "English") - let option2 = AVMediaSelectionOptionMock(displayName: "English") - expect(option1 < option2).to(beFalse()) - } - - func testSortedOptionsWithOriginal() { - let option1 = AVMediaSelectionOptionMock(displayName: "English") - let option2 = AVMediaSelectionOptionMock(displayName: "French", characteristics: [.isOriginalContent]) - expect(option2 < option1).to(beTrue()) - } -} diff --git a/Tests/PlayerTests/MediaSelection/MediaSelectionTests.swift b/Tests/PlayerTests/MediaSelection/MediaSelectionTests.swift deleted file mode 100644 index f4a8a097..00000000 --- a/Tests/PlayerTests/MediaSelection/MediaSelectionTests.swift +++ /dev/null @@ -1,297 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class MediaSelectionTests: TestCase { - func testCharacteristicsAndOptionsWhenEmpty() { - let player = Player() - expect(player.mediaSelectionCharacteristics).toAlways(beEmpty(), until: .seconds(2)) - expect(player.mediaSelectionOptions(for: .audible)).to(beEmpty()) - expect(player.mediaSelectionOptions(for: .legible)).to(beEmpty()) - expect(player.mediaSelectionOptions(for: .visual)).to(beEmpty()) - } - - func testCharacteristicsAndOptionsWhenAvailable() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - expect(player.mediaSelectionCharacteristics).toEventually(equal([.audible, .legible])) - expect(player.mediaSelectionOptions(for: .audible)).notTo(beEmpty()) - expect(player.mediaSelectionOptions(for: .legible)).notTo(beEmpty()) - expect(player.mediaSelectionOptions(for: .visual)).to(beEmpty()) - } - - func testCharacteristicsAndOptionsWhenFailed() { - let player = Player(item: .simple(url: Stream.unavailable.url)) - expect(player.mediaSelectionCharacteristics).toAlways(beEmpty(), until: .seconds(2)) - expect(player.mediaSelectionOptions(for: .audible)).to(beEmpty()) - expect(player.mediaSelectionOptions(for: .legible)).to(beEmpty()) - expect(player.mediaSelectionOptions(for: .visual)).to(beEmpty()) - } - - func testCharacteristicsAndOptionsWhenExhausted() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - expect(player.mediaSelectionCharacteristics).toEventuallyNot(beEmpty()) - player.play() - expect(player.mediaSelectionCharacteristics).toEventually(beEmpty()) - } - - func testCharacteristicsAndOptionsWhenUnavailable() { - let player = Player(item: .simple(url: Stream.onDemandWithoutOptions.url)) - expect(player.mediaSelectionCharacteristics).toAlways(beEmpty(), until: .seconds(2)) - expect(player.mediaSelectionOptions(for: .audible)).to(beEmpty()) - expect(player.mediaSelectionOptions(for: .legible)).to(beEmpty()) - expect(player.mediaSelectionOptions(for: .visual)).to(beEmpty()) - } - - func testCharacteristicsAndOptionsUpdateWhenAdvancingToNextItem() { - let player = Player(items: [ - .simple(url: Stream.onDemandWithOptions.url), - .simple(url: Stream.onDemandWithoutOptions.url) - ]) - expect(player.mediaSelectionCharacteristics).toEventuallyNot(beEmpty()) - player.advanceToNextItem() - expect(player.mediaSelectionCharacteristics).toEventually(beEmpty()) - } - - func testSingleAudibleOptionIsNeverReturned() { - let player = Player(item: .simple(url: Stream.onDemandWithSingleAudibleOption.url)) - expect(player.mediaSelectionCharacteristics).toEventually(equal([.audible])) - expect(player.mediaSelectionOptions(for: .audible)).to(beEmpty()) - } - - func testLegibleOptionsMustNotContainForcedSubtitles() { - let player = Player(item: .simple(url: Stream.onDemandWithForcedAndUnforcedLegibleOptions.url)) - expect(player.mediaSelectionCharacteristics).toEventually(equal([.audible, .legible])) - expect(player.mediaSelectionOptions(for: .legible)).to(haveCount(6)) - } - - func testInitialAudibleOption() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - expect(player.selectedMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("en")) - expect(player.currentMediaOption(for: .audible)).to(haveLanguageIdentifier("en")) - } - - func testInitialLegibleOptionWithAlwaysOnAccessibilityDisplayType() { - MediaAccessibilityDisplayType.alwaysOn(languageCode: "ja").apply() - - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - expect(player.selectedMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("ja")) - expect(player.currentMediaOption(for: .legible)).to(haveLanguageIdentifier("ja")) - } - - func testInitialLegibleOptionWithAutomaticAccessibilityDisplayType() { - MediaAccessibilityDisplayType.automatic.apply() - - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.automatic)) - expect(player.currentMediaOption(for: .legible)).to(equal(.off)) - } - - func testInitialLegibleOptionWithForcedOnlyAccessibilityDisplayType() { - MediaAccessibilityDisplayType.forcedOnly.apply() - - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.off)) - expect(player.currentMediaOption(for: .legible)).to(equal(.off)) - } - - func testInitialAudibleOptionWithoutAvailableOptions() { - let player = Player(item: .simple(url: Stream.onDemandWithoutOptions.url)) - expect(player.selectedMediaOption(for: .audible)).toAlways(equal(.off), until: .seconds(2)) - expect(player.currentMediaOption(for: .audible)).to(equal(.off)) - } - - func testInitialLegibleOptionWithoutAvailableOptions() { - MediaAccessibilityDisplayType.forcedOnly.apply() - - let player = Player(item: .simple(url: Stream.onDemandWithoutOptions.url)) - expect(player.selectedMediaOption(for: .legible)).toAlways(equal(.off), until: .seconds(2)) - expect(player.currentMediaOption(for: .legible)).to(equal(.off)) - } - - func testAudibleOptionUpdateWhenAdvancingToNextItem() { - let player = Player(items: [ - .simple(url: Stream.onDemandWithOptions.url), - .simple(url: Stream.onDemandWithoutOptions.url) - ]) - expect(player.selectedMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("en")) - player.advanceToNextItem() - expect(player.selectedMediaOption(for: .audible)).toEventually(equal(.off)) - } - - func testLegibleOptionUpdateWhenAdvancingToNextItem() { - MediaAccessibilityDisplayType.alwaysOn(languageCode: "fr").apply() - - let player = Player(items: [ - .simple(url: Stream.onDemandWithOptions.url), - .simple(url: Stream.onDemandWithoutOptions.url) - ]) - expect(player.selectedMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("fr")) - player.advanceToNextItem() - expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.off)) - } - - // When using AirPlay the receiver might offer forced subtitle selection, thus changing subtitles externally. In - // this case the perceived selected option must be `.off`. - @MainActor - func testLegibleOptionStaysOffEvenIfForcedSubtitlesAreEnabledExternally() async throws { - MediaAccessibilityDisplayType.alwaysOn(languageCode: "ja").apply() - - let player = Player(item: .simple(url: Stream.onDemandWithForcedAndUnforcedLegibleOptions.url)) - await expect(player.mediaSelectionOptions(for: .legible)).toEventuallyNot(beEmpty()) - - let group = try await player.group(for: .legible)! - let option = AVMediaSelectionGroup.mediaSelectionOptions( - from: group.options, - withMediaCharacteristics: [.containsOnlyForcedSubtitles] - ) - .first { option in - option.languageIdentifier == "ja" - }! - - // Simulates an external change using the low-level player API directly. - player.systemPlayer.currentItem?.select(option, in: group) - - await expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.off)) - } - - func testSelectAudibleOnOption() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - expect(player.mediaSelectionOptions(for: .audible)).toEventuallyNot(beEmpty()) - - player.select(mediaOption: player.mediaSelectionOptions(for: .audible).first { option in - option.languageIdentifier == "fr" - }!, for: .audible) - expect(player.selectedMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("fr")) - expect(player.currentMediaOption(for: .audible)).to(haveLanguageIdentifier("fr")) - } - - func testSelectAudibleAutomaticOptionDoesNothing() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - expect(player.mediaSelectionOptions(for: .audible)).toEventuallyNot(beEmpty()) - - player.select(mediaOption: .automatic, for: .audible) - expect(player.selectedMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("en")) - expect(player.currentMediaOption(for: .audible)).to(haveLanguageIdentifier("en")) - } - - func testSelectAudibleOffOptionDoesNothing() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - expect(player.mediaSelectionOptions(for: .audible)).toEventuallyNot(beEmpty()) - - player.select(mediaOption: .off, for: .audible) - expect(player.selectedMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("en")) - expect(player.currentMediaOption(for: .audible)).to(haveLanguageIdentifier("en")) - } - - func testSelectLegibleOnOption() { - MediaAccessibilityDisplayType.forcedOnly.apply() - - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - expect(player.mediaSelectionOptions(for: .legible)).toEventuallyNot(beEmpty()) - - player.select(mediaOption: player.mediaSelectionOptions(for: .legible).first { option in - option.languageIdentifier == "ja" - }!, for: .legible) - expect(player.selectedMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("ja")) - expect(player.currentMediaOption(for: .legible)).to(haveLanguageIdentifier("ja")) - } - - func testSelectLegibleAutomaticOption() { - MediaAccessibilityDisplayType.forcedOnly.apply() - - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - expect(player.mediaSelectionOptions(for: .legible)).toEventuallyNot(beEmpty()) - - player.select(mediaOption: .automatic, for: .legible) - expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.automatic)) - expect(player.currentMediaOption(for: .legible)).to(equal(.off)) - } - - func testSelectLegibleOffOption() { - MediaAccessibilityDisplayType.automatic.apply() - - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - expect(player.mediaSelectionOptions(for: .legible)).toEventuallyNot(beEmpty()) - - player.select(mediaOption: .off, for: .legible) - expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.off)) - expect(player.currentMediaOption(for: .legible)).to(equal(.off)) - } - - func testAudibleSelectionIsPreservedBetweenItems() { - MediaAccessibilityDisplayType.alwaysOn(languageCode: "en").apply() - - let player = Player(items: [ - .simple(url: Stream.onDemandWithOptions.url), - .simple(url: Stream.onDemandWithOptions.url) - ]) - expect(player.mediaSelectionOptions(for: .audible)).toEventuallyNot(beEmpty()) - - player.select(mediaOption: player.mediaSelectionOptions(for: .audible).first { option in - option.languageIdentifier == "fr" - }!, for: .audible) - expect(player.currentMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("fr")) - - player.advanceToNextItem() - expect(player.currentMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("fr")) - } - - func testLegibleSelectionIsPreservedBetweenItems() { - MediaAccessibilityDisplayType.alwaysOn(languageCode: "en").apply() - - let player = Player(items: [ - .simple(url: Stream.onDemandWithOptions.url), - .simple(url: Stream.onDemandWithOptions.url) - ]) - expect(player.mediaSelectionOptions(for: .legible)).toEventuallyNot(beEmpty()) - - player.select(mediaOption: player.mediaSelectionOptions(for: .legible).first { option in - option.languageIdentifier == "fr" - }!, for: .legible) - expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("fr")) - - player.advanceToNextItem() - expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("fr")) - } - - func testLegibleOptionSwitchFromOffToAutomatic() { - MediaAccessibilityDisplayType.forcedOnly.apply() - - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - expect(player.mediaSelectionOptions(for: .legible)).toEventuallyNot(beEmpty()) - player.select(mediaOption: .automatic, for: .legible) - - expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.automatic)) - expect(player.currentMediaOption(for: .legible)).to(equal(.off)) - } - - func testObservabilityWhenTogglingBetweenOffAndAutomatic() { - MediaAccessibilityDisplayType.forcedOnly.apply() - - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - expect(player.mediaSelectionOptions(for: .legible)).toEventuallyNot(beEmpty()) - - expectChange(from: player) { - player.select(mediaOption: .automatic, for: .legible) - } - expectChange(from: player) { - player.select(mediaOption: .off, for: .legible) - } - } -} - -private extension Player { - func group(for characteristic: AVMediaCharacteristic) async throws -> AVMediaSelectionGroup? { - guard let item = systemPlayer.currentItem else { return nil } - return try await item.asset.loadMediaSelectionGroup(for: characteristic) - } -} diff --git a/Tests/PlayerTests/MediaSelection/PreferredLanguagesForMediaSelectionTests.swift b/Tests/PlayerTests/MediaSelection/PreferredLanguagesForMediaSelectionTests.swift deleted file mode 100644 index 624dae8a..00000000 --- a/Tests/PlayerTests/MediaSelection/PreferredLanguagesForMediaSelectionTests.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble -import PillarboxStreams - -final class PreferredLanguagesForMediaSelectionTests: TestCase { - func testAudibleOptionMatchesAvailablePreferredLanguage() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - player.setMediaSelection(preferredLanguages: ["fr"], for: .audible) - expect(player.selectedMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("fr")) - } - - func testLegibleOptionMatchesAvailablePreferredLanguage() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - player.setMediaSelection(preferredLanguages: ["fr"], for: .legible) - expect(player.selectedMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("fr")) - } - - func testAudibleOptionIgnoresInvalidPreferredLanguage() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - player.setMediaSelection(preferredLanguages: ["xy"], for: .audible) - expect(player.currentMediaOption(for: .audible)).toNever(haveLanguageIdentifier("xy"), until: .seconds(2)) - } - - func testLegibleOptionIgnoresInvalidPreferredLanguage() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - player.setMediaSelection(preferredLanguages: ["xy"], for: .legible) - expect(player.currentMediaOption(for: .legible)).toNever(haveLanguageIdentifier("xy"), until: .seconds(2)) - } - - func testAudibleOptionIgnoresUnsupportedPreferredLanguage() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - player.setMediaSelection(preferredLanguages: ["it"], for: .audible) - expect(player.currentMediaOption(for: .audible)).toNever(haveLanguageIdentifier("it"), until: .seconds(2)) - } - - func testLegibleOptionIgnoresUnsupportedPreferredLanguage() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - player.setMediaSelection(preferredLanguages: ["it"], for: .legible) - expect(player.currentMediaOption(for: .legible)).toNever(haveLanguageIdentifier("it"), until: .seconds(2)) - } - - func testPreferredAudibleLanguageOverrideSelection() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - expect(player.mediaSelectionOptions(for: .audible)).toEventuallyNot(beEmpty()) - - player.select(mediaOption: player.mediaSelectionOptions(for: .audible).first { option in - option.languageIdentifier == "fr" - }!, for: .audible) - - player.setMediaSelection(preferredLanguages: ["en"], for: .audible) - expect(player.currentMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("en")) - } - - func testPreferredLegibleLanguageOverrideSelection() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - expect(player.mediaSelectionOptions(for: .legible)).toEventuallyNot(beEmpty()) - - player.select(mediaOption: player.mediaSelectionOptions(for: .legible).first { option in - option.languageIdentifier == "ja" - }!, for: .legible) - - player.setMediaSelection(preferredLanguages: ["fr"], for: .legible) - expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("fr")) - } - - func testPreferredAudibleLanguageIsPreservedBetweenItems() { - let player = Player(items: [ - .simple(url: Stream.onDemandWithOptions.url), - .simple(url: Stream.onDemandWithOptions.url) - ]) - player.setMediaSelection(preferredLanguages: ["fr"], for: .audible) - expect(player.currentMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("fr")) - - player.advanceToNextItem() - expect(player.currentMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("fr")) - } - - func testPreferredLegibleLanguageIsPreservedBetweenItems() { - let player = Player(items: [ - .simple(url: Stream.onDemandWithOptions.url), - .simple(url: Stream.onDemandWithOptions.url) - ]) - player.setMediaSelection(preferredLanguages: ["fr"], for: .legible) - expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("fr")) - - player.advanceToNextItem() - expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("fr")) - } - - func testPreferredLegibleLanguageAcrossItems() { - let player = Player(items: [ - .simple(url: Stream.onDemandWithOptions.url), - .simple(url: Stream.onDemandWithManyLegibleAndAudibleOptions.url) - ]) - - player.setMediaSelection(preferredLanguages: ["en"], for: .legible) - expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("en")) - - player.advanceToNextItem() - expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("en")) - - player.setMediaSelection(preferredLanguages: ["it"], for: .legible) - expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("it")) - - player.returnToPrevious() - expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("en")) - } - - func testSelectLegibleOffOptionWithPreferredLanguage() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - - player.setMediaSelection(preferredLanguages: ["en"], for: .legible) - expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("en")) - - player.select(mediaOption: .off, for: .legible) - expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.off)) - } - - func testSelectLegibleAutomaticOptionWithPreferredLanguage() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - - player.setMediaSelection(preferredLanguages: ["en"], for: .legible) - expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("en")) - - player.select(mediaOption: .automatic, for: .legible) - expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.automatic)) - } - - func testMediaSelectionReset() { - let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) - player.setMediaSelection(preferredLanguages: ["fr"], for: .audible) - expect(player.currentMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("fr")) - player.setMediaSelection(preferredLanguages: [], for: .audible) - expect(player.currentMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("en")) - } -} diff --git a/Tests/PlayerTests/Metrics/AccessLogEventTests.swift b/Tests/PlayerTests/Metrics/AccessLogEventTests.swift deleted file mode 100644 index 85730db5..00000000 --- a/Tests/PlayerTests/Metrics/AccessLogEventTests.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble - -final class AccessLogEventTests: TestCase { - func testNegativeValues() { - let event = AccessLogEvent( - uri: nil, - serverAddress: nil, - playbackStartDate: nil, - playbackSessionId: nil, - playbackStartOffset: -1, - playbackType: nil, - startupTime: -1, - observedBitrateStandardDeviation: -1, - indicatedBitrate: -1, - observedBitrate: -1, - averageAudioBitrate: -1, - averageVideoBitrate: -1, - indicatedAverageBitrate: -1, - numberOfServerAddressChanges: -1, - mediaRequestsWWAN: -1, - transferDuration: -1, - numberOfBytesTransferred: -1, - numberOfMediaRequests: -1, - playbackDuration: -1, - numberOfDroppedVideoFrames: -1, - numberOfStalls: -1, - segmentsDownloadedDuration: -1, - downloadOverdue: -1, - switchBitrate: -1 - ) - expect(event.playbackStartOffset).to(beNil()) - expect(event.startupTime).to(beNil()) - expect(event.observedBitrateStandardDeviation).to(beNil()) - expect(event.indicatedBitrate).to(beNil()) - expect(event.observedBitrate).to(beNil()) - expect(event.averageAudioBitrate).to(beNil()) - expect(event.averageVideoBitrate).to(beNil()) - expect(event.indicatedAverageBitrate).to(beNil()) - expect(event.numberOfServerAddressChanges).to(equal(0)) - expect(event.mediaRequestsWWAN).to(equal(0)) - expect(event.transferDuration).to(equal(0)) - expect(event.numberOfBytesTransferred).to(equal(0)) - expect(event.numberOfMediaRequests).to(equal(0)) - expect(event.playbackDuration).to(equal(0)) - expect(event.numberOfDroppedVideoFrames).to(equal(0)) - expect(event.numberOfStalls).to(equal(0)) - expect(event.segmentsDownloadedDuration).to(equal(0)) - expect(event.downloadOverdue).to(equal(0)) - expect(event.switchBitrate).to(equal(0)) - } -} diff --git a/Tests/PlayerTests/Metrics/MetricsCollectorEventsTests.swift b/Tests/PlayerTests/Metrics/MetricsCollectorEventsTests.swift deleted file mode 100644 index 5b0d2b4c..00000000 --- a/Tests/PlayerTests/Metrics/MetricsCollectorEventsTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble -import PillarboxStreams - -final class MetricsCollectorEventsTests: TestCase { - func testUnbound() { - let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) - expect(metricsCollector.metricEvents).toAlways(beEmpty(), until: .milliseconds(500)) - } - - func testEmptyPlayer() { - let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) - metricsCollector.player = Player() - expect(metricsCollector.metricEvents).toAlways(beEmpty(), until: .milliseconds(500)) - } - - func testPausedPlayer() { - let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) - let player = Player(item: .simple(url: Stream.onDemand.url)) - metricsCollector.player = player - expect(metricsCollector.metricEvents).toEventuallyNot(beEmpty()) - } - - func testPlayerSetToNil() { - let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - metricsCollector.player = player - player.play() - expect(metricsCollector.metricEvents).toEventuallyNot(beEmpty()) - - metricsCollector.player = nil - expect(metricsCollector.metricEvents).to(beEmpty()) - } -} diff --git a/Tests/PlayerTests/Metrics/MetricsCollectorTests.swift b/Tests/PlayerTests/Metrics/MetricsCollectorTests.swift deleted file mode 100644 index b3ffa351..00000000 --- a/Tests/PlayerTests/Metrics/MetricsCollectorTests.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Combine -import CoreMedia -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class MetricsCollectorTests: TestCase { - func testUnbound() { - let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) - expectAtLeastEqualPublished( - values: [[]], - from: metricsCollector.$metrics - .map { $0.compactMap(\.uri) } - .removeDuplicates() - ) - } - - func testEmptyPlayer() { - let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) - expectAtLeastEqualPublished( - values: [[]], - from: metricsCollector.$metrics - .map { $0.compactMap(\.uri) } - .removeDuplicates() - ) { - metricsCollector.player = Player() - } - } - - func testPausedPlayer() { - let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expectAtLeastEqualPublished( - values: [[]], - from: metricsCollector.$metrics - .map { $0.compactMap(\.uri) } - .removeDuplicates() - ) { - metricsCollector.player = player - } - } - - func testPlayback() { - let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expectAtLeastEqualPublished( - values: [[], [Stream.onDemand.url.absoluteString]], - from: metricsCollector.$metrics - .map { $0.compactMap(\.uri) } - .removeDuplicates() - ) { - metricsCollector.player = player - player.play() - } - } - - func testPlayerSetToNil() { - let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - metricsCollector.player = player - player.play() - expect(metricsCollector.metrics).toEventuallyNot(beEmpty()) - - metricsCollector.player = nil - expect(metricsCollector.metrics).to(beEmpty()) - } -} diff --git a/Tests/PlayerTests/Metrics/MetricsStateTests.swift b/Tests/PlayerTests/Metrics/MetricsStateTests.swift deleted file mode 100644 index 8639a8ba..00000000 --- a/Tests/PlayerTests/Metrics/MetricsStateTests.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble - -final class MetricsStateTests: TestCase { - // swiftlint:disable:next function_body_length - func testMetrics() { - let state = MetricsState(with: [ - .init( - uri: "uri", - serverAddress: "serverAddress", - playbackStartDate: Date(timeIntervalSince1970: 1), - playbackSessionId: "playbackSessionId", - playbackStartOffset: 2, - playbackType: "playbackType", - startupTime: 3, - observedBitrateStandardDeviation: 4, - indicatedBitrate: 5, - observedBitrate: 6, - averageAudioBitrate: 7, - averageVideoBitrate: 8, - indicatedAverageBitrate: 9, - numberOfServerAddressChanges: 10, - mediaRequestsWWAN: 11, - transferDuration: 12, - numberOfBytesTransferred: 13, - numberOfMediaRequests: 14, - playbackDuration: 15, - numberOfDroppedVideoFrames: 16, - numberOfStalls: 17, - segmentsDownloadedDuration: 18, - downloadOverdue: 19, - switchBitrate: 20 - ) - ], at: .init(value: 12, timescale: 1)) - - let metrics = state.metrics(from: .empty) - expect(metrics.playbackStartDate).to(equal(Date(timeIntervalSince1970: 1))) - expect(metrics.time).to(equal(.init(value: 12, timescale: 1))) - expect(metrics.uri).to(equal("uri")) - expect(metrics.serverAddress).to(equal("serverAddress")) - expect(metrics.playbackSessionId).to(equal("playbackSessionId")) - expect(metrics.playbackStartOffset).to(equal(2)) - expect(metrics.playbackType).to(equal("playbackType")) - expect(metrics.startupTime).to(equal(3)) - expect(metrics.observedBitrateStandardDeviation).to(equal(4)) - expect(metrics.indicatedBitrate).to(equal(5)) - expect(metrics.observedBitrate).to(equal(6)) - expect(metrics.averageAudioBitrate).to(equal(7)) - expect(metrics.averageVideoBitrate).to(equal(8)) - expect(metrics.indicatedAverageBitrate).to(equal(9)) - - expect(metrics.increment.numberOfServerAddressChanges).to(equal(10)) - expect(metrics.increment.mediaRequestsWWAN).to(equal(11)) - expect(metrics.increment.transferDuration).to(equal(12)) - expect(metrics.increment.numberOfBytesTransferred).to(equal(13)) - expect(metrics.increment.numberOfMediaRequests).to(equal(14)) - expect(metrics.increment.playbackDuration).to(equal(15)) - expect(metrics.increment.numberOfDroppedVideoFrames).to(equal(16)) - expect(metrics.increment.numberOfStalls).to(equal(17)) - expect(metrics.increment.segmentsDownloadedDuration).to(equal(18)) - expect(metrics.increment.downloadOverdue).to(equal(19)) - expect(metrics.increment.switchBitrate).to(equal(20)) - - expect(metrics.total.numberOfServerAddressChanges).to(equal(10)) - expect(metrics.total.mediaRequestsWWAN).to(equal(11)) - expect(metrics.total.transferDuration).to(equal(12)) - expect(metrics.total.numberOfBytesTransferred).to(equal(13)) - expect(metrics.total.numberOfMediaRequests).to(equal(14)) - expect(metrics.total.playbackDuration).to(equal(15)) - expect(metrics.total.numberOfDroppedVideoFrames).to(equal(16)) - expect(metrics.total.numberOfStalls).to(equal(17)) - expect(metrics.total.segmentsDownloadedDuration).to(equal(18)) - expect(metrics.total.downloadOverdue).to(equal(19)) - expect(metrics.total.switchBitrate).to(equal(20)) - } -} - -private extension AccessLogEvent { - init(numberOfStalls: Int = -1) { - self.init( - uri: nil, - serverAddress: nil, - playbackStartDate: nil, - playbackSessionId: nil, - playbackStartOffset: -1, - playbackType: nil, - startupTime: -1, - observedBitrateStandardDeviation: -1, - indicatedBitrate: -1, - observedBitrate: -1, - averageAudioBitrate: -1, - averageVideoBitrate: -1, - indicatedAverageBitrate: -1, - numberOfServerAddressChanges: -1, - mediaRequestsWWAN: -1, - transferDuration: -1, - numberOfBytesTransferred: -1, - numberOfMediaRequests: -1, - playbackDuration: -1, - numberOfDroppedVideoFrames: -1, - numberOfStalls: numberOfStalls, - segmentsDownloadedDuration: -1, - downloadOverdue: -1, - switchBitrate: -1 - ) - } -} diff --git a/Tests/PlayerTests/Player/BlockedTimeRangeTests.swift b/Tests/PlayerTests/Player/BlockedTimeRangeTests.swift deleted file mode 100644 index 460410b7..00000000 --- a/Tests/PlayerTests/Player/BlockedTimeRangeTests.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble -import PillarboxStreams - -private let kBlockedTimeRange = CMTimeRange(start: .init(value: 20, timescale: 1), end: .init(value: 60, timescale: 1)) -private let kOverlappingBlockedTimeRange = CMTimeRange(start: .init(value: 50, timescale: 1), end: .init(value: 100, timescale: 1)) -private let kNestedBlockedTimeRange = CMTimeRange(start: .init(value: 30, timescale: 1), end: .init(value: 50, timescale: 1)) - -private struct MetadataWithBlockedTimeRange: AssetMetadata { - var playerMetadata: PlayerMetadata { - .init(timeRanges: [ - .init(kind: .blocked, start: kBlockedTimeRange.start, end: kBlockedTimeRange.end) - ]) - } -} - -private struct MetadataWithOverlappingBlockedTimeRanges: AssetMetadata { - var playerMetadata: PlayerMetadata { - .init(timeRanges: [ - .init(kind: .blocked, start: kBlockedTimeRange.start, end: kBlockedTimeRange.end), - .init(kind: .blocked, start: kOverlappingBlockedTimeRange.start, end: kOverlappingBlockedTimeRange.end) - ]) - } -} - -private struct MetadataWithNestedBlockedTimeRanges: AssetMetadata { - var playerMetadata: PlayerMetadata { - .init(timeRanges: [ - .init(kind: .blocked, start: kBlockedTimeRange.start, end: kBlockedTimeRange.end), - .init(kind: .blocked, start: kNestedBlockedTimeRange.start, end: kNestedBlockedTimeRange.end) - ]) - } -} - -final class BlockedTimeRangeTests: TestCase { - func testSeekInBlockedTimeRange() { - let player = Player(item: .simple(url: Stream.onDemand.url, metadata: MetadataWithBlockedTimeRange())) - expect(player.streamType).toEventually(equal(.onDemand)) - player.seek(at(.init(value: 30, timescale: 1))) - expect(kBlockedTimeRange.containsTime(player.time())).toNever(beTrue(), until: .seconds(2)) - expect(player.time()).to(equal(kBlockedTimeRange.end)) - } - - func testSeekInOverlappingBlockedTimeRange() { - let player = Player(item: .simple(url: Stream.onDemand.url, metadata: MetadataWithOverlappingBlockedTimeRanges())) - expect(player.streamType).toEventually(equal(.onDemand)) - player.seek(at(.init(value: 30, timescale: 1))) - expect(kOverlappingBlockedTimeRange.containsTime(player.time())).toNever(beTrue(), until: .seconds(2)) - expect(player.time()).to(equal(kOverlappingBlockedTimeRange.end)) - } - - func testSeekInNestedBlockedTimeRange() { - let player = Player(item: .simple(url: Stream.onDemand.url, metadata: MetadataWithNestedBlockedTimeRanges())) - expect(player.streamType).toEventually(equal(.onDemand)) - player.seek(at(.init(value: 40, timescale: 1))) - expect(kNestedBlockedTimeRange.containsTime(player.time())).toNever(beTrue(), until: .seconds(2)) - expect(player.time()).to(equal(kBlockedTimeRange.end)) - } - - func testBlockedTimeRangeTraversal() { - let configuration = PlayerItemConfiguration(position: at(.init(value: 29, timescale: 1))) - let player = Player(item: .simple(url: Stream.onDemand.url, metadata: MetadataWithBlockedTimeRange(), configuration: configuration)) - player.play() - expect(player.time()).toEventually(beGreaterThan(kBlockedTimeRange.end)) - } - - func testOnDemandStartInBlockedTimeRange() { - let configuration = PlayerItemConfiguration(position: at(.init(value: 30, timescale: 1))) - let player = Player(item: .simple(url: Stream.onDemand.url, metadata: MetadataWithBlockedTimeRange(), configuration: configuration)) - expect(player.time()).toEventually(equal(kBlockedTimeRange.end)) - } -} diff --git a/Tests/PlayerTests/Player/ErrorTests.swift b/Tests/PlayerTests/Player/ErrorTests.swift index f42096e9..5e3d4c0f 100644 --- a/Tests/PlayerTests/Player/ErrorTests.swift +++ b/Tests/PlayerTests/Player/ErrorTests.swift @@ -11,40 +11,10 @@ import Foundation import Nimble import PillarboxCircumspect import PillarboxStreams +import XCTest -final class ErrorTests: TestCase { - private static func errorCodePublisher(for player: Player) -> AnyPublisher { - player.$error - .map { error in - guard let error else { return nil } - return .init(rawValue: (error as NSError).code) - } - .eraseToAnyPublisher() - } - - func testNoStream() { - let player = Player() - expectNothingPublishedNext(from: player.$error, during: .milliseconds(500)) - } - - func testValidStream() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expectNothingPublishedNext(from: player.$error, during: .milliseconds(500)) - } - - func testInvalidStream() { - let player = Player(item: .simple(url: Stream.unavailable.url)) - expectEqualPublishedNext( - values: [.init(rawValue: NSURLErrorFileDoesNotExist)], - from: Self.errorCodePublisher(for: player), - during: .seconds(1) - ) - } - - func testReset() { - let player = Player(item: .simple(url: Stream.unavailable.url)) - expect(player.error).toEventuallyNot(beNil()) - player.removeAllItems() - expect(player.error).toEventually(beNil()) +final class ErrorTests: XCTestCase { + func testTruth() { + expect(true).to(beTrue()) } } diff --git a/Tests/PlayerTests/Player/PlaybackSpeedUpdateTests.swift b/Tests/PlayerTests/Player/PlaybackSpeedUpdateTests.swift deleted file mode 100644 index c749f3c7..00000000 --- a/Tests/PlayerTests/Player/PlaybackSpeedUpdateTests.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble - -final class PlaybackSpeedUpdateTests: TestCase { - func testUpdateIndefiniteWithValue() { - let speed = PlaybackSpeed.indefinite - let updatedSpeed = speed.updated(with: .value(2)) - expect(updatedSpeed.value).to(equal(2)) - expect(updatedSpeed.range).to(beNil()) - } - - func testUpdateIndefiniteWithRange() { - let speed = PlaybackSpeed.indefinite - let updatedSpeed = speed.updated(with: .range(0...2)) - expect(updatedSpeed.value).to(equal(1)) - expect(updatedSpeed.range).to(equal(0...2)) - } - - func testUpdateDefiniteWithSameRange() { - let speed = PlaybackSpeed(value: 2, range: 0...2) - let updatedSpeed = speed.updated(with: .range(0...2)) - expect(updatedSpeed.value).to(equal(2)) - expect(updatedSpeed.range).to(equal(0...2)) - } - - func testUpdateDefiniteWithIndefiniteRange() { - let speed = PlaybackSpeed(value: 2, range: 0...2) - let updatedSpeed = speed.updated(with: .range(nil)) - expect(updatedSpeed.value).to(equal(1)) - expect(updatedSpeed.range).to(beNil()) - } -} diff --git a/Tests/PlayerTests/Player/PlaybackTests.swift b/Tests/PlayerTests/Player/PlaybackTests.swift deleted file mode 100644 index e1f46b28..00000000 --- a/Tests/PlayerTests/Player/PlaybackTests.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Combine -import PillarboxCircumspect -import PillarboxStreams -import XCTest - -final class PlaybackTests: XCTestCase { - private func playbackStatePublisher(for player: Player) -> AnyPublisher { - player.propertiesPublisher - .slice(at: \.playbackState) - .eraseToAnyPublisher() - } - - func testHLS() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expectAtLeastEqualPublished( - values: [.idle, .paused], - from: playbackStatePublisher(for: player) - ) - } - - func testMP3() { - let item = PlayerItem.simple(url: Stream.mp3.url) - let player = Player(item: item) - expectAtLeastEqualPublished( - values: [.idle, .paused], - from: playbackStatePublisher(for: player) - ) - } - - func testUnknown() { - let item = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(item: item) - expectEqualPublished( - values: [.idle], - from: playbackStatePublisher(for: player), - during: .seconds(1) - ) - } -} diff --git a/Tests/PlayerTests/Player/PlayerTests.swift b/Tests/PlayerTests/Player/PlayerTests.swift deleted file mode 100644 index dd51a251..00000000 --- a/Tests/PlayerTests/Player/PlayerTests.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class PlayerTests: TestCase { - func testDeallocation() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - var player: Player? = Player(item: item) - - weak var weakPlayer = player - autoreleasepool { - player = nil - } - expect(weakPlayer).to(beNil()) - } - - func testTimesWhenEmpty() { - let player = Player() - expect(player.time()).toAlways(equal(.invalid), until: .seconds(1)) - } - - func testTimesInEmptyRange() { - let player = Player(item: .simple(url: Stream.live.url)) - expect(player.seekableTimeRange).toEventuallyNot(equal(.invalid)) - player.play() - expect(player.time()).toNever(equal(.invalid), until: .seconds(1)) - } - - func testMetadataUpdatesMustNotChangePlayerItem() { - let player = Player(item: .mock(url: Stream.onDemand.url, withMetadataUpdateAfter: 1)) - expect(player.queuePlayer.currentItem?.url).toEventually(equal(Stream.onDemand.url)) - let currentItem = player.queuePlayer.currentItem - expect(player.queuePlayer.currentItem).toAlways(equal(currentItem), until: .seconds(2)) - } - - func testRetrieveCurrentValueOnSubscription() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.properties.isBuffering).toEventually(beFalse()) - expectEqualPublished( - values: [false], - from: player.propertiesPublisher.slice(at: \.isBuffering), - during: .seconds(1) - ) - } - - func testPreloadedItems() { - let player = Player( - items: [ - .simple(url: Stream.onDemand.url), - .simple(url: Stream.onDemand.url), - .simple(url: Stream.onDemand.url) - ] - ) - let expectedResources: [Resource] = [ - .simple(url: Stream.onDemand.url), - .simple(url: Stream.onDemand.url), - .loading - ] - expect(player.items.map(\.content.resource)).toEventually(beSimilarTo(expectedResources)) - expect(player.items.map(\.content.resource)).toAlways(beSimilarTo(expectedResources), until: .seconds(1)) - } - - func testNoMetricsWhenFailed() { - let player = Player(item: .failing(loadedAfter: 0.1)) - expect(player.properties.metrics()).toAlways(beNil(), until: .seconds(1)) - } -} diff --git a/Tests/PlayerTests/Player/QueueTests.swift b/Tests/PlayerTests/Player/QueueTests.swift deleted file mode 100644 index a6806eed..00000000 --- a/Tests/PlayerTests/Player/QueueTests.swift +++ /dev/null @@ -1,184 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class QueueTests: TestCase { - func testWhenEmpty() { - let player = Player() - expect(player.urls).to(beEmpty()) - expect(player.currentItem).to(beNil()) - } - - func testPlayableItem() { - let item = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(item: item) - expect(player.urls).toEventually(equal([ - Stream.shortOnDemand.url - ])) - expect(player.currentItem).to(equal(item)) - } - - func testEntirePlayback() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - player.play() - expect(player.urls).toEventually(beEmpty()) - expect(player.currentItem).to(beNil()) - } - - func testFailingUnavailableItem() { - let item = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(item: item) - // Item is consumed by `AVQueuePlayer` for some reason. - expect(player.urls).toEventually(beEmpty()) - expect(player.currentItem).to(equal(item)) - } - - func testFailingUnauthorizedItem() { - let item = PlayerItem.simple(url: Stream.unauthorized.url) - let player = Player(item: item) - expect(player.urls).toEventually(equal([ - Stream.unauthorized.url - ])) - expect(player.currentItem).to(equal(item)) - } - - func testFailingMp3Item() { - let item = PlayerItem.simple(url: Stream.unavailableMp3.url) - let player = Player(item: item) - expect(player.urls).toEventually(equal([ - Stream.unavailableMp3.url - ])) - expect(player.currentItem).to(equal(item)) - } - - func testBetweenPlayableItems() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - player.play() - - expect(player.urls).toEventually(equal([ - Stream.shortOnDemand.url, - Stream.onDemand.url - ])) - expect(player.currentItem).to(equal(item1)) - - expect(player.urls).toEventually(equal([ - Stream.onDemand.url - ])) - expect(player.currentItem).to(equal(item2)) - } - - func testFailingUnavailableItemFollowedByPlayableItem() { - let item1 = PlayerItem.simple(url: Stream.unavailable.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - // Item is consumed by `AVQueuePlayer` for some reason. - expect(player.urls).toEventually(beEmpty()) - expect(player.currentItem).to(equal(item1)) - } - - func testFailingUnauthorizedItemFollowedByPlayableItem() { - let item1 = PlayerItem.simple(url: Stream.unauthorized.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.urls).toEventually(equal([ - Stream.unauthorized.url - ])) - expect(player.currentItem).to(equal(item1)) - } - - func testFailingMp3ItemFollowedByPlayableItem() { - let item1 = PlayerItem.simple(url: Stream.unavailableMp3.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.urls).toEventually(equal([ - Stream.unavailableMp3.url - ])) - expect(player.currentItem).to(equal(item1)) - } - - func testFailingItemUnavailableBetweenPlayableItems() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.unavailable.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.play() - expect(player.urls).toEventually(beEmpty()) - expect(player.currentItem).to(equal(item2)) - } - - func testFailingMp3ItemBetweenPlayableItems() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.unavailableMp3.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.play() - expect(player.urls).toEventually(beEmpty()) - expect(player.currentItem).to(equal(item2)) - } - - func testPlayableItemReplacingFailingUnavailableItem() { - let player = Player(item: .simple(url: Stream.unavailable.url)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - player.items = [item] - expect(player.urls).toEventually(equal([ - Stream.onDemand.url - ])) - expect(player.currentItem).to(equal(item)) - } - - func testPlayableItemReplacingFailingUnauthorizedItem() { - let player = Player(item: .simple(url: Stream.unauthorized.url)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - player.items = [item] - expect(player.urls).toEventually(equal([ - Stream.onDemand.url - ])) - expect(player.currentItem).to(equal(item)) - } - - func testPlayableItemReplacingFailingMp3Item() { - let player = Player(item: .simple(url: Stream.unavailableMp3.url)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - player.items = [item] - expect(player.urls).toEventually(equal([ - Stream.onDemand.url - ])) - expect(player.currentItem).to(equal(item)) - } - - func testReplaceCurrentItem() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - player.items = [item] - expect(player.urls).toEventually(equal([ - Stream.onDemand.url - ])) - expect(player.currentItem).to(equal(item)) - } - - func testRemoveCurrentItemFollowedByPlayableItem() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - player.remove(player.items.first!) - expect(player.urls).toEventually(equal([ - Stream.onDemand.url - ])) - expect(player.currentItem).to(equal(item2)) - } - - func testRemoveAllItems() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - player.removeAllItems() - expect(player.urls).to(beEmpty()) - } -} diff --git a/Tests/PlayerTests/Player/ReplayChecksTests.swift b/Tests/PlayerTests/Player/ReplayChecksTests.swift deleted file mode 100644 index 6a6f3f73..00000000 --- a/Tests/PlayerTests/Player/ReplayChecksTests.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class ReplayChecksTests: TestCase { - func testEmptyPlayer() { - let player = Player() - expect(player.canReplay()).to(beFalse()) - } - - func testWithOneGoodItem() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - expect(player.canReplay()).to(beFalse()) - } - - func testWithOneGoodItemPlayedEntirely() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - player.play() - expect(player.canReplay()).toEventually(beTrue()) - } - - func testWithOneBadItemConsumed() { - // This item is consumed by the player when failing. - let player = Player(item: .simple(url: Stream.unavailable.url)) - expect(player.canReplay()).toEventually(beTrue()) - } - - func testWithOneBadItemNotConsumed() { - // This item is not consumed by the player when failing (for an unknown reason). - let player = Player(item: .simple(url: Stream.unauthorized.url)) - expect(player.canReplay()).toEventually(beTrue()) - } - - func testWithManyGoodItems() { - let player = Player(items: [ - .simple(url: Stream.shortOnDemand.url), - .simple(url: Stream.shortOnDemand.url) - ]) - player.play() - expect(player.canReplay()).toEventually(beTrue()) - } - - func testWithManyBadItems() { - let player = Player(items: [ - .simple(url: Stream.unavailable.url), - .simple(url: Stream.unavailable.url) - ]) - player.play() - expect(player.canReplay()).toEventually(beTrue()) - } - - func testWithOneGoodItemAndOneBadItem() { - let player = Player(items: [ - .simple(url: Stream.shortOnDemand.url), - .simple(url: Stream.unavailable.url) - ]) - player.play() - expect(player.canReplay()).toEventually(beTrue()) - } - - func testWithOneLongGoodItemAndOneBadItem() { - let player = Player(items: [ - .simple(url: Stream.onDemand.url), - .simple(url: Stream.unavailable.url) - ]) - player.play() - expect(player.canReplay()).toNever(beTrue(), until: .milliseconds(500)) - } -} diff --git a/Tests/PlayerTests/Player/ReplayTests.swift b/Tests/PlayerTests/Player/ReplayTests.swift deleted file mode 100644 index 1553af19..00000000 --- a/Tests/PlayerTests/Player/ReplayTests.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class ReplayTests: TestCase { - func testWithOneGoodItem() { - let item = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(item: item) - player.replay() - expect(player.currentItem).to(equal(item)) - } - - func testWithOneGoodItemPlayedEntirely() { - let item = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(item: item) - player.play() - expect(player.currentItem).toEventually(beNil()) - player.replay() - expect(player.currentItem).toEventually(equal(item)) - } - - func testWithOneBadItem() { - let item = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(item: item) - expect(player.currentItem).toAlways(equal(item), until: .milliseconds(500)) - player.replay() - expect(player.currentItem).toAlways(equal(item), until: .milliseconds(500)) - } - - func testWithManyGoodItems() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(items: [item1, item2]) - player.play() - expect(player.currentItem).toEventually(equal(item2)) - player.replay() - expect(player.currentItem).to(equal(item2)) - } - - func testWithManyBadItems() { - let item1 = PlayerItem.simple(url: Stream.unavailable.url) - let item2 = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(items: [item1, item2]) - player.play() - expect(player.currentItem).toAlways(equal(item1), until: .milliseconds(500)) - player.replay() - expect(player.currentItem).toAlways(equal(item1), until: .milliseconds(500)) - } - - func testWithOneGoodItemAndOneBadItem() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(items: [item1, item2]) - player.play() - expect(player.currentItem).toEventually(equal(item2)) - player.replay() - expect(player.currentItem).to(equal(item2)) - } - - func testResumePlaybackIfNeeded() { - let item = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(item: item) - player.play() - expect(player.currentItem).toEventually(beNil()) - player.pause() - player.replay() - expect(player.currentItem).toEventually(equal(item)) - expect(player.playbackState).toEventually(equal(.playing)) - } -} diff --git a/Tests/PlayerTests/Player/SeekChecksTests.swift b/Tests/PlayerTests/Player/SeekChecksTests.swift deleted file mode 100644 index dccc164a..00000000 --- a/Tests/PlayerTests/Player/SeekChecksTests.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble -import PillarboxStreams - -final class SeekChecksTests: TestCase { - func testCannotSeekWithEmptyPlayer() { - let player = Player() - expect(player.canSeek(to: .zero)).to(beFalse()) - } - - func testCanSeekInTimeRange() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.canSeek(to: CMTimeMultiplyByFloat64(Stream.onDemand.duration, multiplier: 0.5))).to(beTrue()) - } - - func testCannotSeekInEmptyTimeRange() { - let player = Player(item: .simple(url: Stream.live.url)) - expect(player.streamType).toEventually(equal(.live)) - expect(player.canSeek(to: .zero)).to(beFalse()) - } - - func testCanSeekToTimeRangeStart() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.canSeek(to: player.seekableTimeRange.start)).to(beTrue()) - } - - func testCanSeekToTimeRangeEnd() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.canSeek(to: player.seekableTimeRange.end)).to(beTrue()) - } - - func testCannotSeekBeforeTimeRangeStart() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.canSeek(to: CMTime(value: -10, timescale: 1))).to(beFalse()) - } - - func testCannotSeekAfterTimeRangeEnd() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.canSeek(to: player.seekableTimeRange.end + CMTime(value: 1, timescale: 1))).to(beFalse()) - } -} diff --git a/Tests/PlayerTests/Player/SeekTests.swift b/Tests/PlayerTests/Player/SeekTests.swift deleted file mode 100644 index 1e2d04e5..00000000 --- a/Tests/PlayerTests/Player/SeekTests.swift +++ /dev/null @@ -1,120 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -private struct MockMetadata: AssetMetadata { - var playerMetadata: PlayerMetadata { - .init(timeRanges: [ - .init(kind: .blocked, start: .init(value: 20, timescale: 1), end: .init(value: 60, timescale: 1)) - ]) - } -} - -final class SeekTests: TestCase { - func testSeekWhenEmpty() { - let player = Player() - waitUntil { done in - player.seek(near(.zero)) { finished in - expect(finished).to(beTrue()) - done() - } - } - } - - func testSeekInTimeRange() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - waitUntil { done in - player.seek(near(CMTimeMultiplyByFloat64(Stream.onDemand.duration, multiplier: 0.5))) { finished in - expect(finished).to(beTrue()) - done() - } - } - } - - func testSeekInEmptyTimeRange() { - let player = Player(item: .simple(url: Stream.live.url)) - expect(player.streamType).toEventually(equal(.live)) - waitUntil { done in - player.seek(near(.zero)) { finished in - expect(finished).to(beTrue()) - done() - } - } - } - - func testSeekToTimeRangeStart() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - waitUntil { done in - player.seek(near(player.seekableTimeRange.start)) { finished in - expect(finished).to(beTrue()) - done() - } - } - } - - func testSeekToTimeRangeEnd() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - waitUntil { done in - player.seek(near(player.seekableTimeRange.end)) { finished in - expect(finished).to(beTrue()) - done() - } - } - } - - func testSeekBeforeTimeRangeStart() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - waitUntil { done in - player.seek(near(CMTime(value: -10, timescale: 1))) { finished in - expect(finished).to(beTrue()) - expect(player.time()).to(equal(.zero)) - done() - } - } - } - - func testSeekAfterTimeRangeEnd() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - waitUntil { done in - player.seek(near(player.seekableTimeRange.end + CMTime(value: 10, timescale: 1))) { finished in - expect(finished).to(beTrue()) - expect(player.time()).to(equal(player.seekableTimeRange.end, by: beClose(within: 1))) - done() - } - } - } - - func testTimesDuringSeekBeforeTimeRangeStart() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - player.play() - player.seek(near(CMTime(value: -10, timescale: 1))) - expect(player.time()).toAlways(beGreaterThanOrEqualTo(player.seekableTimeRange.start), until: .seconds(1)) - } - - func testOnDemandStartAtTime() { - let configuration = PlayerItemConfiguration(position: at(.init(value: 10, timescale: 1))) - let player = Player(item: .simple(url: Stream.onDemand.url, configuration: configuration)) - expect(player.time().seconds).toEventually(equal(10)) - } - - func testDvrStartAtTime() { - let configuration = PlayerItemConfiguration(position: at(.init(value: 10, timescale: 1))) - let player = Player(item: .simple(url: Stream.dvr.url, configuration: configuration)) - expect(player.time().seconds).toEventually(equal(10)) - } -} diff --git a/Tests/PlayerTests/Player/SpeedTests.swift b/Tests/PlayerTests/Player/SpeedTests.swift deleted file mode 100644 index d70a1421..00000000 --- a/Tests/PlayerTests/Player/SpeedTests.swift +++ /dev/null @@ -1,205 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class SpeedTests: TestCase { - func testEmpty() { - let player = Player() - expect(player.effectivePlaybackSpeed).toAlways(equal(1), until: .seconds(2)) - expect(player.playbackSpeedRange).toAlways(equal(1...1), until: .seconds(2)) - } - - func testNoSpeedUpdateWhenEmpty() { - let player = Player() - player.setDesiredPlaybackSpeed(2) - expect(player.effectivePlaybackSpeed).toAlways(equal(1), until: .seconds(2)) - expect(player.playbackSpeedRange).toAlways(equal(1...1), until: .seconds(2)) - } - - func testOnDemand() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - player.setDesiredPlaybackSpeed(2) - expect(player.effectivePlaybackSpeed).toEventually(equal(2)) - expect(player.playbackSpeedRange).toEventually(equal(0.1...2)) - } - - func testDvr() { - let player = Player(item: .simple(url: Stream.dvr.url)) - player.setDesiredPlaybackSpeed(0.5) - expect(player.effectivePlaybackSpeed).toEventually(equal(0.5)) - expect(player.playbackSpeedRange).toEventually(equal(0.1...1)) - } - - func testLive() { - let player = Player(item: .simple(url: Stream.live.url)) - player.setDesiredPlaybackSpeed(2) - expect(player.effectivePlaybackSpeed).toAlways(equal(1), until: .seconds(2)) - expect(player.playbackSpeedRange).toAlways(equal(1...1), until: .seconds(2)) - } - - func testDvrInThePast() { - let player = Player(item: .simple(url: Stream.dvr.url)) - expect(player.seekableTimeRange).toEventuallyNot(equal(.invalid)) - waitUntil { done in - player.seek(at(.init(value: 1, timescale: 1))) { _ in - done() - } - } - - expect(player.playbackSpeedRange).toEventually(equal(0.1...2)) - player.setDesiredPlaybackSpeed(2) - expect(player.effectivePlaybackSpeed).toEventually(equal(2)) - } - - func testPlaylistOnDemandToLive() { - let item1 = PlayerItem(asset: .simple(url: Stream.onDemand.url)) - let item2 = PlayerItem(asset: .simple(url: Stream.live.url)) - let player = Player(items: [item1, item2]) - - player.setDesiredPlaybackSpeed(2) - expect(player.effectivePlaybackSpeed).toEventually(equal(2)) - - player.advanceToNextItem() - expect(player.effectivePlaybackSpeed).toEventually(equal(1)) - expect(player.playbackSpeedRange).toEventually(equal(1...1)) - } - - func testPlaylistOnDemandToOnDemand() { - let item1 = PlayerItem(asset: .simple(url: Stream.onDemand.url)) - let item2 = PlayerItem(asset: .simple(url: Stream.onDemand.url)) - let player = Player(items: [item1, item2]) - player.setDesiredPlaybackSpeed(2) - expect(player.effectivePlaybackSpeed).toEventually(equal(2)) - - player.advanceToNextItem() - expect(player.effectivePlaybackSpeed).toEventually(equal(2)) - expect(player.playbackSpeedRange).toEventually(equal(0.1...2)) - } - - func testSpeedUpdateWhenStartingPlayback() { - let player = Player(item: .simple(url: Stream.dvr.url)) - expectAtLeastEqualPublished( - values: [1, 0.5], - from: player.changePublisher(at: \.effectivePlaybackSpeed).removeDuplicates() - ) { - player.setDesiredPlaybackSpeed(0.5) - } - } - - func testSpeedRangeUpdateWhenStartingPlayback() { - let player = Player(item: .simple(url: Stream.dvr.url)) - expectAtLeastEqualPublished( - values: [1...1, 0.1...1], - from: player.changePublisher(at: \.playbackSpeedRange).removeDuplicates() - ) { - player.setDesiredPlaybackSpeed(0.5) - } - } - - func testSpeedUpdateWhenApproachingLiveEdge() { - let player = Player(item: .simple(url: Stream.dvr.url)) - player.play() - expect(player.seekableTimeRange).toEventuallyNot(equal(.invalid)) - waitUntil { done in - player.seek(at(.init(value: 10, timescale: 1))) { _ in - done() - } - } - - player.setDesiredPlaybackSpeed(2) - expect(player.effectivePlaybackSpeed).toEventually(equal(2)) - expect(player.playbackSpeedRange).toEventually(equal(0.1...2)) - - expect(player.effectivePlaybackSpeed).toEventually(equal(1)) - expect(player.playbackSpeedRange).toEventually(equal(0.1...1)) - } - - func testPlaylistEnd() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - player.setDesiredPlaybackSpeed(2) - player.play() - expect(player.currentItem).toEventually(beNil()) - - expect(player.effectivePlaybackSpeed).toEventually(equal(1)) - expect(player.playbackSpeedRange).toEventually(equal(1...1)) - } - - func testItemAppendMustStartAtCurrentSpeed() { - let player = Player() - player.setDesiredPlaybackSpeed(2) - player.append(.simple(url: Stream.onDemand.url)) - expect(player.effectivePlaybackSpeed).toEventually(equal(2)) - } - - func testInitialSpeedMustSetRate() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - player.setDesiredPlaybackSpeed(2) - player.play() - expect(player.queuePlayer.defaultRate).toEventually(equal(2)) - expect(player.queuePlayer.rate).toEventually(equal(2)) - } - - func testSpeedUpdateMustUpdateRate() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - player.play() - expect(player.playbackState).toEventually(equal(.playing)) - - player.setDesiredPlaybackSpeed(2) - expect(player.queuePlayer.defaultRate).toEventually(equal(2)) - expect(player.queuePlayer.rate).toEventually(equal(2)) - } - - func testSpeedUpdateWhilePausedMustUpdateRate() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.playbackState).toEventually(equal(.paused)) - - player.setDesiredPlaybackSpeed(2) - player.play() - - expect(player.queuePlayer.defaultRate).toEventually(equal(2)) - expect(player.queuePlayer.rate).toEventually(equal(2)) - } - - func testSpeedUpdateMustNotResumePlayback() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.playbackState).toEventually(equal(.paused)) - player.setDesiredPlaybackSpeed(2) - expect(player.playbackState).toAlways(equal(.paused), until: .seconds(2)) - } - - func testPlayMustNotResetSpeed() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - player.setDesiredPlaybackSpeed(2) - player.play() - expect(player.effectivePlaybackSpeed).toEventually(equal(2)) - } - - func testRateChangeMustNotUpdatePlaybackSpeedOutsideAVPlayerViewController() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - player.queuePlayer.rate = 2 - expect(player.effectivePlaybackSpeed).toAlways(equal(1), until: .seconds(2)) - } - - func testNoDesiredUpdateIsIgnored() { - let player = Player() - expectAtLeastEqualPublished(values: [ - .value(1), - .value(2), - .value(2) - ], from: player.desiredPlaybackSpeedUpdatePublisher()) { - player.setDesiredPlaybackSpeed(1) - player.setDesiredPlaybackSpeed(2) - player.setDesiredPlaybackSpeed(2) - } - } -} diff --git a/Tests/PlayerTests/Player/TextStyleRulesTests.swift b/Tests/PlayerTests/Player/TextStyleRulesTests.swift deleted file mode 100644 index 8507ce40..00000000 --- a/Tests/PlayerTests/Player/TextStyleRulesTests.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble -import PillarboxStreams - -final class TextStyleRulesTests: TestCase { - private static let textStyleRules = [ - AVTextStyleRule(textMarkupAttributes: [ - kCMTextMarkupAttribute_ForegroundColorARGB: [1, 1, 0, 0], - kCMTextMarkupAttribute_ItalicStyle: true - ]) - ] - - func testDefaultWithEmptyPlayer() { - let player = Player() - expect(player.textStyleRules).to(beEmpty()) - } - - func testDefaultWithLoadedPlayer() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.textStyleRules).to(beEmpty()) - expect(player.queuePlayer.currentItem?.textStyleRules).to(beEmpty()) - } - - func testStyleUpdate() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - player.textStyleRules = Self.textStyleRules - expect(player.textStyleRules).to(equal(Self.textStyleRules)) - expect(player.queuePlayer.currentItem?.textStyleRules).to(equal(Self.textStyleRules)) - } - - func testStylePreservedBetweenItems() { - let player = Player(items: [ - .simple(url: Stream.shortOnDemand.url), - .simple(url: Stream.onDemand.url) - ]) - player.textStyleRules = Self.textStyleRules - player.advanceToNextItem() - expect(player.queuePlayer.currentItem?.textStyleRules).to(equal(Self.textStyleRules)) - } -} diff --git a/Tests/PlayerTests/PlayerItem/PlayerItemAssetPublisherTests.swift b/Tests/PlayerTests/PlayerItem/PlayerItemAssetPublisherTests.swift deleted file mode 100644 index f56e3c92..00000000 --- a/Tests/PlayerTests/PlayerItem/PlayerItemAssetPublisherTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import PillarboxCircumspect -import PillarboxStreams - -final class PlayerItemAssetPublisherTests: TestCase { - func testNoLoad() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - expectSimilarPublished( - values: [.loading], - from: item.$content.map(\.resource), - during: .milliseconds(500) - ) - } - - func testLoad() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - expectSimilarPublished( - values: [.loading, .simple(url: Stream.onDemand.url)], - from: item.$content.map(\.resource), - during: .milliseconds(500) - ) { - PlayerItem.load(for: item.id) - } - } - - func testReload() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - expectSimilarPublished( - values: [.loading, .simple(url: Stream.onDemand.url)], - from: item.$content.map(\.resource), - during: .milliseconds(500) - ) { - PlayerItem.load(for: item.id) - } - - expectSimilarPublishedNext( - values: [.simple(url: Stream.onDemand.url)], - from: item.$content.map(\.resource), - during: .milliseconds(500) - ) { - PlayerItem.reload(for: item.id) - } - } -} diff --git a/Tests/PlayerTests/PlayerItem/PlayerItemTests.swift b/Tests/PlayerTests/PlayerItem/PlayerItemTests.swift deleted file mode 100644 index 14704a8f..00000000 --- a/Tests/PlayerTests/PlayerItem/PlayerItemTests.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class PlayerItemTests: TestCase { - private static let limits = PlayerLimits( - preferredPeakBitRate: 100, - preferredPeakBitRateForExpensiveNetworks: 200, - preferredMaximumResolution: .init(width: 100, height: 200), - preferredMaximumResolutionForExpensiveNetworks: .init(width: 300, height: 400) - ) - - func testSimpleItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - PlayerItem.load(for: item.id) - expect(item.content.resource).toEventually(equal(.simple(url: Stream.onDemand.url))) - let playerItem = item.content.playerItem(configuration: .default, limits: .none) - expect(playerItem.preferredForwardBufferDuration).to(equal(0)) - expect(playerItem.preferredPeakBitRate).to(equal(0)) - expect(playerItem.preferredPeakBitRateForExpensiveNetworks).to(equal(0)) - expect(playerItem.preferredMaximumResolution).to(equal(.zero)) - expect(playerItem.preferredMaximumResolutionForExpensiveNetworks).to(equal(.zero)) - } - - func testSimpleItemWithConfiguration() { - let item = PlayerItem.simple(url: Stream.onDemand.url, configuration: .init(preferredForwardBufferDuration: 4)) - PlayerItem.load(for: item.id) - expect(item.content.resource).toEventually(equal(.simple(url: Stream.onDemand.url))) - expect(item.content.playerItem(configuration: .default, limits: .none).preferredForwardBufferDuration).to(equal(4)) - } - - func testSimpleItemWithLimits() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - PlayerItem.load(for: item.id) - expect(item.content.resource).toEventually(equal(.simple(url: Stream.onDemand.url))) - let playerItem = item.content.playerItem(configuration: .default, limits: Self.limits) - expect(playerItem.preferredPeakBitRate).to(equal(100)) - expect(playerItem.preferredPeakBitRateForExpensiveNetworks).to(equal(200)) - expect(playerItem.preferredMaximumResolution).to(equal(.init(width: 100, height: 200))) - expect(playerItem.preferredMaximumResolutionForExpensiveNetworks).to(equal(.init(width: 300, height: 400))) - } - - func testCustomItem() { - let delegate = ResourceLoaderDelegateMock() - let item = PlayerItem.custom(url: Stream.onDemand.url, delegate: delegate) - PlayerItem.load(for: item.id) - expect(item.content.resource).toEventually(equal(.custom(url: Stream.onDemand.url, delegate: delegate))) - let playerItem = item.content.playerItem(configuration: .default, limits: .none) - expect(playerItem.preferredForwardBufferDuration).to(equal(0)) - expect(playerItem.preferredPeakBitRate).to(equal(0)) - expect(playerItem.preferredPeakBitRateForExpensiveNetworks).to(equal(0)) - expect(playerItem.preferredMaximumResolution).to(equal(.zero)) - expect(playerItem.preferredMaximumResolutionForExpensiveNetworks).to(equal(.zero)) - } - - func testCustomItemWithConfiguration() { - let delegate = ResourceLoaderDelegateMock() - let item = PlayerItem.custom( - url: Stream.onDemand.url, - delegate: delegate, - configuration: .init(preferredForwardBufferDuration: 4) - ) - PlayerItem.load(for: item.id) - expect(item.content.resource).toEventually(equal(.custom(url: Stream.onDemand.url, delegate: delegate))) - expect(item.content.playerItem( - configuration: .default, - limits: .none - ).preferredForwardBufferDuration).to(equal(4)) - } - - func testCustomItemWithLimits() { - let delegate = ResourceLoaderDelegateMock() - let item = PlayerItem.custom(url: Stream.onDemand.url, delegate: delegate) - PlayerItem.load(for: item.id) - expect(item.content.resource).toEventually(equal(.custom(url: Stream.onDemand.url, delegate: delegate))) - let playerItem = item.content.playerItem(configuration: .default, limits: Self.limits) - expect(playerItem.preferredPeakBitRate).to(equal(100)) - expect(playerItem.preferredPeakBitRateForExpensiveNetworks).to(equal(200)) - expect(playerItem.preferredMaximumResolution).to(equal(.init(width: 100, height: 200))) - expect(playerItem.preferredMaximumResolutionForExpensiveNetworks).to(equal(.init(width: 300, height: 400))) - } - - func testEncryptedItem() { - let delegate = ContentKeySessionDelegateMock() - let item = PlayerItem.encrypted(url: Stream.onDemand.url, delegate: delegate) - PlayerItem.load(for: item.id) - expect(item.content.resource).toEventually(equal(.encrypted(url: Stream.onDemand.url, delegate: delegate))) - let playerItem = item.content.playerItem(configuration: .default, limits: .none) - expect(playerItem.preferredForwardBufferDuration).to(equal(0)) - expect(playerItem.preferredPeakBitRate).to(equal(0)) - expect(playerItem.preferredPeakBitRateForExpensiveNetworks).to(equal(0)) - expect(playerItem.preferredMaximumResolution).to(equal(.zero)) - expect(playerItem.preferredMaximumResolutionForExpensiveNetworks).to(equal(.zero)) - } - - func testEncryptedItemWithConfiguration() { - let delegate = ContentKeySessionDelegateMock() - let item = PlayerItem.encrypted( - url: Stream.onDemand.url, - delegate: delegate, - configuration: .init(preferredForwardBufferDuration: 4) - ) - PlayerItem.load(for: item.id) - expect(item.content.resource).toEventually(equal(.encrypted(url: Stream.onDemand.url, delegate: delegate))) - expect(item.content.playerItem( - configuration: .default, - limits: .none - ).preferredForwardBufferDuration).to(equal(4)) - } - - func testEncryptedItemWithNonStandardPlayerConfiguration() { - let delegate = ContentKeySessionDelegateMock() - let item = PlayerItem.encrypted(url: Stream.onDemand.url, delegate: delegate) - PlayerItem.load(for: item.id) - expect(item.content.resource).toEventually(equal(.encrypted(url: Stream.onDemand.url, delegate: delegate))) - let playerItem = item.content.playerItem(configuration: .default, limits: Self.limits) - expect(playerItem.preferredPeakBitRate).to(equal(100)) - expect(playerItem.preferredPeakBitRateForExpensiveNetworks).to(equal(200)) - expect(playerItem.preferredMaximumResolution).to(equal(.init(width: 100, height: 200))) - expect(playerItem.preferredMaximumResolutionForExpensiveNetworks).to(equal(.init(width: 300, height: 400))) - } -} diff --git a/Tests/PlayerTests/Playlist/CurrentItemTests.swift b/Tests/PlayerTests/Playlist/CurrentItemTests.swift deleted file mode 100644 index 72079426..00000000 --- a/Tests/PlayerTests/Playlist/CurrentItemTests.swift +++ /dev/null @@ -1,191 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class CurrentItemTests: TestCase { - func testCurrentItem() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(items: [item1, item2]) - expectAtLeastEqualPublished( - values: [item1, item2, nil], - from: player.changePublisher(at: \.currentItem).removeDuplicates() - ) { - player.play() - } - } - - func testCurrentItemWithFirstFailedItem() { - let item1 = PlayerItem.simple(url: Stream.unavailable.url) - let item2 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(items: [item1, item2]) - expectEqualPublished( - values: [item1], - from: player.changePublisher(at: \.currentItem).removeDuplicates(), - during: .milliseconds(500) - ) { - player.play() - } - } - - func testCurrentItemWithMiddleFailedItem() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.unavailable.url) - let item3 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(items: [item1, item2, item3]) - expectEqualPublished( - values: [item1, item2], - from: player.changePublisher(at: \.currentItem).removeDuplicates(), - during: .seconds(2) - ) { - player.play() - } - } - - func testCurrentItemWithLastFailedItem() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(items: [item1, item2]) - expectAtLeastEqualPublished( - values: [item1, item2], - from: player.changePublisher(at: \.currentItem).removeDuplicates() - ) { - player.play() - } - } - - func testCurrentItemWithFirstItemRemoved() { - let item1 = PlayerItem.simple(url: Stream.unavailable.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.error).toEventuallyNot(beNil()) - player.remove(item1) - expect(player.currentItem).toAlways(equal(item2), until: .seconds(1)) - } - - func testCurrentItemWithSecondItemRemoved() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.unavailable.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - expect(player.currentItem).toEventually(equal(item2)) - expect(player.error).toEventuallyNot(beNil()) - player.remove(item2) - expect(player.currentItem).toAlways(equal(item3), until: .seconds(1)) - } - - func testCurrentItemWithFailedItem() { - let item = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(item: item) - expectEqualPublished( - values: [item], - from: player.changePublisher(at: \.currentItem).removeDuplicates(), - during: .milliseconds(500) - ) - } - - func testCurrentItemWithEmptyPlayer() { - let player = Player() - expect(player.currentItem).to(beNil()) - } - - func testSlowFirstCurrentItem() { - let item1 = PlayerItem.mock(url: Stream.shortOnDemand.url, loadedAfter: 1) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expectAtLeastEqualPublished( - values: [item1, item2], - from: player.changePublisher(at: \.currentItem).removeDuplicates() - ) { - player.play() - } - } - - func testCurrentItemAfterPlayerEnded() { - let item = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(items: [item]) - expectAtLeastEqualPublished( - values: [item, nil], - from: player.changePublisher(at: \.currentItem).removeDuplicates() - ) { - player.play() - } - } - - func testSetCurrentItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(items: [item1, item2]) - expectEqualPublished( - values: [item1, item2], - from: player.changePublisher(at: \.currentItem).removeDuplicates(), - during: .milliseconds(500) - ) { - player.currentItem = item2 - } - } - - func testSetCurrentItemUpdatePlayerCurrentItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(items: [item1, item2]) - player.currentItem = item2 - expect(player.queuePlayer.currentItem?.url).toEventually(equal(Stream.shortOnDemand.url)) - } - - func testPlayerPreloadedItemCount() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let item4 = PlayerItem.simple(url: Stream.onDemand.url) - let item5 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3, item4, item5]) - player.currentItem = item3 - - let items = player.queuePlayer.items() - expect(items).to(haveCount(player.configuration.preloadedItems)) - } - - func testSetCurrentItemWithUnknownItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item3 = PlayerItem.simple(url: Stream.mediumOnDemand.url) - let player = Player(items: [item1, item2]) - player.currentItem = item3 - expect(player.currentItem).to(equal(item3)) - expect(player.items).to(equalDiff([item3, item2])) - } - - func testSetCurrentItemToNil() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expect(player.currentItem).to(equal(item)) - player.currentItem = nil - expect(player.currentItem).to(beNil()) - expect(player.items).to(equalDiff([item])) - expect(player.queuePlayer.items()).to(beEmpty()) - } - - func testSetCurrentItemToSameItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item]) - player.play() - expect(player.time().seconds).toEventually(beGreaterThan(1)) - player.pause() - player.currentItem = item - expect(player.playbackState).toAlways(equal(.paused), until: .seconds(1)) - expect(player.currentItem).to(equal(item)) - expect(player.items).to(equalDiff([item])) - expect(player.time().seconds).to(equal(0)) - } -} diff --git a/Tests/PlayerTests/Playlist/ItemInsertionAfterTests.swift b/Tests/PlayerTests/Playlist/ItemInsertionAfterTests.swift deleted file mode 100644 index 7452d337..00000000 --- a/Tests/PlayerTests/Playlist/ItemInsertionAfterTests.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class ItemInsertionAfterTests: TestCase { - func testInsertItemAfterNextItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) - expect(player.insert(insertedItem, after: item2)).to(beTrue()) - expect(player.items).to(equalDiff([item1, item2, insertedItem, item3])) - } - - func testInsertItemAfterCurrentItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) - expect(player.insert(insertedItem, after: item2)).to(beTrue()) - expect(player.items).to(equalDiff([item1, item2, insertedItem, item3])) - } - - func testInsertItemAfterPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - player.advanceToNextItem() - let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) - expect(player.insert(insertedItem, after: item2)).to(beTrue()) - expect(player.items).to(equalDiff([item1, item2, insertedItem, item3])) - } - - func testInsertItemAfterLastItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) - expect(player.insert(insertedItem, after: item2)).to(beTrue()) - expect(player.items).to(equalDiff([item1, item2, insertedItem])) - } - - func testInsertItemAfterIdenticalItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.insert(item1, after: item2)).to(beFalse()) - expect(player.items).to(equalDiff([item1, item2])) - } - - func testInsertItemAfterForeignItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) - let foreignItem = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item]) - expect(player.insert(insertedItem, after: foreignItem)).to(beFalse()) - expect(player.items).to(equalDiff([item])) - } - - func testInsertItemAfterNil() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item]) - let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) - expect(player.insert(insertedItem, after: nil)).to(beTrue()) - expect(player.items).to(equalDiff([item, insertedItem])) - } - - func testAppendItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item]) - let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) - expect(player.append(insertedItem)).to(beTrue()) - expect(player.items).to(equalDiff([item, insertedItem])) - } -} diff --git a/Tests/PlayerTests/Playlist/ItemInsertionBeforeTests.swift b/Tests/PlayerTests/Playlist/ItemInsertionBeforeTests.swift deleted file mode 100644 index f36f303b..00000000 --- a/Tests/PlayerTests/Playlist/ItemInsertionBeforeTests.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class ItemInsertionBeforeTests: TestCase { - func testInsertItemBeforeNextItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) - expect(player.insert(insertedItem, before: item2)).to(beTrue()) - expect(player.items).to(equalDiff([item1, insertedItem, item2, item3])) - } - - func testInsertItemBeforeCurrentItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) - expect(player.insert(insertedItem, before: item2)).to(beTrue()) - expect(player.items).to(equalDiff([item1, insertedItem, item2, item3])) - } - - func testInsertItemBeforePreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - player.advanceToNextItem() - let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) - expect(player.insert(insertedItem, before: item2)).to(beTrue()) - expect(player.items).to(equalDiff([item1, insertedItem, item2, item3])) - } - - func testInsertItemBeforeFirstItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) - expect(player.insert(insertedItem, before: item1)).to(beTrue()) - expect(player.items).to(equalDiff([insertedItem, item1, item2])) - } - - func testInsertItemBeforeIdenticalItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.insert(item1, before: item2)).to(beFalse()) - expect(player.items).to(equalDiff([item1, item2])) - } - - func testInsertItemBeforeForeignItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) - let foreignItem = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item]) - expect(player.insert(insertedItem, before: foreignItem)).to(beFalse()) - expect(player.items).to(equalDiff([item])) - } - - func testInsertItemBeforeNil() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item]) - let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) - expect(player.insert(insertedItem, before: nil)).to(beTrue()) - expect(player.items).to(equalDiff([insertedItem, item])) - } - - func testPrependItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item]) - let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) - expect(player.prepend(insertedItem)).to(beTrue()) - expect(player.items).to(equalDiff([insertedItem, item])) - } -} diff --git a/Tests/PlayerTests/Playlist/ItemMoveAfterTests.swift b/Tests/PlayerTests/Playlist/ItemMoveAfterTests.swift deleted file mode 100644 index df0b0945..00000000 --- a/Tests/PlayerTests/Playlist/ItemMoveAfterTests.swift +++ /dev/null @@ -1,147 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class ItemMoveAfterTests: TestCase { - func testMovePreviousItemAfterNextItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - expect(player.move(item1, after: item3)).to(beTrue()) - expect(player.items).to(equalDiff([item2, item3, item1])) - } - - func testMovePreviousItemAfterCurrentItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - player.advanceToNextItem() - expect(player.move(item1, after: item3)).to(beTrue()) - expect(player.items).to(equalDiff([item2, item3, item1])) - } - - func testMovePreviousItemAfterPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - player.advanceToNextItem() - expect(player.move(item1, after: item2)).to(beTrue()) - expect(player.items).to(equalDiff([item2, item1, item3])) - } - - func testMoveCurrentItemAfterNextItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - expect(player.move(item1, after: item2)).to(beTrue()) - expect(player.items).to(equalDiff([item2, item1, item3])) - } - - func testMoveCurrentItemAfterPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - player.advanceToNextItem() - expect(player.move(item3, after: item1)).to(beTrue()) - expect(player.items).to(equalDiff([item1, item3, item2])) - } - - func testMoveNextItemAfterPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - expect(player.move(item3, after: item1)).to(beTrue()) - expect(player.items).to(equalDiff([item1, item3, item2])) - } - - func testMoveNextItemAfterCurrentItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - expect(player.move(item3, after: item1)).to(beTrue()) - expect(player.items).to(equalDiff([item1, item3, item2])) - } - - func testMoveNextItemAfterNextItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - expect(player.move(item2, after: item3)).to(beTrue()) - expect(player.items).to(equalDiff([item1, item3, item2])) - } - - func testMoveItemAfterIdenticalItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item]) - expect(player.move(item, after: item)).to(beFalse()) - expect(player.items).to(equalDiff([item])) - } - - func testMoveItemAfterItemAlreadyAtExpectedLocation() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.move(item2, after: item1)).to(beFalse()) - expect(player.items).to(equalDiff([item1, item2])) - } - - func testMoveForeignItemAfterItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let foreignItem = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item]) - expect(player.move(foreignItem, after: item)).to(beFalse()) - expect(player.items).to(equalDiff([item])) - } - - func testMoveItemAfterForeignItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let foreignItem = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item]) - expect(player.move(item, after: foreignItem)).to(beFalse()) - expect(player.items).to(equalDiff([item])) - } - - func testMoveItemAfterLastItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.move(item1, after: item2)).to(beTrue()) - expect(player.items).to(equalDiff([item2, item1])) - } - - func testMoveItemAfterNil() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.move(item1, after: nil)).to(beTrue()) - expect(player.items).to(equalDiff([item2, item1])) - } - - func testMoveLastItemAfterNil() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item]) - expect(player.move(item, after: nil)).to(beFalse()) - expect(player.items).to(equalDiff([item])) - } -} diff --git a/Tests/PlayerTests/Playlist/ItemMoveBeforeTests.swift b/Tests/PlayerTests/Playlist/ItemMoveBeforeTests.swift deleted file mode 100644 index c9cabc1d..00000000 --- a/Tests/PlayerTests/Playlist/ItemMoveBeforeTests.swift +++ /dev/null @@ -1,147 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class ItemMoveBeforeTests: TestCase { - func testMovePreviousItemBeforeNextItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - expect(player.move(item1, before: item3)).to(beTrue()) - expect(player.items).to(equalDiff([item2, item1, item3])) - } - - func testMovePreviousItemBeforeCurrentItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - player.advanceToNextItem() - expect(player.move(item1, before: item3)).to(beTrue()) - expect(player.items).to(equalDiff([item2, item1, item3])) - } - - func testMovePreviousItemBeforePreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - player.advanceToNextItem() - expect(player.move(item2, before: item1)).to(beTrue()) - expect(player.items).to(equalDiff([item2, item1, item3])) - } - - func testMoveCurrentItemBeforeNextItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - expect(player.move(item1, before: item3)).to(beTrue()) - expect(player.items).to(equalDiff([item2, item1, item3])) - } - - func testMoveCurrentItemBeforePreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - expect(player.move(item2, before: item1)).to(beTrue()) - expect(player.items).to(equalDiff([item2, item1, item3])) - } - - func testMoveNextItemBeforePreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - expect(player.move(item3, before: item1)).to(beTrue()) - expect(player.items).to(equalDiff([item3, item1, item2])) - } - - func testMoveNextItemBeforeCurrentItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - expect(player.move(item3, before: item2)).to(beTrue()) - expect(player.items).to(equalDiff([item1, item3, item2])) - } - - func testMoveNextItemBeforeNextItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - expect(player.move(item3, before: item2)).to(beTrue()) - expect(player.items).to(equalDiff([item1, item3, item2])) - } - - func testMoveItemBeforeIdenticalItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item]) - expect(player.move(item, before: item)).to(beFalse()) - expect(player.items).to(equalDiff([item])) - } - - func testMoveItemBeforeItemAlreadyAtExpectedLocation() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.move(item1, before: item2)).to(beFalse()) - expect(player.items).to(equalDiff([item1, item2])) - } - - func testMoveForeignItemBeforeItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let foreignItem = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item]) - expect(player.move(foreignItem, before: item)).to(beFalse()) - expect(player.items).to(equalDiff([item])) - } - - func testMoveBeforeForeignItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let foreignItem = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item]) - expect(player.move(item, before: foreignItem)).to(beFalse()) - expect(player.items).to(equalDiff([item])) - } - - func testMoveItemBeforeFirstItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.move(item2, before: item1)).to(beTrue()) - expect(player.items).to(equalDiff([item2, item1])) - } - - func testMoveBeforeNil() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.move(item2, before: nil)).to(beTrue()) - expect(player.items).to(equalDiff([item2, item1])) - } - - func testMoveFirstItemBeforeNil() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item]) - expect(player.move(item, before: nil)).to(beFalse()) - expect(player.items).to(equalDiff([item])) - } -} diff --git a/Tests/PlayerTests/Playlist/ItemNavigationBackwardChecksTests.swift b/Tests/PlayerTests/Playlist/ItemNavigationBackwardChecksTests.swift deleted file mode 100644 index 6ff6bc0b..00000000 --- a/Tests/PlayerTests/Playlist/ItemNavigationBackwardChecksTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class ItemNavigationBackwardChecksTests: TestCase { - func testCanReturnToPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - player.advanceToNextItem() - expect(player.canReturnToPreviousItem()).to(beTrue()) - } - - func testCannotReturnToPreviousItemAtFront() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.canReturnToPreviousItem()).to(beFalse()) - } - - func testCannotReturnToPreviousItemWhenEmpty() { - let player = Player() - expect(player.canReturnToPreviousItem()).to(beFalse()) - } - - func testWrapAtFrontWithRepeatAll() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - player.repeatMode = .all - expect(player.canReturnToPreviousItem()).to(beTrue()) - } -} diff --git a/Tests/PlayerTests/Playlist/ItemNavigationBackwardTests.swift b/Tests/PlayerTests/Playlist/ItemNavigationBackwardTests.swift deleted file mode 100644 index 174fc0fc..00000000 --- a/Tests/PlayerTests/Playlist/ItemNavigationBackwardTests.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class ItemNavigationBackwardTests: TestCase { - func testReturnToPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - player.advanceToNextItem() - player.returnToPreviousItem() - expect(player.currentItem).to(equal(item1)) - } - - func testReturnToPreviousItemAtFront() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - player.returnToPreviousItem() - expect(player.currentItem).to(equal(item1)) - } - - func testReturnToPreviousItemOnFailedItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.unavailable.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - player.returnToPreviousItem() - expect(player.currentItem).to(equal(item1)) - } - - func testWrapAtFrontWithRepeatAll() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - player.repeatMode = .all - player.returnToPreviousItem() - expect(player.currentItem).to(equal(item2)) - } -} diff --git a/Tests/PlayerTests/Playlist/ItemNavigationForwardChecksTests.swift b/Tests/PlayerTests/Playlist/ItemNavigationForwardChecksTests.swift deleted file mode 100644 index 98a6d21c..00000000 --- a/Tests/PlayerTests/Playlist/ItemNavigationForwardChecksTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class ItemNavigationForwardChecksTests: TestCase { - func testCanAdvanceToNextItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.canAdvanceToNextItem()).to(beTrue()) - } - - func testCannotAdvanceToNextItemAtBack() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - player.advanceToNextItem() - expect(player.canAdvanceToNextItem()).to(beFalse()) - } - - func testCannotAdvanceToNextItemWhenEmpty() { - let player = Player() - expect(player.canAdvanceToNextItem()).to(beFalse()) - } - - func testWrapAtBackWithRepeatAll() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - player.repeatMode = .all - expect(player.canAdvanceToNextItem()).to(beTrue()) - } -} diff --git a/Tests/PlayerTests/Playlist/ItemNavigationForwardTests.swift b/Tests/PlayerTests/Playlist/ItemNavigationForwardTests.swift deleted file mode 100644 index 92377e48..00000000 --- a/Tests/PlayerTests/Playlist/ItemNavigationForwardTests.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class ItemNavigationForwardTests: TestCase { - func testAdvanceToNextItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - player.advanceToNextItem() - expect(player.currentItem).to(equal(item2)) - } - - func testAdvanceToNextItemAtBack() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - player.advanceToNextItem() - player.advanceToNextItem() - expect(player.currentItem).to(equal(item2)) - } - - func testAdvanceToNextItemOnFailedItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.unavailable.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - player.advanceToNextItem() - expect(player.currentItem).to(equal(item3)) - } - - func testPlayerPreloadedItemCount() { - let player = Player(items: [ - PlayerItem.simple(url: Stream.onDemand.url), - PlayerItem.simple(url: Stream.squareOnDemand.url), - PlayerItem.simple(url: Stream.mediumOnDemand.url), - PlayerItem.simple(url: Stream.onDemand.url), - PlayerItem.simple(url: Stream.shortOnDemand.url) - ]) - player.advanceToNextItem() - - let items = player.queuePlayer.items() - expect(items).to(haveCount(player.configuration.preloadedItems)) - } - - func testWrapAtBackWithRepeatAll() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - player.repeatMode = .all - player.advanceToNextItem() - player.advanceToNextItem() - expect(player.currentItem).to(equal(item1)) - } -} diff --git a/Tests/PlayerTests/Playlist/ItemRemovalTests.swift b/Tests/PlayerTests/Playlist/ItemRemovalTests.swift deleted file mode 100644 index 583300d3..00000000 --- a/Tests/PlayerTests/Playlist/ItemRemovalTests.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class ItemRemovalTests: TestCase { - func testRemovePreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - player.remove(item1) - expect(player.items).to(equalDiff([item2, item3])) - } - - func testRemoveCurrentItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - player.remove(item2) - expect(player.currentItem).to(equal(item3)) - expect(player.items).to(equalDiff([item1, item3])) - } - - func testRemoveNextItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - player.remove(item3) - expect(player.items).to(equalDiff([item1, item2])) - } - - func testRemoveForeignItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let foreignItem = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item]) - player.remove(foreignItem) - expect(player.items).to(equalDiff([item])) - } - - func testRemoveAllItems() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.removeAllItems() - expect(player.items).to(beEmpty()) - expect(player.currentItem).to(beNil()) - } -} diff --git a/Tests/PlayerTests/Playlist/ItemsTests.swift b/Tests/PlayerTests/Playlist/ItemsTests.swift deleted file mode 100644 index bbcb8f90..00000000 --- a/Tests/PlayerTests/Playlist/ItemsTests.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class ItemsTests: TestCase { - func testItemsOnFirstItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - expect(player.items).to(equalDiff([item1, item2, item3])) - expect(player.previousItems).to(beEmpty()) - expect(player.nextItems).to(equalDiff([item2, item3])) - } - - func testItemsOnMiddleItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - expect(player.items).to(equalDiff([item1, item2, item3])) - expect(player.previousItems).to(equalDiff([item1])) - expect(player.nextItems).to(equalDiff([item3])) - } - - func testItemsOnLastItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.advanceToNextItem() - player.advanceToNextItem() - expect(player.items).to(equalDiff([item1, item2, item3])) - expect(player.previousItems).to(equalDiff([item1, item2])) - expect(player.nextItems).to(beEmpty()) - } - - func testEmpty() { - let player = Player() - expect(player.currentItem).to(beNil()) - expect(player.items).to(beEmpty()) - expect(player.nextItems).to(beEmpty()) - expect(player.previousItems).to(beEmpty()) - } - - func testRemoveAll() { - let item = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(item: item) - expect(player.currentItem).to(equal(item)) - player.removeAllItems() - expect(player.currentItem).to(beNil()) - } - - func testAppendAfterRemoveAll() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - player.removeAllItems() - let item = PlayerItem.simple(url: Stream.onDemand.url) - player.append(item) - expect(player.currentItem).to(equal(item)) - } -} diff --git a/Tests/PlayerTests/Playlist/ItemsUpdateTests.swift b/Tests/PlayerTests/Playlist/ItemsUpdateTests.swift deleted file mode 100644 index c98459d4..00000000 --- a/Tests/PlayerTests/Playlist/ItemsUpdateTests.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class ItemsUpdateTests: TestCase { - func testUpdateWithCurrentItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let item4 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.items = [item4, item3, item1] - expect(player.items).to(equalDiff([item4, item3, item1])) - expect(player.currentItem).to(equal(item1)) - } - - func testUpdateWithCurrentItemMustNotInterruptPlayback() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemandWithForcedAndUnforcedLegibleOptions.url) - let item3 = PlayerItem.simple(url: Stream.onDemandWithSingleAudibleOption.url) - let item4 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(items: [item1, item2, item3]) - expect(player.queuePlayer.currentItem?.url).toEventually(equal(Stream.onDemand.url)) - player.items = [item4, item3, item1] - expect(player.queuePlayer.currentItem?.url).toAlways(equal(Stream.onDemand.url), until: .seconds(2)) - } - - func testUpdateWithoutCurrentItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let item3 = PlayerItem.simple(url: Stream.onDemand.url) - let item4 = PlayerItem.simple(url: Stream.onDemand.url) - let item5 = PlayerItem.simple(url: Stream.onDemand.url) - let item6 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2, item3]) - player.items = [item4, item5, item6] - expect(player.items).to(equalDiff([item4, item5, item6])) - expect(player.currentItem).to(equal(item4)) - } -} diff --git a/Tests/PlayerTests/Playlist/NavigationBackwardChecksTests.swift b/Tests/PlayerTests/Playlist/NavigationBackwardChecksTests.swift deleted file mode 100644 index 15a5e2c8..00000000 --- a/Tests/PlayerTests/Playlist/NavigationBackwardChecksTests.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble -import PillarboxStreams - -final class NavigationBackwardChecksTests: TestCase { - private static func configuration() -> PlayerConfiguration { - .init(navigationMode: .immediate) - } - - func testCannotReturnForOnDemandAtBeginningWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item, configuration: Self.configuration()) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.canReturnToPrevious()).to(beFalse()) - } - - func testCanReturnForOnDemandNearBeginningWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item, configuration: Self.configuration()) - expect(player.streamType).toEventually(equal(.onDemand)) - - waitUntil { done in - player.seek(at(CMTime(value: 1, timescale: 1))) { _ in - done() - } - } - - expect(player.canReturnToPrevious()).to(beFalse()) - } - - func testCanReturnForOnDemandAtBeginningWithPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2], configuration: Self.configuration()) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.canReturnToPrevious()).to(beTrue()) - } - - func testCanReturnForOnDemandNotAtBeginning() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2], configuration: Self.configuration()) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.onDemand)) - - waitUntil { done in - player.seek(at(CMTime(value: 5, timescale: 1))) { _ in - done() - } - } - - expect(player.canReturnToPrevious()).to(beTrue()) - } - - func testCanReturnForLiveWithPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.live.url) - let player = Player(items: [item1, item2], configuration: Self.configuration()) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.live)) - expect(player.canReturnToPrevious()).to(beTrue()) - } - - func testCannotReturnForLiveWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.live.url) - let player = Player(item: item, configuration: Self.configuration()) - expect(player.streamType).toEventually(equal(.live)) - expect(player.canReturnToPrevious()).to(beFalse()) - } - - func testCanReturnForDvrWithPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(items: [item1, item2], configuration: Self.configuration()) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.dvr)) - expect(player.canReturnToPrevious()).to(beTrue()) - } - - func testCannotReturnForDvrWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(item: item, configuration: Self.configuration()) - expect(player.streamType).toEventually(equal(.dvr)) - expect(player.canReturnToPrevious()).to(beFalse()) - } - - func testCanReturnForUnknownWithPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(items: [item1, item2], configuration: Self.configuration()) - player.advanceToNextItem() - expect(player.streamType).to(equal(.unknown)) - expect(player.canReturnToPrevious()).to(beTrue()) - } - - func testCannotReturnForUnknownWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(item: item, configuration: Self.configuration()) - expect(player.streamType).to(equal(.unknown)) - expect(player.canReturnToPrevious()).to(beFalse()) - } -} diff --git a/Tests/PlayerTests/Playlist/NavigationBackwardTests.swift b/Tests/PlayerTests/Playlist/NavigationBackwardTests.swift deleted file mode 100644 index b7c086c1..00000000 --- a/Tests/PlayerTests/Playlist/NavigationBackwardTests.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble -import PillarboxStreams - -final class NavigationBackwardTests: TestCase { - private static func configuration() -> PlayerConfiguration { - .init(navigationMode: .immediate) - } - - func testReturnForOnDemandAtBeginningWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item, configuration: Self.configuration()) - expect(player.streamType).toEventually(equal(.onDemand)) - player.returnToPrevious() - expect(player.currentItem).to(equal(item)) - } - - func testReturnForOnDemandNearBeginningWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item, configuration: Self.configuration()) - expect(player.streamType).toEventually(equal(.onDemand)) - - waitUntil { done in - player.seek(at(CMTime(value: 1, timescale: 1))) { _ in - done() - } - } - - player.returnToPrevious() - expect(player.currentItem).to(equal(item)) - expect(player.time()).toNever(equal(.zero), until: .seconds(3)) - } - - func testReturnForOnDemandAtBeginningWithPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2], configuration: Self.configuration()) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.onDemand)) - player.returnToPrevious() - expect(player.currentItem).to(equal(item1)) - } - - func testReturnForOnDemandNotAtBeginning() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2], configuration: Self.configuration()) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.onDemand)) - - waitUntil { done in - player.seek(at(CMTime(value: 5, timescale: 1))) { _ in - done() - } - } - player.returnToPrevious() - expect(player.currentItem).to(equal(item1)) - expect(player.time()).toEventually(equal(.zero)) - } - - func testReturnForLiveWithPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.live.url) - let player = Player(items: [item1, item2], configuration: Self.configuration()) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.live)) - player.returnToPreviousItem() - expect(player.currentItem).to(equal(item1)) - } - - func testReturnForLiveWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.live.url) - let player = Player(item: item, configuration: Self.configuration()) - expect(player.streamType).toEventually(equal(.live)) - player.returnToPreviousItem() - expect(player.currentItem).to(equal(item)) - } - - func testReturnForDvrWithPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(items: [item1, item2], configuration: Self.configuration()) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.dvr)) - player.returnToPreviousItem() - expect(player.currentItem).to(equal(item1)) - } - - func testReturnForDvrWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(item: item, configuration: Self.configuration()) - expect(player.streamType).toEventually(equal(.dvr)) - player.returnToPreviousItem() - expect(player.currentItem).to(equal(item)) - } - - func testReturnForUnknownWithPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(items: [item1, item2], configuration: Self.configuration()) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.unknown)) - player.returnToPreviousItem() - expect(player.currentItem).to(equal(item1)) - } - - func testReturnForUnknownWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(item: item, configuration: Self.configuration()) - expect(player.streamType).toEventually(equal(.unknown)) - player.returnToPreviousItem() - expect(player.currentItem).to(equal(item)) - } - - func testPlayerPreloadedItemCount() { - let player = Player(items: [ - PlayerItem.simple(url: Stream.onDemand.url), - PlayerItem.simple(url: Stream.squareOnDemand.url), - PlayerItem.simple(url: Stream.mediumOnDemand.url), - PlayerItem.simple(url: Stream.onDemand.url), - PlayerItem.simple(url: Stream.shortOnDemand.url) - ]) - player.advanceToNextItem() - player.returnToPrevious() - - let items = player.queuePlayer.items() - expect(items).to(haveCount(player.configuration.preloadedItems)) - } -} diff --git a/Tests/PlayerTests/Playlist/NavigationForwardChecksTests.swift b/Tests/PlayerTests/Playlist/NavigationForwardChecksTests.swift deleted file mode 100644 index 2e075dc8..00000000 --- a/Tests/PlayerTests/Playlist/NavigationForwardChecksTests.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class NavigationForwardChecksTests: TestCase { - func testCanAdvanceForOnDemandWithNextItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.live.url) - let player = Player(items: [item1, item2]) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.canAdvanceToNext()).to(beTrue()) - } - - func testCannotAdvanceForOnDemandWithoutNextItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.canAdvanceToNext()).to(beFalse()) - } - - func testCanAdvanceForLiveWithNextItem() { - let item1 = PlayerItem.simple(url: Stream.live.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.streamType).toEventually(equal(.live)) - expect(player.canAdvanceToNext()).to(beTrue()) - } - - func testCannotAdvanceForLiveWithoutNextItem() { - let item = PlayerItem.simple(url: Stream.live.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.live)) - expect(player.canAdvanceToNext()).to(beFalse()) - } - - func testCanAdvanceForDvrWithNextItem() { - let item1 = PlayerItem.simple(url: Stream.dvr.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.streamType).toEventually(equal(.dvr)) - expect(player.canAdvanceToNext()).to(beTrue()) - } - - func testCannotAdvanceForDvrWithoutNextItem() { - let item = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.dvr)) - expect(player.canAdvanceToNext()).to(beFalse()) - } - - func testCanAdvanceForUnknownWithNextItem() { - let item1 = PlayerItem.simple(url: Stream.unavailable.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.streamType).to(equal(.unknown)) - expect(player.canAdvanceToNext()).to(beTrue()) - } - - func testCannotAdvanceForUnknownWithoutNextItem() { - let item = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(item: item) - expect(player.streamType).to(equal(.unknown)) - expect(player.canAdvanceToNext()).to(beFalse()) - } -} diff --git a/Tests/PlayerTests/Playlist/NavigationForwardTests.swift b/Tests/PlayerTests/Playlist/NavigationForwardTests.swift deleted file mode 100644 index 0e40acfd..00000000 --- a/Tests/PlayerTests/Playlist/NavigationForwardTests.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class NavigationForwardTests: TestCase { - func testAdvanceForOnDemandWithNextItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.live.url) - let player = Player(items: [item1, item2]) - expect(player.streamType).toEventually(equal(.onDemand)) - player.advanceToNext() - expect(player.currentItem).to(equal(item2)) - } - - func testAdvanceForOnDemandWithoutNextItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.onDemand)) - player.advanceToNext() - expect(player.currentItem).to(equal(item)) - } - - func testAdvanceForLiveWithNextItem() { - let item1 = PlayerItem.simple(url: Stream.live.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.streamType).toEventually(equal(.live)) - player.advanceToNext() - expect(player.currentItem).to(equal(item2)) - } - - func testAdvanceForLiveWithoutNextItem() { - let item = PlayerItem.simple(url: Stream.live.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.live)) - player.advanceToNext() - expect(player.currentItem).to(equal(item)) - } - - func testAdvanceForDvrWithNextItem() { - let item1 = PlayerItem.simple(url: Stream.dvr.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.streamType).toEventually(equal(.dvr)) - player.advanceToNext() - expect(player.currentItem).to(equal(item2)) - } - - func testAdvanceForDvrWithoutNextItem() { - let item = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.dvr)) - player.advanceToNext() - expect(player.currentItem).to(equal(item)) - } - - func testAdvanceForUnknownWithNextItem() { - let item1 = PlayerItem.simple(url: Stream.unavailable.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.streamType).to(equal(.unknown)) - player.advanceToNext() - expect(player.currentItem).to(equal(item2)) - } - - func testAdvanceForUnknownWithoutNextItem() { - let item = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(item: item) - expect(player.streamType).to(equal(.unknown)) - player.advanceToNext() - expect(player.currentItem).to(equal(item)) - } -} diff --git a/Tests/PlayerTests/Playlist/NavigationSmartBackwardChecksTests.swift b/Tests/PlayerTests/Playlist/NavigationSmartBackwardChecksTests.swift deleted file mode 100644 index 5eb6d1fe..00000000 --- a/Tests/PlayerTests/Playlist/NavigationSmartBackwardChecksTests.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble -import PillarboxStreams - -final class NavigationSmartBackwardChecksTests: TestCase { - func testCanReturnForOnDemandAtBeginningWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.canReturnToPrevious()).to(beTrue()) - } - - func testCanReturnForOnDemandNearBeginningWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.onDemand)) - - waitUntil { done in - player.seek(at(CMTime(value: 1, timescale: 1))) { _ in - done() - } - } - - expect(player.canReturnToPrevious()).to(beTrue()) - } - - func testCanReturnForOnDemandAtBeginningWithPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.canReturnToPrevious()).to(beTrue()) - } - - func testCanReturnForOnDemandNotAtBeginning() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.onDemand)) - - waitUntil { done in - player.seek(at(CMTime(value: 5, timescale: 1))) { _ in - done() - } - } - - expect(player.canReturnToPrevious()).to(beTrue()) - } - - func testCanReturnForLiveWithPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.live.url) - let player = Player(items: [item1, item2]) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.live)) - expect(player.canReturnToPrevious()).to(beTrue()) - } - - func testCannotReturnForLiveWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.live.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.live)) - expect(player.canReturnToPrevious()).to(beFalse()) - } - - func testCanReturnForDvrWithPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(items: [item1, item2]) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.dvr)) - expect(player.canReturnToPrevious()).to(beTrue()) - } - - func testCannotReturnForDvrWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.dvr)) - expect(player.canReturnToPrevious()).to(beFalse()) - } - - func testCanReturnForUnknownWithPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(items: [item1, item2]) - player.advanceToNextItem() - expect(player.streamType).to(equal(.unknown)) - expect(player.canReturnToPrevious()).to(beTrue()) - } - - func testCannotReturnForUnknownWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(item: item) - expect(player.streamType).to(equal(.unknown)) - expect(player.canReturnToPrevious()).to(beFalse()) - } -} diff --git a/Tests/PlayerTests/Playlist/NavigationSmartBackwardTests.swift b/Tests/PlayerTests/Playlist/NavigationSmartBackwardTests.swift deleted file mode 100644 index 222a445f..00000000 --- a/Tests/PlayerTests/Playlist/NavigationSmartBackwardTests.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble -import PillarboxStreams - -final class NavigationSmartBackwardTests: TestCase { - func testReturnForOnDemandAtBeginningWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.onDemand)) - player.returnToPrevious() - expect(player.currentItem).to(equal(item)) - } - - func testReturnForOnDemandNearBeginningWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.onDemand)) - - waitUntil { done in - player.seek(at(CMTime(value: 1, timescale: 1))) { _ in - done() - } - } - - player.returnToPrevious() - expect(player.currentItem).to(equal(item)) - expect(player.time()).toEventually(equal(.zero)) - } - - func testReturnForOnDemandAtBeginningWithPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.onDemand)) - player.returnToPrevious() - expect(player.currentItem).to(equal(item1)) - } - - func testReturnForOnDemandNotAtBeginning() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.onDemand)) - - waitUntil { done in - player.seek(at(CMTime(value: 5, timescale: 1))) { _ in - done() - } - } - player.returnToPrevious() - expect(player.currentItem).to(equal(item2)) - expect(player.time()).toEventually(equal(.zero)) - } - - func testReturnForLiveWithPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.live.url) - let player = Player(items: [item1, item2]) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.live)) - player.returnToPreviousItem() - expect(player.currentItem).to(equal(item1)) - } - - func testReturnForLiveWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.live.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.live)) - player.returnToPreviousItem() - expect(player.currentItem).to(equal(item)) - } - - func testReturnForDvrWithPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(items: [item1, item2]) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.dvr)) - player.returnToPreviousItem() - expect(player.currentItem).to(equal(item1)) - } - - func testReturnForDvrWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.dvr)) - player.returnToPreviousItem() - expect(player.currentItem).to(equal(item)) - } - - func testReturnForUnknownWithPreviousItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(items: [item1, item2]) - player.advanceToNextItem() - expect(player.streamType).toEventually(equal(.unknown)) - player.returnToPreviousItem() - expect(player.currentItem).to(equal(item1)) - } - - func testReturnForUnknownWithoutPreviousItem() { - let item = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.unknown)) - player.returnToPreviousItem() - expect(player.currentItem).to(equal(item)) - } -} diff --git a/Tests/PlayerTests/Playlist/NavigationSmartForwardChecksTests.swift b/Tests/PlayerTests/Playlist/NavigationSmartForwardChecksTests.swift deleted file mode 100644 index e6c1e496..00000000 --- a/Tests/PlayerTests/Playlist/NavigationSmartForwardChecksTests.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class NavigationSmartForwardChecksTests: TestCase { - func testCanAdvanceForOnDemandWithNextItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.live.url) - let player = Player(items: [item1, item2]) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.canAdvanceToNext()).to(beTrue()) - } - - func testCannotAdvanceForOnDemandWithoutNextItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.canAdvanceToNext()).to(beFalse()) - } - - func testCanAdvanceForLiveWithNextItem() { - let item1 = PlayerItem.simple(url: Stream.live.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.streamType).toEventually(equal(.live)) - expect(player.canAdvanceToNext()).to(beTrue()) - } - - func testCannotAdvanceForLiveWithoutNextItem() { - let item = PlayerItem.simple(url: Stream.live.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.live)) - expect(player.canAdvanceToNext()).to(beFalse()) - } - - func testCanAdvanceForDvrWithNextItem() { - let item1 = PlayerItem.simple(url: Stream.dvr.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.streamType).toEventually(equal(.dvr)) - expect(player.canAdvanceToNext()).to(beTrue()) - } - - func testCannotAdvanceForDvrWithoutNextItem() { - let item = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.dvr)) - expect(player.canAdvanceToNext()).to(beFalse()) - } - - func testCanAdvanceForUnknownWithNextItem() { - let item1 = PlayerItem.simple(url: Stream.unavailable.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.streamType).to(equal(.unknown)) - expect(player.canAdvanceToNext()).to(beTrue()) - } - - func testCannotAdvanceForUnknownWithoutNextItem() { - let item = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(item: item) - expect(player.streamType).to(equal(.unknown)) - expect(player.canAdvanceToNext()).to(beFalse()) - } -} diff --git a/Tests/PlayerTests/Playlist/NavigationSmartForwardTests.swift b/Tests/PlayerTests/Playlist/NavigationSmartForwardTests.swift deleted file mode 100644 index 6f579725..00000000 --- a/Tests/PlayerTests/Playlist/NavigationSmartForwardTests.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class NavigationSmartForwardTests: TestCase { - func testAdvanceForOnDemandWithNextItem() { - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let item2 = PlayerItem.simple(url: Stream.live.url) - let player = Player(items: [item1, item2]) - expect(player.streamType).toEventually(equal(.onDemand)) - player.advanceToNext() - expect(player.currentItem).to(equal(item2)) - } - - func testAdvanceForOnDemandWithoutNextItem() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.onDemand)) - player.advanceToNext() - expect(player.currentItem).to(equal(item)) - } - - func testAdvanceForLiveWithNextItem() { - let item1 = PlayerItem.simple(url: Stream.live.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.streamType).toEventually(equal(.live)) - player.advanceToNext() - expect(player.currentItem).to(equal(item2)) - } - - func testAdvanceForLiveWithoutNextItem() { - let item = PlayerItem.simple(url: Stream.live.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.live)) - player.advanceToNext() - expect(player.currentItem).to(equal(item)) - } - - func testAdvanceForDvrWithNextItem() { - let item1 = PlayerItem.simple(url: Stream.dvr.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.streamType).toEventually(equal(.dvr)) - player.advanceToNext() - expect(player.currentItem).to(equal(item2)) - } - - func testAdvanceForDvrWithoutNextItem() { - let item = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.dvr)) - player.advanceToNext() - expect(player.currentItem).to(equal(item)) - } - - func testAdvanceForUnknownWithNextItem() { - let item1 = PlayerItem.simple(url: Stream.unavailable.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - expect(player.streamType).to(equal(.unknown)) - player.advanceToNext() - expect(player.currentItem).to(equal(item2)) - } - - func testAdvanceForUnknownWithoutNextItem() { - let item = PlayerItem.simple(url: Stream.unavailable.url) - let player = Player(item: item) - expect(player.streamType).to(equal(.unknown)) - player.advanceToNext() - expect(player.currentItem).to(equal(item)) - } -} diff --git a/Tests/PlayerTests/Playlist/RepeatModeTests.swift b/Tests/PlayerTests/Playlist/RepeatModeTests.swift deleted file mode 100644 index 298cc51b..00000000 --- a/Tests/PlayerTests/Playlist/RepeatModeTests.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class RepeatModeTests: TestCase { - func testRepeatOne() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(items: [item1, item2]) - player.repeatMode = .one - player.play() - expect(player.currentItem).toAlways(equal(item1), until: .seconds(2)) - player.repeatMode = .off - expect(player.currentItem).toEventually(equal(item2)) - } - - func testRepeatAll() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(items: [item1, item2]) - player.repeatMode = .all - player.play() - expect(player.currentItem).toEventually(equal(item1)) - expect(player.currentItem).toEventually(equal(item2)) - expect(player.currentItem).toEventually(equal(item1)) - player.repeatMode = .off - expect(player.currentItem).toEventually(equal(item2)) - expect(player.currentItem).toEventually(beNil()) - } - - func testRepeatModeUpdateDoesNotRestartPlayback() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - player.play() - expect(player.streamType).toEventually(equal(.onDemand)) - player.repeatMode = .one - expect(player.streamType).toNever(equal(.unknown), until: .milliseconds(100)) - } - - func testRepeatModeUpdateDoesNotReplay() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - player.play() - expect(player.currentItem).toEventually(beNil()) - player.repeatMode = .one - expect(player.currentItem).toAlways(beNil(), until: .milliseconds(100)) - } -} diff --git a/Tests/PlayerTests/ProgressTracker/ProgressTrackerPlaybackStateTests.swift b/Tests/PlayerTests/ProgressTracker/ProgressTrackerPlaybackStateTests.swift deleted file mode 100644 index b8ee8729..00000000 --- a/Tests/PlayerTests/ProgressTracker/ProgressTrackerPlaybackStateTests.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble -import PillarboxStreams - -final class ProgressTrackerPlaybackStateTests: TestCase { - func testInteractionPausesPlayback() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - progressTracker.player = player - player.play() - expect(player.playbackState).toEventually(equal(.playing)) - - progressTracker.isInteracting = true - expect(player.playbackState).toEventually(equal(.paused)) - - progressTracker.isInteracting = false - expect(player.playbackState).toEventually(equal(.playing)) - } - - func testInteractionDoesUpdateAlreadyPausedPlayback() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - progressTracker.player = player - expect(player.playbackState).toEventually(equal(.paused)) - - progressTracker.isInteracting = true - expect(player.playbackState).toAlways(equal(.paused), until: .seconds(1)) - - progressTracker.isInteracting = false - expect(player.playbackState).toAlways(equal(.paused), until: .seconds(1)) - } - - func testTransferInteractionBetweenPlayers() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - - let item1 = PlayerItem.simple(url: Stream.onDemand.url) - let player1 = Player(item: item1) - progressTracker.player = player1 - player1.play() - expect(player1.playbackState).toEventually(equal(.playing)) - - progressTracker.isInteracting = true - expect(player1.playbackState).toEventually(equal(.paused)) - - let item2 = PlayerItem.simple(url: Stream.onDemand.url) - let player2 = Player(item: item2) - progressTracker.player = player2 - player2.play() - expect(player2.playbackState).toEventually(equal(.playing)) - - progressTracker.player = player2 - expect(player1.playbackState).toEventually(equal(.playing)) - expect(player2.playbackState).toEventually(equal(.paused)) - } -} diff --git a/Tests/PlayerTests/ProgressTracker/ProgressTrackerProgressAvailabilityTests.swift b/Tests/PlayerTests/ProgressTracker/ProgressTrackerProgressAvailabilityTests.swift deleted file mode 100644 index 5dc223fe..00000000 --- a/Tests/PlayerTests/ProgressTracker/ProgressTrackerProgressAvailabilityTests.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Combine -import CoreMedia -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class ProgressTrackerProgressAvailabilityTests: TestCase { - func testUnbound() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - expectAtLeastEqualPublished( - values: [false], - from: progressTracker.changePublisher(at: \.isProgressAvailable) - .removeDuplicates() - ) - } - - func testEmptyPlayer() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - expectAtLeastEqualPublished( - values: [false], - from: progressTracker.changePublisher(at: \.isProgressAvailable) - .removeDuplicates() - ) { - progressTracker.player = Player() - } - } - - func testPausedPlayer() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expectAtLeastEqualPublished( - values: [false, true], - from: progressTracker.changePublisher(at: \.isProgressAvailable) - .removeDuplicates() - ) { - progressTracker.player = player - } - } - - func testEntirePlayback() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(item: item) - expectAtLeastEqualPublished( - values: [false, true, false], - from: progressTracker.changePublisher(at: \.isProgressAvailable) - .removeDuplicates() - ) { - progressTracker.player = player - player.play() - } - } - - func testPausedDvrStream() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(item: item) - expectAtLeastEqualPublished( - values: [false, true], - from: progressTracker.changePublisher(at: \.isProgressAvailable) - .removeDuplicates() - ) { - progressTracker.player = player - } - } - - func testPlayerChange() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - progressTracker.player = player - player.play() - expect(progressTracker.isProgressAvailable).toEventually(beTrue()) - - expectAtLeastEqualPublished( - values: [true, false], - from: progressTracker.changePublisher(at: \.isProgressAvailable) - .removeDuplicates() - ) { - progressTracker.player = Player() - } - } - - func testPlayerSetToNil() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - progressTracker.player = player - player.play() - expect(progressTracker.isProgressAvailable).toEventually(beTrue()) - - expectAtLeastEqualPublished( - values: [true, false], - from: progressTracker.changePublisher(at: \.isProgressAvailable) - .removeDuplicates() - ) { - progressTracker.player = nil - } - } - - func testBoundToPlayerAtSomeTime() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - - expect(player.seekableTimeRange).toEventuallyNot(equal(.invalid)) - let time = CMTime(value: 20, timescale: 1) - - waitUntil { done in - player.seek(at(time)) { _ in - done() - } - } - - expectAtLeastEqualPublished( - values: [false, true], - from: progressTracker.changePublisher(at: \.isProgressAvailable) - .removeDuplicates() - ) { - progressTracker.player = player - } - } -} diff --git a/Tests/PlayerTests/ProgressTracker/ProgressTrackerProgressTests.swift b/Tests/PlayerTests/ProgressTracker/ProgressTrackerProgressTests.swift deleted file mode 100644 index 559fb6f6..00000000 --- a/Tests/PlayerTests/ProgressTracker/ProgressTrackerProgressTests.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Combine -import CoreMedia -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class ProgressTrackerProgressTests: TestCase { - func testUnbound() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - expectAtLeastEqualPublished( - values: [0], - from: progressTracker.changePublisher(at: \.progress) - .removeDuplicates() - ) - } - - func testEmptyPlayer() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - expectAtLeastEqualPublished( - values: [0], - from: progressTracker.changePublisher(at: \.progress) - .removeDuplicates() - ) { - progressTracker.player = Player() - } - } - - func testPausedPlayer() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expectAtLeastEqualPublished( - values: [0], - from: progressTracker.changePublisher(at: \.progress) - .removeDuplicates() - ) { - progressTracker.player = player - } - } - - func testEntirePlayback() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(item: item) - expectPublished( - values: [0, 0.25, 0.5, 0.75, 1, 0], - from: progressTracker.changePublisher(at: \.progress) - .removeDuplicates(), - to: beClose(within: 0.1), - during: .seconds(2) - ) { - progressTracker.player = player - player.play() - } - } - - func testPausedDvrStream() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(item: item) - expectAtLeastPublished( - values: [0, 1, 0.95, 0.9, 0.85, 0.8], - from: progressTracker.changePublisher(at: \.progress) - .removeDuplicates(), - to: beClose(within: 0.1) - ) { - progressTracker.player = player - } - } - - func testPlayerChange() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - progressTracker.player = player - player.play() - expect(progressTracker.progress).toEventuallyNot(equal(0)) - - let progress = progressTracker.progress - expectAtLeastEqualPublished( - values: [progress, 0], - from: progressTracker.changePublisher(at: \.progress) - .removeDuplicates() - ) { - progressTracker.player = Player() - } - } - - func testPlayerSetToNil() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - progressTracker.player = player - player.play() - expect(progressTracker.progress).toEventuallyNot(equal(0)) - - let progress = progressTracker.progress - expectAtLeastEqualPublished( - values: [progress, 0], - from: progressTracker.changePublisher(at: \.progress) - .removeDuplicates() - ) { - progressTracker.player = nil - } - } - - func testBoundToPlayerAtSomeTime() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - - expect(player.seekableTimeRange).toEventuallyNot(equal(.invalid)) - let time = CMTime(value: 20, timescale: 1) - - waitUntil { done in - player.seek(at(time)) { _ in - done() - } - } - - let progress = Float(20.0 / Stream.onDemand.duration.seconds) - expectAtLeastPublished( - values: [0, progress], - from: progressTracker.changePublisher(at: \.progress) - .removeDuplicates(), - to: beClose(within: 0.1) - ) { - progressTracker.player = player - } - } - - func testProgressForTimeInTimeRange() { - let timeRange = CMTimeRange(start: .zero, end: .init(value: 10, timescale: 1)) - expect(ProgressTracker.progress(for: .init(value: 5, timescale: 1), in: timeRange)).to(equal(0.5)) - expect(ProgressTracker.progress(for: .init(value: 15, timescale: 1), in: timeRange)).to(equal(1.5)) - } - - func testValidProgressInRange() { - expect(ProgressTracker.validProgress(nil, in: 0...1)).to(equal(0)) - expect(ProgressTracker.validProgress(0.5, in: 0...1)).to(equal(0.5)) - expect(ProgressTracker.validProgress(1.5, in: 0...1)).to(equal(1)) - } - - func testTimeForProgressInTimeRange() { - let timeRange = CMTimeRange(start: .zero, end: .init(value: 10, timescale: 1)) - expect(ProgressTracker.time(forProgress: 0.5, in: timeRange)).to(equal(CMTime(value: 5, timescale: 1))) - expect(ProgressTracker.time(forProgress: 1.5, in: timeRange)).to(equal(CMTime(value: 15, timescale: 1))) - } -} diff --git a/Tests/PlayerTests/ProgressTracker/ProgressTrackerRangeTests.swift b/Tests/PlayerTests/ProgressTracker/ProgressTrackerRangeTests.swift deleted file mode 100644 index 1c80b1eb..00000000 --- a/Tests/PlayerTests/ProgressTracker/ProgressTrackerRangeTests.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Combine -import CoreMedia -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class ProgressTrackerRangeTests: TestCase { - func testUnbound() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - expectAtLeastEqualPublished( - values: [0...0], - from: progressTracker.changePublisher(at: \.range) - .removeDuplicates() - ) - } - - func testEmptyPlayer() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - expectAtLeastEqualPublished( - values: [0...0], - from: progressTracker.changePublisher(at: \.range) - .removeDuplicates() - ) { - progressTracker.player = Player() - } - } - - func testPausedPlayer() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expectAtLeastEqualPublished( - values: [0...0, 0...1], - from: progressTracker.changePublisher(at: \.range) - .removeDuplicates() - ) { - progressTracker.player = player - } - } - - func testEntirePlayback() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(item: item) - expectAtLeastEqualPublished( - values: [0...0, 0...1, 0...0], - from: progressTracker.changePublisher(at: \.range) - .removeDuplicates() - ) { - progressTracker.player = player - player.play() - } - } - - func testPausedDvrStream() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(item: item) - expectAtLeastEqualPublished( - values: [0...0, 0...1], - from: progressTracker.changePublisher(at: \.range) - .removeDuplicates() - ) { - progressTracker.player = player - } - } - - func testPlayerChange() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - progressTracker.player = player - player.play() - expect(progressTracker.range).toEventuallyNot(equal(0...0)) - - expectAtLeastEqualPublished( - values: [0...1, 0...0], - from: progressTracker.changePublisher(at: \.range) - .removeDuplicates() - ) { - progressTracker.player = Player() - } - } - - func testPlayerSetToNil() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - progressTracker.player = player - player.play() - expect(progressTracker.range).toEventuallyNot(equal(0...0)) - - expectAtLeastEqualPublished( - values: [0...1, 0...0], - from: progressTracker.changePublisher(at: \.range) - .removeDuplicates() - ) { - progressTracker.player = nil - } - } - - func testBoundToPlayerAtSomeTime() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - - expect(player.seekableTimeRange).toEventuallyNot(equal(.invalid)) - let time = CMTime(value: 20, timescale: 1) - - waitUntil { done in - player.seek(at(time)) { _ in - done() - } - } - - expectAtLeastEqualPublished( - values: [0...0, 0...1], - from: progressTracker.changePublisher(at: \.range) - .removeDuplicates() - ) { - progressTracker.player = player - } - } -} diff --git a/Tests/PlayerTests/ProgressTracker/ProgressTrackerSeekBehaviorTests.swift b/Tests/PlayerTests/ProgressTracker/ProgressTrackerSeekBehaviorTests.swift deleted file mode 100644 index f7088c0d..00000000 --- a/Tests/PlayerTests/ProgressTracker/ProgressTrackerSeekBehaviorTests.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Combine -import CoreMedia -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class ProgressTrackerSeekBehaviorTests: TestCase { - private func isSeekingPublisher(for player: Player) -> AnyPublisher { - player.propertiesPublisher - .slice(at: \.isSeeking) - .eraseToAnyPublisher() - } - - func testImmediateSeek() { - let progressTracker = ProgressTracker( - interval: CMTime(value: 1, timescale: 4), - seekBehavior: .immediate - ) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - progressTracker.player = player - expect(progressTracker.range).toEventually(equal(0...1)) - - expectAtLeastEqualPublished( - values: [false, true, false], - from: isSeekingPublisher(for: player) - ) { - progressTracker.isInteracting = true - progressTracker.progress = 0.5 - } - expect(progressTracker.progress).to(equal(0.5)) - } - - func testDeferredSeek() { - let progressTracker = ProgressTracker( - interval: CMTime(value: 1, timescale: 4), - seekBehavior: .deferred - ) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - progressTracker.player = player - expect(progressTracker.range).toEventually(equal(0...1)) - - expectAtLeastEqualPublished( - values: [false], - from: isSeekingPublisher(for: player) - ) { - progressTracker.isInteracting = true - progressTracker.progress = 0.5 - } - - expectAtLeastEqualPublishedNext( - values: [true, false], - from: isSeekingPublisher(for: player) - ) { - progressTracker.isInteracting = false - } - expect(progressTracker.progress).toEventually(equal(0.5)) - } -} diff --git a/Tests/PlayerTests/ProgressTracker/ProgressTrackerTimeTests.swift b/Tests/PlayerTests/ProgressTracker/ProgressTrackerTimeTests.swift deleted file mode 100644 index 0752bafe..00000000 --- a/Tests/PlayerTests/ProgressTracker/ProgressTrackerTimeTests.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Combine -import CoreMedia -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class ProgressTrackerTimeTests: TestCase { - func testUnbound() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - expectAtLeastEqualPublished( - values: [.invalid], - from: progressTracker.changePublisher(at: \.time) - .removeDuplicates() - ) - } - - func testEmptyPlayer() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - expectAtLeastEqualPublished( - values: [.invalid], - from: progressTracker.changePublisher(at: \.time) - .removeDuplicates() - ) { - progressTracker.player = Player() - } - } - - func testPausedPlayer() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expectAtLeastEqualPublished( - values: [.invalid, .zero], - from: progressTracker.changePublisher(at: \.time) - .removeDuplicates() - ) { - progressTracker.player = player - } - } - - func testEntirePlayback() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.shortOnDemand.url) - let player = Player(item: item) - expectPublished( - values: [ - .invalid, - .zero, - CMTime(value: 1, timescale: 4), - CMTime(value: 1, timescale: 2), - CMTime(value: 3, timescale: 4), - CMTime(value: 1, timescale: 1), - .invalid - ], - from: progressTracker.changePublisher(at: \.time) - .removeDuplicates(), - to: beClose(within: 0.1), - during: .seconds(2) - ) { - progressTracker.player = player - player.play() - } - } - - func testPausedDvrStream() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(item: item) - expectAtLeastPublished( - values: [ - .invalid, - CMTime(value: 17, timescale: 1), - CMTime(value: 17, timescale: 1), - CMTime(value: 17, timescale: 1), - CMTime(value: 17, timescale: 1), - CMTime(value: 17, timescale: 1) - ], - from: progressTracker.changePublisher(at: \.time) - .removeDuplicates(), - to: beClose(within: 0.1) - ) { - progressTracker.player = player - } - } - - func testPlayerChange() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - progressTracker.player = player - player.play() - expect(progressTracker.time).toEventuallyNot(equal(.invalid)) - - let time = progressTracker.time - expectAtLeastEqualPublished( - values: [time, .invalid], - from: progressTracker.changePublisher(at: \.time) - .removeDuplicates() - ) { - progressTracker.player = Player() - } - } - - func testPlayerSetToNil() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - progressTracker.player = player - player.play() - expect(progressTracker.time).toEventuallyNot(equal(.invalid)) - - let time = progressTracker.time - expectAtLeastEqualPublished( - values: [time, .invalid], - from: progressTracker.changePublisher(at: \.time) - .removeDuplicates() - ) { - progressTracker.player = nil - } - } - - func testBoundToPlayerAtSomeTime() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - - expect(player.seekableTimeRange).toEventuallyNot(equal(.invalid)) - let time = CMTime(value: 20, timescale: 1) - - waitUntil { done in - player.seek(at(time)) { _ in - done() - } - } - - expectAtLeastPublished( - values: [.invalid, time], - from: progressTracker.changePublisher(at: \.time) - .removeDuplicates(), - to: beClose(within: 0.1) - ) { - progressTracker.player = player - } - } -} diff --git a/Tests/PlayerTests/ProgressTracker/ProgressTrackerValueTests.swift b/Tests/PlayerTests/ProgressTracker/ProgressTrackerValueTests.swift deleted file mode 100644 index 830d10a8..00000000 --- a/Tests/PlayerTests/ProgressTracker/ProgressTrackerValueTests.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble -import PillarboxStreams - -final class ProgressTrackerValueTests: TestCase { - func testProgressValueInRange() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - progressTracker.player = player - player.play() - expect(progressTracker.range).toEventuallyNot(equal(0...0)) - progressTracker.progress = 0.5 - expect(progressTracker.progress).to(beCloseTo(0.5, within: 0.1)) - } - - func testProgressValueBelowZero() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - progressTracker.player = player - player.play() - expect(progressTracker.range).toEventuallyNot(equal(0...0)) - progressTracker.progress = -10 - expect(progressTracker.progress).to(beCloseTo(0, within: 0.1)) - } - - func testProgressValueAboveOne() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - progressTracker.player = player - player.play() - expect(progressTracker.range).toEventuallyNot(equal(0...0)) - progressTracker.progress = 10 - expect(progressTracker.progress).to(beCloseTo(1, within: 0.1)) - } - - func testCannotChangeProgressWhenUnavailable() { - let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) - let player = Player() - progressTracker.player = player - expect(progressTracker.isProgressAvailable).to(equal(false)) - expect(progressTracker.progress).to(beCloseTo(0, within: 0.1)) - progressTracker.progress = 0.5 - expect(progressTracker.progress).to(beCloseTo(0, within: 0.1)) - } -} diff --git a/Tests/PlayerTests/Publishers/AVAssetMediaSelectionGroupsPublisherTests.swift b/Tests/PlayerTests/Publishers/AVAssetMediaSelectionGroupsPublisherTests.swift deleted file mode 100644 index 9dd24d60..00000000 --- a/Tests/PlayerTests/Publishers/AVAssetMediaSelectionGroupsPublisherTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble -import PillarboxStreams - -// swiftlint:disable:next type_name -final class AVAssetMediaSelectionGroupsPublisherTests: TestCase { - func testFetch() throws { - let asset = AVURLAsset(url: Stream.onDemandWithOptions.url) - let groups = try waitForSingleOutput(from: asset.mediaSelectionGroupsPublisher()) - expect(groups[.audible]).notTo(beNil()) - expect(groups[.legible]).notTo(beNil()) - } - - func testFetchWithoutSelectionAvailable() throws { - let asset = AVURLAsset(url: Stream.onDemandWithoutOptions.url) - let groups = try waitForSingleOutput(from: asset.mediaSelectionGroupsPublisher()) - expect(groups).to(beEmpty()) - } - - func testRepeatedFetch() throws { - let asset = AVURLAsset(url: Stream.onDemandWithOptions.url) - - let groups1 = try waitForSingleOutput(from: asset.mediaSelectionGroupsPublisher()) - expect(groups1).notTo(beEmpty()) - - let groups2 = try waitForSingleOutput(from: asset.mediaSelectionGroupsPublisher()) - expect(groups2).to(equal(groups1)) - } -} diff --git a/Tests/PlayerTests/Publishers/AVAsynchronousKeyValueLoadingPublisherTests.swift b/Tests/PlayerTests/Publishers/AVAsynchronousKeyValueLoadingPublisherTests.swift deleted file mode 100644 index b2ad7d09..00000000 --- a/Tests/PlayerTests/Publishers/AVAsynchronousKeyValueLoadingPublisherTests.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -// swiftlint:disable:next type_name -final class AVAsynchronousKeyValueLoadingPublisherTests: TestCase { - func testAssetFetch() throws { - let asset = AVURLAsset(url: Stream.onDemand.url) - let duration = try waitForSingleOutput(from: asset.propertyPublisher(.duration)) - expect(duration).to(equal(Stream.onDemand.duration, by: beClose(within: 1))) - } - - func testAssetRepeatedFetch() throws { - let asset = AVURLAsset(url: Stream.onDemand.url) - - let duration1 = try waitForSingleOutput(from: asset.propertyPublisher(.duration)) - expect(duration1).to(equal(Stream.onDemand.duration, by: beClose(within: 1))) - - let duration2 = try waitForSingleOutput(from: asset.propertyPublisher(.duration)) - expect(duration2).to(equal(Stream.onDemand.duration, by: beClose(within: 1))) - } - - func testAssetFailedFetch() throws { - let asset = AVURLAsset(url: Stream.unavailable.url) - let error = try waitForFailure(from: asset.propertyPublisher(.duration)) - expect(error).notTo(beNil()) - } - - func testAssetMultipleFetch() throws { - let asset = AVURLAsset(url: Stream.onDemand.url) - let (duration, preferredRate) = try waitForSingleOutput(from: asset.propertyPublisher(.duration, .preferredRate)) - expect(duration).to(equal(Stream.onDemand.duration, by: beClose(within: 1))) - expect(preferredRate).to(equal(1)) - } - - func testAssetFailedMultipleFetch() throws { - let asset = AVURLAsset(url: Stream.unavailable.url) - let error = try waitForFailure(from: asset.propertyPublisher(.duration, .preferredRate)) - expect(error).notTo(beNil()) - } - - func testMetadataItemFetch() throws { - let item = AVMetadataItem(identifier: .commonIdentifierTitle, value: "Title")! - let title = try waitForSingleOutput(from: item.propertyPublisher(.stringValue)) - expect(title).to(equal("Title")) - } - - func testMetadataItemFetchWithTypeMismatch() throws { - let item = AVMetadataItem(identifier: .commonIdentifierTitle, value: "Title")! - let title = try waitForSingleOutput(from: item.propertyPublisher(.dateValue)) - expect(title).to(beNil()) - } -} diff --git a/Tests/PlayerTests/Publishers/AVPlayerBoundaryTimePublisherTests.swift b/Tests/PlayerTests/Publishers/AVPlayerBoundaryTimePublisherTests.swift deleted file mode 100644 index 3a4c2e7f..00000000 --- a/Tests/PlayerTests/Publishers/AVPlayerBoundaryTimePublisherTests.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Combine -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class AVPlayerBoundaryTimePublisherTests: TestCase { - func testEmpty() { - let player = AVPlayer() - expectNothingPublished( - from: Publishers.BoundaryTimePublisher( - for: player, - times: [CMTimeMake(value: 1, timescale: 2)] - ), - during: .seconds(2) - ) - } - - func testNoPlayback() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = AVPlayer(playerItem: item) - expectNothingPublished( - from: Publishers.BoundaryTimePublisher( - for: player, - times: [CMTimeMake(value: 1, timescale: 2)] - ), - during: .seconds(2) - ) - } - - func testPlayback() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = AVPlayer(playerItem: item) - expectAtLeastEqualPublished( - values: [ - "tick", "tick" - ], - from: Publishers.BoundaryTimePublisher( - for: player, - times: [ - CMTimeMake(value: 1, timescale: 2), - CMTimeMake(value: 2, timescale: 2) - ] - ) - .map { "tick" } - ) { - player.play() - } - } - - func testDeallocation() { - var player: AVPlayer? = AVPlayer() - _ = Publishers.BoundaryTimePublisher( - for: player!, - times: [ - CMTimeMake(value: 1, timescale: 2) - ] - ) - - weak var weakPlayer = player - autoreleasepool { - player = nil - } - expect(weakPlayer).to(beNil()) - } -} diff --git a/Tests/PlayerTests/Publishers/AVPlayerItemErrorPublisherTests.swift b/Tests/PlayerTests/Publishers/AVPlayerItemErrorPublisherTests.swift deleted file mode 100644 index 0240d286..00000000 --- a/Tests/PlayerTests/Publishers/AVPlayerItemErrorPublisherTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Combine -import PillarboxStreams - -final class AVPlayerItemErrorPublisherTests: TestCase { - private static func errorCodePublisher(for item: AVPlayerItem) -> AnyPublisher { - item.errorPublisher() - .map { .init(rawValue: ($0 as NSError).code) } - .eraseToAnyPublisher() - } - - func testNoError() { - let item = AVPlayerItem(url: Stream.onDemand.url) - _ = AVPlayer(playerItem: item) - expectNothingPublished(from: item.errorPublisher(), during: .milliseconds(500)) - } - - func testM3u8Error() { - let item = AVPlayerItem(url: Stream.unavailable.url) - _ = AVPlayer(playerItem: item) - expectAtLeastEqualPublished(values: [ - URLError.fileDoesNotExist - ], from: Self.errorCodePublisher(for: item)) - } - - func testMp3Error() { - let item = AVPlayerItem(url: Stream.unavailableMp3.url) - _ = AVPlayer(playerItem: item) - expectAtLeastEqualPublished(values: [ - URLError.fileDoesNotExist - ], from: Self.errorCodePublisher(for: item)) - } -} diff --git a/Tests/PlayerTests/Publishers/AVPlayerItemMetricEventPublisherTests.swift b/Tests/PlayerTests/Publishers/AVPlayerItemMetricEventPublisherTests.swift deleted file mode 100644 index f4811e4f..00000000 --- a/Tests/PlayerTests/Publishers/AVPlayerItemMetricEventPublisherTests.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import PillarboxStreams - -final class AVPlayerItemMetricEventPublisherTests: TestCase { - func testPlayableItemAssetMetricEvent() { - let item = AVPlayerItem(url: Stream.onDemand.url) - _ = AVPlayer(playerItem: item) - expectOnlySimilarPublished( - values: [.anyAsset], - from: item.assetMetricEventPublisher() - ) - } - - func testFailingItemAssetMetricEvent() { - let item = AVPlayerItem(url: Stream.unavailable.url) - _ = AVPlayer(playerItem: item) - expectNothingPublished(from: item.assetMetricEventPublisher(), during: .milliseconds(500)) - } - - func testPlayableItemFailureMetricEvent() { - let item = AVPlayerItem(url: Stream.onDemand.url) - _ = AVPlayer(playerItem: item) - expectNothingPublished(from: item.failureMetricEventPublisher(), during: .milliseconds(500)) - } - - func testFailingItemFailureMetricEvent() { - let item = AVPlayerItem(url: Stream.unavailable.url) - _ = AVPlayer(playerItem: item) - expectOnlySimilarPublished( - values: [.anyFailure], - from: item.failureMetricEventPublisher() - ) - } - - func testPlayableItemWarningMetricEvent() { - let item = AVPlayerItem(url: Stream.onDemand.url) - _ = AVPlayer(playerItem: item) - expectNothingPublished(from: item.warningMetricEventPublisher(), during: .milliseconds(500)) - } - - func testPlayableItemStallMetricEvent() { - let item = AVPlayerItem(url: Stream.onDemand.url) - _ = AVPlayer(playerItem: item) - expectNothingPublished(from: item.stallEventPublisher(), during: .milliseconds(500)) - } -} diff --git a/Tests/PlayerTests/Publishers/AVPlayerPeriodicTimePublisherTests.swift b/Tests/PlayerTests/Publishers/AVPlayerPeriodicTimePublisherTests.swift deleted file mode 100644 index 17f281de..00000000 --- a/Tests/PlayerTests/Publishers/AVPlayerPeriodicTimePublisherTests.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Combine -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class AVPlayerPeriodicTimePublisherTests: TestCase { - func testEmpty() { - let player = AVPlayer() - expectNothingPublished( - from: Publishers.PeriodicTimePublisher( - for: player, - interval: CMTimeMake(value: 1, timescale: 10) - ), - during: .milliseconds(500) - ) - } - - func testPlayback() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = AVPlayer(playerItem: item) - expectAtLeastPublished( - values: [ - .zero, - CMTimeMake(value: 1, timescale: 10), - CMTimeMake(value: 2, timescale: 10), - CMTimeMake(value: 3, timescale: 10) - ], - from: Publishers.PeriodicTimePublisher( - for: player, - interval: CMTimeMake(value: 1, timescale: 10) - ), - to: beClose(within: 0.1) - ) { - player.play() - } - } - - func testDeallocation() { - var player: AVPlayer? = AVPlayer() - _ = Publishers.PeriodicTimePublisher( - for: player!, - interval: CMTime(value: 1, timescale: 1) - ) - - weak var weakPlayer = player - autoreleasepool { - player = nil - } - expect(weakPlayer).to(beNil()) - } -} diff --git a/Tests/PlayerTests/Publishers/MetadataPublisherTests.swift b/Tests/PlayerTests/Publishers/MetadataPublisherTests.swift deleted file mode 100644 index ceee5204..00000000 --- a/Tests/PlayerTests/Publishers/MetadataPublisherTests.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Combine -import PillarboxCircumspect -import PillarboxStreams - -final class MetadataPublisherTests: TestCase { - private static func titlePublisherTest(for player: Player) -> AnyPublisher { - player.metadataPublisher.map(\.title).eraseToAnyPublisher() - } - - func testEmpty() { - let player = Player() - expectEqualPublished( - values: [nil], - from: Self.titlePublisherTest(for: player), - during: .milliseconds(100) - ) - } - - func testImmediatelyAvailableWithoutMetadata() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expectEqualPublished( - values: [nil], - from: Self.titlePublisherTest(for: player), - during: .milliseconds(100) - ) - } - - func testAvailableAfterDelay() { - let player = Player( - item: .mock(url: Stream.onDemand.url, loadedAfter: 0.1, withMetadata: AssetMetadataMock(title: "title")) - ) - expectEqualPublished( - values: [nil, "title"], - from: Self.titlePublisherTest(for: player), - during: .milliseconds(200) - ) - } - - func testImmediatelyAvailableWithMetadata() { - let player = Player(item: .mock( - url: Stream.onDemand.url, - loadedAfter: 0, - withMetadata: AssetMetadataMock(title: "title") - )) - expectEqualPublished( - values: [nil, "title"], - from: Self.titlePublisherTest(for: player), - during: .milliseconds(200) - ) - } - - func testUpdate() { - let player = Player(item: .mock(url: Stream.onDemand.url, withMetadataUpdateAfter: 0.1)) - expectEqualPublished( - values: [nil, "title0", "title1"], - from: Self.titlePublisherTest(for: player), - during: .milliseconds(500) - ) - } - - func testNetworkItemReloading() { - let player = Player(item: .webServiceMock(media: .media1)) - expectAtLeastEqualPublished( - values: [nil, "Title 1"], - from: Self.titlePublisherTest(for: player) - ) - expectEqualPublishedNext( - values: [nil, "Title 2"], - from: Self.titlePublisherTest(for: player), - during: .milliseconds(500) - ) { - player.items = [.webServiceMock(media: .media2)] - } - } - - func testEntirePlayback() { - let player = Player(item: .mock(url: Stream.shortOnDemand.url, loadedAfter: 0, withMetadata: AssetMetadataMock(title: "title"))) - expectEqualPublished( - values: [nil, "title", nil], - from: Self.titlePublisherTest(for: player), - during: .seconds(2) - ) { - player.play() - } - } -} diff --git a/Tests/PlayerTests/Publishers/NowPlayingInfoPublisherTests.swift b/Tests/PlayerTests/Publishers/NowPlayingInfoPublisherTests.swift deleted file mode 100644 index 0b179ae5..00000000 --- a/Tests/PlayerTests/Publishers/NowPlayingInfoPublisherTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Combine -import MediaPlayer -import PillarboxCircumspect -import PillarboxStreams - -final class NowPlayingInfoPublisherTests: TestCase { - private static func nowPlayingInfoPublisher(for player: Player) -> AnyPublisher { - player.nowPlayingPublisher() - .map(\.info) - .eraseToAnyPublisher() - } - - func testInactive() { - let player = Player(item: .mock(url: Stream.onDemand.url, loadedAfter: 0, withMetadata: AssetMetadataMock(title: "title"))) - expectSimilarPublished( - values: [[:]], - from: Self.nowPlayingInfoPublisher(for: player), - during: .milliseconds(100) - ) - } - - func testToggleActive() { - let player = Player(item: .mock(url: Stream.onDemand.url, loadedAfter: 0, withMetadata: AssetMetadataMock(title: "title"))) - expectAtLeastSimilarPublished( - values: [[:], [MPNowPlayingInfoPropertyIsLiveStream: false]], - from: Self.nowPlayingInfoPublisher(for: player) - ) { - player.isActive = true - } - - expectSimilarPublishedNext( - values: [[:]], - from: Self.nowPlayingInfoPublisher(for: player), - during: .milliseconds(100) - ) { - player.isActive = false - } - } -} diff --git a/Tests/PlayerTests/Publishers/PeriodicMetricsPublisherTests.swift b/Tests/PlayerTests/Publishers/PeriodicMetricsPublisherTests.swift deleted file mode 100644 index 9fe33272..00000000 --- a/Tests/PlayerTests/Publishers/PeriodicMetricsPublisherTests.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Combine -import PillarboxCircumspect -import PillarboxStreams - -final class PeriodicMetricsPublisherTests: TestCase { - func testEmpty() { - let player = Player() - expectEqualPublished( - values: [0], - from: player.periodicMetricsPublisher(forInterval: .init(value: 1, timescale: 4)).map(\.count), - during: .seconds(1) - ) - } - - func testPlayback() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expectAtLeastEqualPublished( - values: [0, 1, 2], - from: player.periodicMetricsPublisher(forInterval: .init(value: 1, timescale: 4)).map(\.count) - ) { - player.play() - } - } - - func testPlaylist() { - let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) - let item2 = PlayerItem.simple(url: Stream.mediumOnDemand.url) - let player = Player(items: [item1, item2]) - let publisher = player.periodicMetricsPublisher(forInterval: .init(value: 1, timescale: 1)).map(\.count) - expectAtLeastEqualPublished(values: [0, 1, 0, 1], from: publisher) { - player.play() - } - } - - func testNoMetricsForLiveMp3() { - let player = Player(item: .simple(url: Stream.liveMp3.url)) - let publisher = player.periodicMetricsPublisher(forInterval: .init(value: 1, timescale: 4)).map(\.count) - expectEqualPublished(values: [0], from: publisher, during: .milliseconds(500)) { - player.play() - } - } - - func testLimit() { - let item = PlayerItem.simple(url: Stream.onDemand.url) - let player = Player(item: item) - expectAtLeastEqualPublished( - values: [0, 1, 2, 2, 2, 2], - from: player.periodicMetricsPublisher(forInterval: .init(value: 1, timescale: 4), limit: 2).map(\.count) - ) { - player.play() - } - } - - func testFailure() { - let item = PlayerItem.failing(loadedAfter: 0.1) - let player = Player(item: item) - expectEqualPublished( - values: [0], - from: player.periodicMetricsPublisher(forInterval: .init(value: 1, timescale: 4)).map(\.count), - during: .seconds(1) - ) - } -} diff --git a/Tests/PlayerTests/Publishers/PlayerItemMetricEventPublisherTests.swift b/Tests/PlayerTests/Publishers/PlayerItemMetricEventPublisherTests.swift deleted file mode 100644 index 108c5624..00000000 --- a/Tests/PlayerTests/Publishers/PlayerItemMetricEventPublisherTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import PillarboxStreams - -final class PlayerItemMetricEventPublisherTests: TestCase { - func testPlayableItemMetricEvent() { - let item = PlayerItem.mock(url: Stream.onDemand.url, loadedAfter: 0.1) - expectAtLeastSimilarPublished( - values: [.anyMetadata], - from: item.metricEventPublisher() - ) { - PlayerItem.load(for: item.id) - } - } - - func testFailingItemMetricEvent() { - let item = PlayerItem.failing(loadedAfter: 0.1) - expectNothingPublished(from: item.metricEventPublisher(), during: .milliseconds(500)) { - PlayerItem.load(for: item.id) - } - } -} diff --git a/Tests/PlayerTests/Publishers/PlayerPublisherTests.swift b/Tests/PlayerTests/Publishers/PlayerPublisherTests.swift deleted file mode 100644 index 814e75c3..00000000 --- a/Tests/PlayerTests/Publishers/PlayerPublisherTests.swift +++ /dev/null @@ -1,159 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Combine -import PillarboxCircumspect -import PillarboxCore -import PillarboxStreams - -final class PlayerPublisherTests: TestCase { - private static func bufferingPublisher(for player: Player) -> AnyPublisher { - player.propertiesPublisher - .slice(at: \.isBuffering) - .eraseToAnyPublisher() - } - - private static func presentationSizePublisher(for player: Player) -> AnyPublisher { - player.propertiesPublisher - .slice(at: \.presentationSize) - .eraseToAnyPublisher() - } - - private static func itemStatusPublisher(for player: Player) -> AnyPublisher { - player.propertiesPublisher - .slice(at: \.itemStatus) - .eraseToAnyPublisher() - } - - private static func durationPublisher(for player: Player) -> AnyPublisher { - player.propertiesPublisher - .slice(at: \.duration) - .eraseToAnyPublisher() - } - - private static func seekableTimeRangePublisher(for player: Player) -> AnyPublisher { - player.propertiesPublisher - .slice(at: \.seekableTimeRange) - .eraseToAnyPublisher() - } - - func testBufferingEmpty() { - let player = Player() - expectEqualPublished( - values: [false], - from: Self.bufferingPublisher(for: player), - during: .milliseconds(500) - ) - } - - func testBuffering() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expectEqualPublished( - values: [true, false], - from: Self.bufferingPublisher(for: player), - during: .seconds(1) - ) - } - - func testPresentationSizeEmpty() { - let player = Player() - expectAtLeastEqualPublished( - values: [nil], - from: Self.presentationSizePublisher(for: player) - ) - } - - func testPresentationSize() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - expectAtLeastEqualPublished( - values: [nil, CGSize(width: 640, height: 360), nil], - from: Self.presentationSizePublisher(for: player) - ) { - player.play() - } - } - - func testItemStatusEmpty() { - let player = Player() - expectAtLeastEqualPublished( - values: [.unknown], - from: Self.itemStatusPublisher(for: player) - ) - } - - func testConsumedItemStatusLifeCycle() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - expectAtLeastEqualPublished( - values: [.unknown, .readyToPlay, .ended, .unknown], - from: Self.itemStatusPublisher(for: player) - ) { - player.play() - } - } - - func testPausedItemStatusLifeCycle() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - expectAtLeastEqualPublished( - values: [.unknown, .readyToPlay, .ended], - from: Self.itemStatusPublisher(for: player) - ) { - player.actionAtItemEnd = .pause - player.play() - } - expectAtLeastEqualPublishedNext( - values: [.readyToPlay], - from: Self.itemStatusPublisher(for: player) - ) { - player.seek(to: .zero) - } - } - - func testDurationEmpty() { - let player = Player() - expectAtLeastPublished( - values: [.invalid], - from: Self.durationPublisher(for: player), - to: beClose(within: 1) - ) - } - - func testDuration() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - expectAtLeastPublished( - values: [.invalid, Stream.shortOnDemand.duration, .invalid], - from: Self.durationPublisher(for: player), - to: beClose(within: 1) - ) { - player.play() - } - } - - func testSeekableTimeRangeEmpty() { - let player = Player() - expectAtLeastEqualPublished( - values: [.invalid], - from: Self.seekableTimeRangePublisher(for: player) - ) - } - - func testSeekableTimeRangeLifeCycle() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - expectAtLeastPublished( - values: [ - .invalid, - CMTimeRange(start: .zero, duration: Stream.shortOnDemand.duration), - .invalid - ], - from: Self.seekableTimeRangePublisher(for: player), - to: beClose(within: 1) - ) { - player.play() - } - } -} diff --git a/Tests/PlayerTests/QueuePlayer/QueuePlayerItemsTests.swift b/Tests/PlayerTests/QueuePlayer/QueuePlayerItemsTests.swift deleted file mode 100644 index cc078414..00000000 --- a/Tests/PlayerTests/QueuePlayer/QueuePlayerItemsTests.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class QueuePlayerItemsTests: TestCase { - func testReplaceItemsWithEmptyList() { - let item1 = AVPlayerItem(url: Stream.onDemand.url) - let item2 = AVPlayerItem(url: Stream.onDemand.url) - let item3 = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(items: [item1, item2, item3]) - player.replaceItems(with: []) - expect(player.items()).to(beEmpty()) - } - - func testReplaceItemsWhenEmpty() { - let player = QueuePlayer() - let item1 = AVPlayerItem(url: Stream.onDemand.url) - let item2 = AVPlayerItem(url: Stream.onDemand.url) - let item3 = AVPlayerItem(url: Stream.onDemand.url) - player.replaceItems(with: [item1, item2, item3]) - expect(player.items()).to(equalDiff([item1, item2, item3])) - } - - func testReplaceItemsWithOtherItems() { - let item1 = AVPlayerItem(url: Stream.onDemand.url) - let item2 = AVPlayerItem(url: Stream.onDemand.url) - let item3 = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(items: [item1, item2, item3]) - let item4 = AVPlayerItem(url: Stream.onDemand.url) - let item5 = AVPlayerItem(url: Stream.onDemand.url) - player.replaceItems(with: [item4, item5]) - expect(player.items()).to(equalDiff([item4, item5])) - } - - func testReplaceItemsWithPreservedCurrentItem() { - let item1 = AVPlayerItem(url: Stream.onDemand.url) - let item2 = AVPlayerItem(url: Stream.onDemand.url) - let item3 = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(items: [item1, item2, item3]) - let item4 = AVPlayerItem(url: Stream.onDemand.url) - player.replaceItems(with: [item1, item4]) - expect(player.items()).to(equalDiff([item1, item4])) - } - - func testReplaceItemsWithIdenticalItems() { - let item1 = AVPlayerItem(url: Stream.onDemand.url) - let item2 = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(items: [item1, item2]) - player.replaceItems(with: [item1, item2]) - expect(player.items()).to(equalDiff([item1, item2])) - } - - func testReplaceItemsWithNextItems() { - let item1 = AVPlayerItem(url: Stream.onDemand.url) - let item2 = AVPlayerItem(url: Stream.onDemand.url) - let item3 = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(items: [item1, item2, item3]) - player.replaceItems(with: [item2, item3]) - expect(player.items()).to(equalDiff([item2, item3])) - } - - func testReplaceItemsWithPreviousItems() { - let item2 = AVPlayerItem(url: Stream.onDemand.url) - let item3 = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(items: [item2, item3]) - let item1 = AVPlayerItem(url: Stream.onDemand.url) - player.replaceItems(with: [item1, item2, item3]) - expect(player.items()).to(equalDiff([item1, item2, item3])) - } - - func testReplaceItemsLastReplacementWins() { - let player = QueuePlayer() - let item1 = AVPlayerItem(url: Stream.onDemand.url) - let item2 = AVPlayerItem(url: Stream.onDemand.url) - player.replaceItems(with: [item1, item2]) - player.replaceItems(with: [item1]) - expect(player.items()).to(equalDiff([item1])) - } -} diff --git a/Tests/PlayerTests/QueuePlayer/QueuePlayerSeekTests.swift b/Tests/PlayerTests/QueuePlayer/QueuePlayerSeekTests.swift deleted file mode 100644 index 6e799634..00000000 --- a/Tests/PlayerTests/QueuePlayer/QueuePlayerSeekTests.swift +++ /dev/null @@ -1,286 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble -import OrderedCollections -import PillarboxCircumspect -import PillarboxStreams - -private class QueuePlayerMock: QueuePlayer { - var seeks: Int = 0 - - override func enqueue(seek: Seek, completion: @escaping () -> Void) { - self.seeks += 1 - super.enqueue(seek: seek, completion: completion) - } -} - -final class QueuePlayerSeekTests: TestCase { - func testNotificationsForSeekWithInvalidTime() { - guard nimbleThrowAssertionsAvailable() else { return } - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - expect { player.seek(to: .invalid) }.to(throwAssertion()) - } - - func testNotificationsForSeekWithEmptyPlayer() { - let player = QueuePlayer() - expect { - player.seek(to: CMTime(value: 1, timescale: 1)) { finished in - expect(finished).to(beTrue()) - } - }.to(postNotifications(equalDiff([]), from: QueuePlayer.notificationCenter)) - } - - func testNotificationsForSeek() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - let time = CMTime(value: 1, timescale: 1) - expect { - player.seek(to: time) { finished in - expect(finished).to(beTrue()) - } - }.to(postNotifications(equalDiff([ - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time]), - Notification(name: .didSeek, object: player) - ]), from: QueuePlayer.notificationCenter)) - } - - func testNotificationsForMultipleSeeks() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - let time1 = CMTime(value: 1, timescale: 1) - let time2 = CMTime(value: 2, timescale: 1) - expect { - player.seek(to: time1) { finished in - expect(finished).to(beTrue()) - } - player.seek(to: time2) { finished in - expect(finished).to(beTrue()) - } - }.to(postNotifications(equalDiff([ - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time1]), - Notification(name: .didSeek, object: player), - - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time2]), - Notification(name: .didSeek, object: player) - ]), from: QueuePlayer.notificationCenter)) - } - - func testNotificationsForMultipleSeeksWithinTimeRange() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - expect(item.timeRange).toEventuallyNot(equal(.invalid)) - - let time1 = CMTime(value: 1, timescale: 1) - let time2 = CMTime(value: 2, timescale: 1) - expect { - player.seek(to: time1) { finished in - expect(finished).to(beFalse()) - } - player.seek(to: time2) { finished in - expect(finished).to(beTrue()) - } - }.toEventually(postNotifications(equalDiff([ - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time1]), - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time2]), - Notification(name: .didSeek, object: player) - ]), from: QueuePlayer.notificationCenter)) - } - - func testNotificationsForSeekAfterSmoothSeekWithinTimeRange() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - expect(item.timeRange).toEventuallyNot(equal(.invalid)) - - let time1 = CMTime(value: 1, timescale: 1) - let time2 = CMTime(value: 2, timescale: 1) - expect { - player.seek(to: time1, smooth: true) { finished in - expect(finished).to(beFalse()) - } - player.seek(to: time2) { finished in - expect(finished).to(beTrue()) - } - }.toEventually(postNotifications(equalDiff([ - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time1]), - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time2]), - Notification(name: .didSeek, object: player) - ]), from: QueuePlayer.notificationCenter)) - } - - func testCompletionsForMultipleSeeks() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - expect(item.timeRange).toEventuallyNot(equal(.invalid)) - - let time1 = CMTime(value: 1, timescale: 1) - let time2 = CMTime(value: 2, timescale: 1) - let time3 = CMTime(value: 3, timescale: 1) - - var results = OrderedDictionary() - - func completion(index: Int) -> ((Bool) -> Void) { - { finished in - expect(results[index]).to(beNil()) - results[index] = finished - } - } - - player.seek(to: time1, completionHandler: completion(index: 1)) - player.seek(to: time2, completionHandler: completion(index: 2)) - player.seek(to: time3, completionHandler: completion(index: 3)) - - expect(results).toEventually(equalDiff([ - 1: false, - 2: false, - 3: true - ])) - } - - func testCompletionsForMultipleSmoothSeeksEndingWithSeek() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - expect(item.timeRange).toEventuallyNot(equal(.invalid)) - - let time1 = CMTime(value: 1, timescale: 1) - let time2 = CMTime(value: 2, timescale: 1) - let time3 = CMTime(value: 3, timescale: 1) - - var results = OrderedDictionary() - - func completion(index: Int) -> ((Bool) -> Void) { - { finished in - expect(results[index]).to(beNil()) - results[index] = finished - } - } - - player.seek(to: time1, smooth: true, completionHandler: completion(index: 1)) - player.seek(to: time2, smooth: true, completionHandler: completion(index: 2)) - player.seek(to: time3, smooth: false, completionHandler: completion(index: 3)) - - expect(results).toEventually(equalDiff([ - 1: false, - 2: false, - 3: true - ])) - } - - // Checks that time is not jumping back when seeking forward several times in a row (no tolerance before is allowed - // in this test as otherwise the player is allowed to pick a position before the desired position), - func testMultipleSeekMonotonicity() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - player.play() - expect(item.timeRange).toEventuallyNot(equal(.invalid)) - - let values = collectOutput(from: player.smoothCurrentTimePublisher(interval: CMTime(value: 1, timescale: 10), queue: .main), during: .seconds(3)) { - player.seek(to: CMTime(value: 8, timescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) { _ in - player.seek(to: CMTime(value: 10, timescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) { _ in - player.seek(to: CMTime(value: 12, timescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) { _ in - player.seek(to: CMTime(value: 100, timescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) { _ in - player.seek(to: CMTime(value: 100, timescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) - } - } - } - } - } - expect(values.sorted()).to(equal(values)) - } - - func testNotificationCompletionOrder() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - expect(item.timeRange).toEventuallyNot(equal(.invalid)) - - let time = CMTime(value: 1, timescale: 1) - let notificationName = Notification.Name("SeekCompleted") - expect { - player.seek(to: time) { _ in - QueuePlayer.notificationCenter.post(name: notificationName, object: self) - } - }.toEventually(postNotifications(equalDiff([ - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time]), - Notification(name: .didSeek, object: player), - Notification(name: notificationName, object: self) - ]), from: QueuePlayer.notificationCenter)) - } - - func testNotificationCompletionOrderWithMultipleSeeks() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - expect(item.timeRange).toEventuallyNot(equal(.invalid)) - - let time1 = CMTime(value: 1, timescale: 1) - let time2 = CMTime(value: 2, timescale: 1) - let notificationName1 = Notification.Name("SeekCompleted1") - let notificationName2 = Notification.Name("SeekCompleted2") - expect { - player.seek(to: time1) { _ in - QueuePlayer.notificationCenter.post(name: notificationName1, object: self) - } - player.seek(to: time2) { _ in - QueuePlayer.notificationCenter.post(name: notificationName2, object: self) - } - }.toEventually(postNotifications(equalDiff([ - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time1]), - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time2]), - Notification(name: notificationName1, object: self), - Notification(name: .didSeek, object: player), - Notification(name: notificationName2, object: self) - ]), from: QueuePlayer.notificationCenter)) - } - - func testEnqueue() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayerMock(playerItem: item) - expect(player.timeRange).toEventuallyNot(equal(.invalid)) - waitUntil { done in - player.seek(to: CMTime(value: 1, timescale: 1)) - player.seek(to: CMTime(value: 2, timescale: 1)) - player.seek(to: CMTime(value: 3, timescale: 1)) - player.seek(to: CMTime(value: 4, timescale: 1)) - player.seek(to: CMTime(value: 5, timescale: 1)) { _ in - done() - } - } - expect(player.seeks).to(equal(5)) - } - - func testEnqueueSmooth() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayerMock(playerItem: item) - expect(player.timeRange).toEventuallyNot(equal(.invalid)) - waitUntil { done in - player.seek(to: CMTime(value: 1, timescale: 1), smooth: true) { _ in } - player.seek(to: CMTime(value: 2, timescale: 1), smooth: true) { _ in } - player.seek(to: CMTime(value: 3, timescale: 1), smooth: true) { _ in } - player.seek(to: CMTime(value: 4, timescale: 1), smooth: true) { _ in } - player.seek(to: CMTime(value: 5, timescale: 1), smooth: true) { _ in - done() - } - } - expect(player.seeks).to(equal(2)) - } - - func testTargetSeekTimeWithMultipleSeeks() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - expect(player.timeRange).toEventuallyNot(equal(.invalid)) - - let time1 = CMTime(value: 1, timescale: 1) - player.seek(to: time1) - expect(player.targetSeekTime).to(equal(time1)) - - let time2 = CMTime(value: 2, timescale: 1) - player.seek(to: time2) - expect(player.targetSeekTime).to(equal(time2)) - } -} diff --git a/Tests/PlayerTests/QueuePlayer/QueuePlayerSeekTimePublisherTests.swift b/Tests/PlayerTests/QueuePlayer/QueuePlayerSeekTimePublisherTests.swift deleted file mode 100644 index 427cb1c5..00000000 --- a/Tests/PlayerTests/QueuePlayer/QueuePlayerSeekTimePublisherTests.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class QueuePlayerSeekTimePublisherTests: TestCase { - func testEmpty() { - let player = QueuePlayer() - expectAtLeastEqualPublished( - values: [nil], - from: player.seekTimePublisher() - ) - } - - func testSeek() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - let time = CMTime(value: 1, timescale: 1) - expectAtLeastEqualPublished( - values: [nil, time, nil], - from: player.seekTimePublisher() - ) { - player.seek(to: time) - } - } - - func testMultipleSeek() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - let time1 = CMTime(value: 1, timescale: 1) - let time2 = CMTime(value: 2, timescale: 1) - expectAtLeastEqualPublished( - values: [nil, time1, nil, time2, nil], - from: player.seekTimePublisher() - ) { - player.seek(to: time1) - player.seek(to: time2) - } - } - - func testMultipleSeeksAtTheSameLocation() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - let time = CMTime(value: 1, timescale: 1) - expectAtLeastEqualPublished( - values: [nil, time, nil, time, nil], - from: player.seekTimePublisher() - ) { - player.seek(to: time) - player.seek(to: time) - } - } - - func testMultipleSeeksWithinTimeRange() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - player.play() - expect(item.timeRange).toEventuallyNot(equal(.invalid)) - - let time1 = CMTime(value: 1, timescale: 1) - let time2 = CMTime(value: 2, timescale: 1) - expectAtLeastEqualPublished( - values: [nil, time1, time2, nil], - from: player.seekTimePublisher() - ) { - player.seek(to: time1) - player.seek(to: time2) - } - } - - func testMultipleSeeksAtTheSameLocationWithinTimeRange() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - player.play() - expect(item.timeRange).toEventuallyNot(equal(.invalid)) - - let time = CMTime(value: 1, timescale: 1) - expectAtLeastEqualPublished( - values: [nil, time, nil], - from: player.seekTimePublisher() - ) { - player.seek(to: time) - player.seek(to: time) - } - } -} diff --git a/Tests/PlayerTests/QueuePlayer/QueuePlayerSmoothSeekTests.swift b/Tests/PlayerTests/QueuePlayer/QueuePlayerSmoothSeekTests.swift deleted file mode 100644 index c775b03e..00000000 --- a/Tests/PlayerTests/QueuePlayer/QueuePlayerSmoothSeekTests.swift +++ /dev/null @@ -1,173 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Nimble -import OrderedCollections -import PillarboxCircumspect -import PillarboxStreams - -final class QueuePlayerSmoothSeekTests: TestCase { - func testNotificationsForSeekWithEmptyPlayer() { - let player = QueuePlayer() - expect { - player.seek(to: CMTime(value: 1, timescale: 1), smooth: true) { finished in - expect(finished).to(beTrue()) - } - }.to(postNotifications(equalDiff([]), from: QueuePlayer.notificationCenter)) - } - - func testNotificationsForSeek() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - let time = CMTime(value: 1, timescale: 1) - expect { - player.seek(to: time, smooth: true) { finished in - expect(finished).to(beTrue()) - } - }.to(postNotifications(equalDiff([ - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time]), - Notification(name: .didSeek, object: player) - ]), from: QueuePlayer.notificationCenter)) - } - - func testNotificationsForMultipleSeeks() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - let time1 = CMTime(value: 1, timescale: 1) - let time2 = CMTime(value: 2, timescale: 1) - expect { - player.seek(to: time1, smooth: true) { finished in - expect(finished).to(beTrue()) - } - player.seek(to: time2, smooth: true) { finished in - expect(finished).to(beTrue()) - } - }.to(postNotifications(equalDiff([ - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time1]), - Notification(name: .didSeek, object: player), - - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time2]), - Notification(name: .didSeek, object: player) - ]), from: QueuePlayer.notificationCenter)) - } - - func testNotificationsForMultipleSeeksWithinTimeRange() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - expect(item.timeRange).toEventuallyNot(equal(.invalid)) - - let time1 = CMTime(value: 1, timescale: 1) - let time2 = CMTime(value: 2, timescale: 1) - expect { - player.seek(to: time1, smooth: true) { finished in - expect(finished).to(beTrue()) - } - player.seek(to: time2, smooth: true) { finished in - expect(finished).to(beTrue()) - } - }.toEventually(postNotifications(equalDiff([ - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time1]), - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time2]), - Notification(name: .didSeek, object: player) - ]), from: QueuePlayer.notificationCenter)) - } - - func testNotificationsForSmoothSeekAfterSeekWithinTimeRange() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - expect(item.timeRange).toEventuallyNot(equal(.invalid)) - - let time1 = CMTime(value: 1, timescale: 1) - let time2 = CMTime(value: 2, timescale: 1) - expect { - player.seek(to: time1) { finished in - expect(finished).to(beTrue()) - } - player.seek(to: time2, smooth: true) { finished in - expect(finished).to(beTrue()) - } - }.toEventually(postNotifications(equalDiff([ - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time1]), - Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time2]), - Notification(name: .didSeek, object: player) - ]), from: QueuePlayer.notificationCenter)) - } - - func testCompletionsForMultipleSeeks() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - expect(item.timeRange).toEventuallyNot(equal(.invalid)) - - let time1 = CMTime(value: 1, timescale: 1) - let time2 = CMTime(value: 2, timescale: 1) - let time3 = CMTime(value: 3, timescale: 1) - - var results = OrderedDictionary() - - func completion(index: Int) -> ((Bool) -> Void) { - { finished in - expect(results[index]).to(beNil()) - results[index] = finished - } - } - - player.seek(to: time1, smooth: true, completionHandler: completion(index: 1)) - player.seek(to: time2, smooth: true, completionHandler: completion(index: 2)) - player.seek(to: time3, smooth: true, completionHandler: completion(index: 3)) - - expect(results).toEventually(equalDiff([ - 1: true, - 2: true, - 3: true - ])) - } - - func testCompletionsForMultipleSeeksEndingWithSmoothSeek() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - expect(item.timeRange).toEventuallyNot(equal(.invalid)) - - let time1 = CMTime(value: 1, timescale: 1) - let time2 = CMTime(value: 2, timescale: 1) - let time3 = CMTime(value: 3, timescale: 1) - - var results = OrderedDictionary() - - func completion(index: Int) -> ((Bool) -> Void) { - { finished in - expect(results[index]).to(beNil()) - results[index] = finished - } - } - - player.seek(to: time1, smooth: false, completionHandler: completion(index: 1)) - player.seek(to: time2, smooth: false, completionHandler: completion(index: 2)) - player.seek(to: time3, smooth: true, completionHandler: completion(index: 3)) - - expect(results).toEventually(equalDiff([ - 1: false, - 2: true, - 3: true - ])) - } - - func testTargetSeekTimeWithMultipleSeeks() { - let item = AVPlayerItem(url: Stream.onDemand.url) - let player = QueuePlayer(playerItem: item) - expect(player.timeRange).toEventuallyNot(equal(.invalid)) - - let time1 = CMTime(value: 1, timescale: 1) - player.seek(to: time1, smooth: true) { _ in } - expect(player.targetSeekTime).to(equal(time1)) - - let time2 = CMTime(value: 2, timescale: 1) - player.seek(to: time2, smooth: true) { _ in } - expect(player.targetSeekTime).to(equal(time2)) - } -} diff --git a/Tests/PlayerTests/Resources/invalid.jpg b/Tests/PlayerTests/Resources/invalid.jpg deleted file mode 100644 index e69de29b..00000000 diff --git a/Tests/PlayerTests/Resources/pixel.jpg b/Tests/PlayerTests/Resources/pixel.jpg deleted file mode 100644 index d07bd67dd7ff3b18ec990a01c72726844270b086..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 373 zcmex=0PFu-3>+Z0Gca(26hN3rO4z^(m;``AmRJ?AgB37?6l8-G2%uZR0JVbAo}nJ3 RKoGkECPo4Zm>~*o0su)5Gfn^i diff --git a/Tests/PlayerTests/Skips/SkipBackwardChecksTests.swift b/Tests/PlayerTests/Skips/SkipBackwardChecksTests.swift deleted file mode 100644 index dede8231..00000000 --- a/Tests/PlayerTests/Skips/SkipBackwardChecksTests.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class SkipBackwardChecksTests: TestCase { - func testCannotSkipWhenEmpty() { - let player = Player() - expect(player.canSkipBackward()).to(beFalse()) - } - - func testCanSkipForOnDemand() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.canSkipBackward()).to(beTrue()) - } - - func testCannotSkipForLive() { - let player = Player(item: .simple(url: Stream.live.url)) - expect(player.streamType).toEventually(equal(.live)) - expect(player.canSkipBackward()).to(beFalse()) - } - - func testCanSkipForDvr() { - let player = Player(item: .simple(url: Stream.dvr.url)) - expect(player.streamType).toEventually(equal(.dvr)) - expect(player.canSkipBackward()).to(beTrue()) - } -} diff --git a/Tests/PlayerTests/Skips/SkipBackwardTests.swift b/Tests/PlayerTests/Skips/SkipBackwardTests.swift deleted file mode 100644 index d46555db..00000000 --- a/Tests/PlayerTests/Skips/SkipBackwardTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class SkipBackwardTests: TestCase { - func testSkipWhenEmpty() { - let player = Player() - waitUntil { done in - player.skipBackward { finished in - expect(finished).to(beTrue()) - done() - } - } - } - - func testSkipForOnDemand() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.time()).to(equal(.zero)) - - waitUntil { done in - player.skipBackward { _ in - expect(player.time()).to(equal(.zero)) - done() - } - } - } - - func testMultipleSkipsForOnDemand() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.time()).to(equal(.zero)) - - waitUntil { done in - player.skipBackward { finished in - expect(finished).to(beFalse()) - } - - player.skipBackward { finished in - expect(player.time()).to(equal(.zero)) - expect(finished).to(beTrue()) - done() - } - } - } - - func testSkipForLive() { - let player = Player(item: .simple(url: Stream.live.url)) - expect(player.streamType).toEventually(equal(.live)) - waitUntil { done in - player.skipBackward { finished in - expect(finished).to(beTrue()) - done() - } - } - } - - func testSkipForDvr() { - let player = Player(item: .simple(url: Stream.dvr.url)) - expect(player.streamType).toEventually(equal(.dvr)) - let headTime = player.time() - waitUntil { done in - player.skipBackward { finished in - expect(player.time()).to(equal(headTime + player.backwardSkipTime, by: beClose(within: player.chunkDuration.seconds))) - expect(finished).to(beTrue()) - done() - } - } - } -} diff --git a/Tests/PlayerTests/Skips/SkipForwardChecksTests.swift b/Tests/PlayerTests/Skips/SkipForwardChecksTests.swift deleted file mode 100644 index 45a8bc82..00000000 --- a/Tests/PlayerTests/Skips/SkipForwardChecksTests.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class SkipForwardChecksTests: TestCase { - func testCannotSkipWhenEmpty() { - let player = Player() - expect(player.canSkipForward()).to(beFalse()) - } - - func testCanSkipForOnDemand() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.canSkipForward()).to(beTrue()) - } - - func testCannotSkipForLive() { - let player = Player(item: .simple(url: Stream.live.url)) - expect(player.streamType).toEventually(equal(.live)) - expect(player.canSkipForward()).to(beFalse()) - } - - func testCannotSkipForDvr() { - let player = Player(item: .simple(url: Stream.dvr.url)) - expect(player.streamType).toEventually(equal(.dvr)) - expect(player.canSkipForward()).to(beFalse()) - } -} diff --git a/Tests/PlayerTests/Skips/SkipForwardTests.swift b/Tests/PlayerTests/Skips/SkipForwardTests.swift deleted file mode 100644 index 94b48028..00000000 --- a/Tests/PlayerTests/Skips/SkipForwardTests.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Combine -import CoreMedia -import Nimble -import PillarboxCircumspect -import PillarboxStreams - -final class SkipForwardTests: TestCase { - private func isSeekingPublisher(for player: Player) -> AnyPublisher { - player.propertiesPublisher - .slice(at: \.isSeeking) - .eraseToAnyPublisher() - } - - func testSkipWhenEmpty() { - let player = Player() - waitUntil { done in - player.skipForward { finished in - expect(finished).to(beTrue()) - done() - } - } - } - - func testSkipForOnDemand() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.time()).to(equal(.zero)) - - waitUntil { done in - player.skipForward { _ in - expect(player.time()).to(equal(player.forwardSkipTime, by: beClose(within: player.chunkDuration.seconds))) - done() - } - } - } - - func testMultipleSkipsForOnDemand() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.time()).to(equal(.zero)) - - waitUntil { done in - player.skipForward { finished in - expect(finished).to(beFalse()) - } - - player.skipForward { finished in - expect(player.time()).to(equal(CMTimeMultiply(player.forwardSkipTime, multiplier: 2), by: beClose(within: player.chunkDuration.seconds))) - expect(finished).to(beTrue()) - done() - } - } - } - - func testSkipForLive() { - let player = Player(item: .simple(url: Stream.live.url)) - expect(player.streamType).toEventually(equal(.live)) - waitUntil { done in - player.skipForward { finished in - expect(finished).to(beTrue()) - done() - } - } - } - - func testSkipForDvr() { - let player = Player(item: .simple(url: Stream.dvr.url)) - expect(player.streamType).toEventually(equal(.dvr)) - let headTime = player.time() - waitUntil { done in - player.skipForward { finished in - expect(player.time()).to(equal(headTime, by: beClose(within: player.chunkDuration.seconds))) - expect(finished).to(beTrue()) - done() - } - } - } - - func testSkipNearEndDoesNotSeekAnymore() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.time()).to(equal(.zero)) - let seekTo = Stream.onDemand.duration - CMTime(value: 1, timescale: 1) - - waitUntil { done in - player.seek(at(seekTo)) { finished in - expect(finished).to(beTrue()) - done() - } - } - - expectNothingPublishedNext(from: isSeekingPublisher(for: player), during: .seconds(2)) { - player.skipForward() - } - } - - func testSkipNearEndCompletion() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.time()).to(equal(.zero)) - let seekTo = Stream.onDemand.duration - CMTime(value: 1, timescale: 1) - - waitUntil { done in - player.seek(at(seekTo)) { finished in - expect(finished).to(beTrue()) - done() - } - } - - waitUntil { done in - player.skipForward { finished in - expect(finished).to(beTrue()) - expect(player.time()).to(equal(seekTo, by: beClose(within: 0.5))) - done() - } - } - } -} diff --git a/Tests/PlayerTests/Skips/SkipToDefaultChecksTests.swift b/Tests/PlayerTests/Skips/SkipToDefaultChecksTests.swift deleted file mode 100644 index f2314685..00000000 --- a/Tests/PlayerTests/Skips/SkipToDefaultChecksTests.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble -import PillarboxStreams - -final class SkipToDefaultChecksTests: TestCase { - func testCannotSkipWhenEmpty() { - let player = Player() - expect(player.canSkipToDefault()).to(beFalse()) - } - - func testCannotSkipForUnknown() { - let player = Player(item: .simple(url: Stream.unavailable.url)) - expect(player.streamType).toEventually(equal(.unknown)) - expect(player.canSkipToDefault()).to(beFalse()) - } - - func testCanSkipForOnDemand() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - expect(player.canSkipToDefault()).to(beTrue()) - } - - func testCannotSkipForDvrInLiveConditions() { - let player = Player(item: .simple(url: Stream.dvr.url)) - expect(player.streamType).toEventually(equal(.dvr)) - expect(player.canSkipToDefault()).to(beFalse()) - } - - func testCanSkipForDvrInPastConditions() { - let player = Player(item: .simple(url: Stream.dvr.url)) - expect(player.streamType).toEventually(equal(.dvr)) - - waitUntil { done in - player.seek(at(CMTime(value: 1, timescale: 1))) { _ in - done() - } - } - - expect(player.canSkipToDefault()).to(beTrue()) - } - - func testCanSkipForLive() { - let player = Player(item: .simple(url: Stream.live.url)) - expect(player.streamType).toEventually(equal(.live)) - expect(player.canSkipToDefault()).to(beTrue()) - } -} diff --git a/Tests/PlayerTests/Skips/SkipToDefaultTests.swift b/Tests/PlayerTests/Skips/SkipToDefaultTests.swift deleted file mode 100644 index 225a9c2a..00000000 --- a/Tests/PlayerTests/Skips/SkipToDefaultTests.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble -import PillarboxStreams - -final class SkipToDefaultTests: TestCase { - func testSkipWhenEmpty() { - let player = Player() - waitUntil { done in - player.skipToDefault { finished in - expect(finished).to(beTrue()) - expect(player.time()).to(equal(.invalid)) - done() - } - } - } - - func testSkipForUnknown() { - let player = Player(item: .simple(url: Stream.unavailable.url)) - expect(player.streamType).toEventually(equal(.unknown)) - waitUntil { done in - player.skipToDefault { finished in - expect(finished).to(beTrue()) - expect(player.time()).to(equal(.zero)) - done() - } - } - } - - func testSkipForOnDemand() { - let player = Player(item: .simple(url: Stream.onDemand.url)) - expect(player.streamType).toEventually(equal(.onDemand)) - waitUntil { done in - player.skipToDefault { finished in - expect(finished).to(beTrue()) - expect(player.time()).to(equal(.zero)) - done() - } - } - } - - func testSkipForLive() { - let player = Player(item: .simple(url: Stream.live.url)) - expect(player.streamType).toEventually(equal(.live)) - waitUntil { done in - player.skipToDefault { finished in - expect(finished).to(beTrue()) - done() - } - } - } - - func testSkipForDvrInLiveConditions() { - let item = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.dvr)) - waitUntil { done in - player.skipToDefault { finished in - expect(finished).to(beTrue()) - done() - } - } - } - - func testSkipForDvrInPastConditions() { - let item = PlayerItem.simple(url: Stream.dvr.url) - let player = Player(item: item) - expect(player.streamType).toEventually(equal(.dvr)) - - waitUntil { done in - player.seek(at(CMTime(value: 1, timescale: 1))) { finished in - expect(finished).to(beTrue()) - done() - } - } - - waitUntil { done in - player.skipToDefault { finished in - expect(finished).to(beTrue()) - done() - } - } - } -} diff --git a/Tests/PlayerTests/Tools/AVMediaSelectionOptionMock.swift b/Tests/PlayerTests/Tools/AVMediaSelectionOptionMock.swift deleted file mode 100644 index 9fd618cb..00000000 --- a/Tests/PlayerTests/Tools/AVMediaSelectionOptionMock.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import AVFoundation - -class AVMediaSelectionOptionMock: AVMediaSelectionOption { - override var displayName: String { - _displayName - } - - override var locale: Locale? { - _locale - } - - private let _displayName: String - private let _locale: Locale - private let _characteristics: [AVMediaCharacteristic] - - init(displayName: String, languageCode: String = "", characteristics: [AVMediaCharacteristic] = []) { - _displayName = displayName - _locale = Locale(identifier: languageCode) - _characteristics = characteristics - super.init() - } - - override func hasMediaCharacteristic(_ mediaCharacteristic: AVMediaCharacteristic) -> Bool { - _characteristics.contains(mediaCharacteristic) - } -} diff --git a/Tests/PlayerTests/Tools/ContentKeySessionDelegateMock.swift b/Tests/PlayerTests/Tools/ContentKeySessionDelegateMock.swift deleted file mode 100644 index 9609cc0f..00000000 --- a/Tests/PlayerTests/Tools/ContentKeySessionDelegateMock.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import AVFoundation - -final class ContentKeySessionDelegateMock: NSObject, AVContentKeySessionDelegate { - func contentKeySession(_ session: AVContentKeySession, didProvide keyRequest: AVContentKeyRequest) {} -} diff --git a/Tests/PlayerTests/Tools/LanguageIdentifiable.swift b/Tests/PlayerTests/Tools/LanguageIdentifiable.swift deleted file mode 100644 index 3e3210a6..00000000 --- a/Tests/PlayerTests/Tools/LanguageIdentifiable.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import AVFoundation -import PillarboxPlayer - -protocol LanguageIdentifiable { - var languageIdentifier: String? { get } -} - -extension MediaSelectionOption: LanguageIdentifiable { - var languageIdentifier: String? { - switch self { - case let .on(option): - return option.languageIdentifier - default: - return nil - } - } -} - -extension AVMediaSelectionOption: LanguageIdentifiable { - var languageIdentifier: String? { - locale?.identifier - } -} diff --git a/Tests/PlayerTests/Tools/Matchers.swift b/Tests/PlayerTests/Tools/Matchers.swift deleted file mode 100644 index af46caf0..00000000 --- a/Tests/PlayerTests/Tools/Matchers.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import Nimble - -/// A Nimble matcher that checks language identifiers. -func haveLanguageIdentifier(_ identifier: String) -> Matcher where T: LanguageIdentifiable { - let message = "have language identifier \(identifier)" - return .define { actualExpression in - let actualIdentifier = try actualExpression.evaluate()?.languageIdentifier - return MatcherResult( - bool: actualIdentifier == identifier, - message: .expectedCustomValueTo(message, actual: actualIdentifier ?? "nil") - ) - } -} diff --git a/Tests/PlayerTests/Tools/MediaAccessibilityDisplayType.swift b/Tests/PlayerTests/Tools/MediaAccessibilityDisplayType.swift deleted file mode 100644 index e7ea68bf..00000000 --- a/Tests/PlayerTests/Tools/MediaAccessibilityDisplayType.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import MediaAccessibility - -enum MediaAccessibilityDisplayType { - case automatic - case forcedOnly - case alwaysOn(languageCode: String) - - func apply() { - switch self { - case .automatic: - MACaptionAppearanceSetDisplayType(.user, .automatic) - case .forcedOnly: - MACaptionAppearanceSetDisplayType(.user, .forcedOnly) - case let .alwaysOn(languageCode: languageCode): - MACaptionAppearanceSetDisplayType(.user, .alwaysOn) - MACaptionAppearanceAddSelectedLanguage(.user, languageCode as CFString) - } - } -} diff --git a/Tests/PlayerTests/Tools/PlayerItem.swift b/Tests/PlayerTests/Tools/PlayerItem.swift deleted file mode 100644 index 9d743b31..00000000 --- a/Tests/PlayerTests/Tools/PlayerItem.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Combine -import Foundation -import PillarboxStreams - -enum MediaMock: String { - case media1 - case media2 -} - -extension PlayerItem { - static func mock( - url: URL, - loadedAfter delay: TimeInterval, - trackerAdapters: [TrackerAdapter] = [] - ) -> Self { - let publisher = Just(Asset.simple(url: url)) - .delayIfNeeded(for: .seconds(delay), scheduler: DispatchQueue.main) - return .init(publisher: publisher, trackerAdapters: trackerAdapters) - } - - static func mock( - url: URL, - loadedAfter delay: TimeInterval, - withMetadata: AssetMetadataMock, - trackerAdapters: [TrackerAdapter] = [] - ) -> Self { - let publisher = Just(Asset.simple(url: url, metadata: withMetadata)) - .delayIfNeeded(for: .seconds(delay), scheduler: DispatchQueue.main) - return .init(publisher: publisher, trackerAdapters: trackerAdapters) - } - - static func mock( - url: URL, - withMetadataUpdateAfter delay: TimeInterval, - trackerAdapters: [TrackerAdapter] = [] - ) -> Self { - let publisher = Just(Asset.simple( - url: url, - metadata: AssetMetadataMock(title: "title1", subtitle: "subtitle1") - )) - .delayIfNeeded(for: .seconds(delay), scheduler: DispatchQueue.main) - .prepend(Asset.simple( - url: url, - metadata: AssetMetadataMock(title: "title0", subtitle: "subtitle0") - )) - return .init(publisher: publisher, trackerAdapters: trackerAdapters) - } - - static func webServiceMock(media: MediaMock, trackerAdapters: [TrackerAdapter] = []) -> Self { - let url = URL(string: "http://localhost:8123/json/\(media).json")! - return webServiceMock(url: url, trackerAdapters: trackerAdapters) - } - - static func failing( - loadedAfter delay: TimeInterval, - trackerAdapters: [TrackerAdapter] = [] - ) -> Self { - let url = URL(string: "http://localhost:8123/missing.json")! - return webServiceMock(url: url, trackerAdapters: trackerAdapters) - } - - private static func webServiceMock(url: URL, trackerAdapters: [TrackerAdapter]) -> Self { - let publisher = URLSession(configuration: .default).dataTaskPublisher(for: url) - .map(\.data) - .decode(type: AssetMetadataMock.self, decoder: JSONDecoder()) - .map { metadata in - Asset.simple(url: Stream.onDemand.url, metadata: metadata) - } - return .init(publisher: publisher, trackerAdapters: trackerAdapters) - } -} diff --git a/Tests/PlayerTests/Tools/PlayerItemTrackerMock.swift b/Tests/PlayerTests/Tools/PlayerItemTrackerMock.swift deleted file mode 100644 index ea5461ac..00000000 --- a/Tests/PlayerTests/Tools/PlayerItemTrackerMock.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Combine - -final class PlayerItemTrackerMock: PlayerItemTracker { - typealias StatePublisher = PassthroughSubject - - enum State: Equatable { - case initialized - case enabled - case metricEvents - case disabled - case deinitialized - } - - struct Configuration { - let statePublisher: StatePublisher - let sessionIdentifier: String? - - init(statePublisher: StatePublisher = .init(), sessionIdentifier: String? = nil) { - self.statePublisher = statePublisher - self.sessionIdentifier = sessionIdentifier - } - } - - private let configuration: Configuration - - var sessionIdentifier: String? { - configuration.sessionIdentifier - } - - init(configuration: Configuration) { - self.configuration = configuration - configuration.statePublisher.send(.initialized) - } - - func updateMetadata(to metadata: Void) {} - - func updateProperties(to properties: PlayerProperties) {} - - func updateMetricEvents(to events: [MetricEvent]) { - configuration.statePublisher.send(.metricEvents) - } - - func enable(for player: AVPlayer) { - configuration.statePublisher.send(.enabled) - } - - func disable(with properties: PlayerProperties) { - configuration.statePublisher.send(.disabled) - } - - deinit { - configuration.statePublisher.send(.deinitialized) - } -} - -extension PlayerItemTrackerMock { - static func adapter(statePublisher: StatePublisher, behavior: TrackingBehavior = .optional) -> TrackerAdapter { - adapter(configuration: Configuration(statePublisher: statePublisher), behavior: behavior) - } -} diff --git a/Tests/PlayerTests/Tools/ResourceLoaderDelegateMock.swift b/Tests/PlayerTests/Tools/ResourceLoaderDelegateMock.swift deleted file mode 100644 index 6991f650..00000000 --- a/Tests/PlayerTests/Tools/ResourceLoaderDelegateMock.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import AVFoundation - -final class ResourceLoaderDelegateMock: NSObject, AVAssetResourceLoaderDelegate { - func resourceLoader( - _ resourceLoader: AVAssetResourceLoader, - shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest - ) -> Bool { - true - } - - func resourceLoader( - _ resourceLoader: AVAssetResourceLoader, - shouldWaitForRenewalOfRequestedResource renewalRequest: AVAssetResourceRenewalRequest - ) -> Bool { - renewalRequest.finishLoading() - return true - } -} diff --git a/Tests/PlayerTests/Tools/Similarity.swift b/Tests/PlayerTests/Tools/Similarity.swift deleted file mode 100644 index 8b96596e..00000000 --- a/Tests/PlayerTests/Tools/Similarity.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import PillarboxCircumspect -import UIKit - -extension ImageSource: Similar { - public static func ~~ (lhs: ImageSource, rhs: ImageSource) -> Bool { - switch (lhs.kind, rhs.kind) { - case (.none, .none): - return true - case let ( - .url( - standardResolution: lhsStandardResolutionUrl, - lowResolution: lhsLowResolutionUrl - ), - .url( - standardResolution: rhsStandardResolutionUrl, - lowResolution: rhsLowResolutionUrl - ) - ): - return lhsStandardResolutionUrl == rhsStandardResolutionUrl && lhsLowResolutionUrl == rhsLowResolutionUrl - case let (.image(lhsImage), .image(rhsImage)): - return lhsImage.pngData() == rhsImage.pngData() - default: - return false - } - } -} - -extension Resource: Similar { - public static func ~~ (lhs: PillarboxPlayer.Resource, rhs: PillarboxPlayer.Resource) -> Bool { - switch (lhs, rhs) { - case let (.simple(url: lhsUrl), .simple(url: rhsUrl)), - let (.custom(url: lhsUrl, delegate: _), .custom(url: rhsUrl, delegate: _)), - let (.encrypted(url: lhsUrl, delegate: _), .encrypted(url: rhsUrl, delegate: _)): - return lhsUrl == rhsUrl - default: - return false - } - } -} - -extension NowPlaying.Info: Similar { - public static func ~~ (lhs: Self, rhs: Self) -> Bool { - // swiftlint:disable:next legacy_objc_type - NSDictionary(dictionary: lhs).isEqual(to: rhs) - } -} - -extension MetricEvent: Similar { - public static func ~~ (lhs: MetricEvent, rhs: MetricEvent) -> Bool { - switch (lhs.kind, rhs.kind) { - case (.metadata, .metadata), (.asset, .asset), (.failure, .failure), (.warning, .warning): - return true - default: - return false - } - } -} - -func beClose(within tolerance: TimeInterval) -> ((CMTime, CMTime) -> Bool) { - CMTime.close(within: tolerance) -} - -func beClose(within tolerance: TimeInterval) -> ((CMTime?, CMTime?) -> Bool) { - CMTime.close(within: tolerance) -} - -func beClose(within tolerance: TimeInterval) -> ((CMTimeRange, CMTimeRange) -> Bool) { - CMTimeRange.close(within: tolerance) -} diff --git a/Tests/PlayerTests/Tools/TestCase.swift b/Tests/PlayerTests/Tools/TestCase.swift deleted file mode 100644 index 91a0702f..00000000 --- a/Tests/PlayerTests/Tools/TestCase.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import Nimble -import XCTest - -/// A simple test suite with more tolerant Nimble settings. Beware that `toAlways` and `toNever` expectations appearing -/// in tests will use the same value by default and should likely always provide an explicit `until` parameter. -class TestCase: XCTestCase { - override class func setUp() { - PollingDefaults.timeout = .seconds(20) - PollingDefaults.pollInterval = .milliseconds(100) - } - - override class func tearDown() { - PollingDefaults.timeout = .seconds(1) - PollingDefaults.pollInterval = .milliseconds(10) - } -} diff --git a/Tests/PlayerTests/Tools/Tools.swift b/Tests/PlayerTests/Tools/Tools.swift deleted file mode 100644 index 338e6804..00000000 --- a/Tests/PlayerTests/Tools/Tools.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -import Foundation - -struct StructError: LocalizedError { - var errorDescription: String? { - "Struct error description" - } -} diff --git a/Tests/PlayerTests/Tools/TrackerUpdateMock.swift b/Tests/PlayerTests/Tools/TrackerUpdateMock.swift deleted file mode 100644 index 12ec1754..00000000 --- a/Tests/PlayerTests/Tools/TrackerUpdateMock.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import Combine - -final class TrackerUpdateMock: PlayerItemTracker where Metadata: Equatable { - typealias StatePublisher = PassthroughSubject - - enum State: Equatable { - case enabled - case disabled - case updatedMetadata(Metadata) - case updatedProperties - } - - struct Configuration { - let statePublisher: StatePublisher - } - - private let configuration: Configuration - - init(configuration: Configuration) { - self.configuration = configuration - } - - func enable(for player: AVPlayer) { - configuration.statePublisher.send(.enabled) - } - - func updateMetadata(to metadata: Metadata) { - configuration.statePublisher.send(.updatedMetadata(metadata)) - } - - func updateProperties(to properties: PlayerProperties) { - configuration.statePublisher.send(.updatedProperties) - } - - func updateMetricEvents(to events: [MetricEvent]) {} - - func disable(with properties: PlayerProperties) { - configuration.statePublisher.send(.disabled) - } -} - -extension TrackerUpdateMock { - static func adapter(statePublisher: StatePublisher, mapper: @escaping (M) -> Metadata) -> TrackerAdapter { - adapter(configuration: Configuration(statePublisher: statePublisher), mapper: mapper) - } -} diff --git a/Tests/PlayerTests/Tracking/PlayerItemTrackerLifeCycleTests.swift b/Tests/PlayerTests/Tracking/PlayerItemTrackerLifeCycleTests.swift deleted file mode 100644 index 1a3939b0..00000000 --- a/Tests/PlayerTests/Tracking/PlayerItemTrackerLifeCycleTests.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import PillarboxCircumspect -import PillarboxStreams - -final class PlayerItemTrackerLifeCycleTests: TestCase { - func testWithShortLivedPlayer() { - let publisher = PlayerItemTrackerMock.StatePublisher() - expectAtLeastEqualPublished(values: [.initialized, .deinitialized], from: publisher) { - _ = PlayerItem.simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [PlayerItemTrackerMock.adapter(statePublisher: publisher)] - ) - } - } - - func testItemPlayback() { - let player = Player() - let publisher = PlayerItemTrackerMock.StatePublisher() - expectEqualPublished(values: [.initialized, .enabled, .metricEvents, .metricEvents], from: publisher, during: .seconds(2)) { - player.append(.simple( - url: Stream.onDemand.url, - trackerAdapters: [PlayerItemTrackerMock.adapter(statePublisher: publisher)] - )) - player.play() - } - } - - func testItemEntirePlayback() { - let player = Player() - let publisher = PlayerItemTrackerMock.StatePublisher() - expectAtLeastEqualPublished(values: [.initialized, .enabled, .metricEvents, .metricEvents, .disabled], from: publisher) { - player.append(.simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [PlayerItemTrackerMock.adapter(statePublisher: publisher)] - )) - player.play() - } - } - - func testDisableDuringDeinitPlayer() { - var player: Player? = Player() - let publisher = PlayerItemTrackerMock.StatePublisher() - expectAtLeastEqualPublished(values: [.initialized, .enabled, .disabled], from: publisher) { - player?.append(.simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [PlayerItemTrackerMock.adapter(statePublisher: publisher)] - )) - player = nil - } - } - - func testNetworkLoadedItemEntirePlayback() { - let player = Player() - let publisher = PlayerItemTrackerMock.StatePublisher() - expectAtLeastEqualPublished(values: [.initialized, .enabled, .metricEvents, .metricEvents, .disabled], from: publisher) { - player.append(.mock( - url: Stream.shortOnDemand.url, - loadedAfter: 1, - trackerAdapters: [PlayerItemTrackerMock.adapter(statePublisher: publisher)] - )) - player.play() - } - } - - func testFailedItem() { - let player = Player() - let publisher = PlayerItemTrackerMock.StatePublisher() - expectEqualPublished(values: [.initialized, .enabled, .metricEvents, .metricEvents], from: publisher, during: .milliseconds(500)) { - player.append(.simple( - url: Stream.unavailable.url, - trackerAdapters: [PlayerItemTrackerMock.adapter(statePublisher: publisher)] - )) - player.play() - } - } - - func testMoveCurrentItem() { - let publisher = PlayerItemTrackerMock.StatePublisher() - let player = Player() - expectAtLeastEqualPublished(values: [.initialized, .enabled, .metricEvents, .metricEvents], from: publisher) { - player.append(.simple( - url: Stream.onDemand.url, - trackerAdapters: [PlayerItemTrackerMock.adapter(statePublisher: publisher)] - )) - player.play() - } - expectNothingPublished(from: publisher, during: .seconds(1)) { - player.prepend(.simple(url: Stream.onDemand.url)) - } - } -} diff --git a/Tests/PlayerTests/Tracking/PlayerItemTrackerMetricPublisherTests.swift b/Tests/PlayerTests/Tracking/PlayerItemTrackerMetricPublisherTests.swift deleted file mode 100644 index 6d9895fa..00000000 --- a/Tests/PlayerTests/Tracking/PlayerItemTrackerMetricPublisherTests.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import PillarboxCircumspect -import PillarboxStreams - -final class PlayerItemTrackerMetricPublisherTests: TestCase { - func testEmptyPlayer() { - let player = Player() - expectSimilarPublished(values: [[]], from: player.metricEventsPublisher, during: .milliseconds(500)) - } - - func testItemPlayback() { - let player = Player(item: .simple(url: Stream.shortOnDemand.url)) - expectAtLeastSimilarPublished(values: [ - [], - [.anyMetadata], - [.anyMetadata, .anyAsset], - [] - ], from: player.metricEventsPublisher) { - player.play() - } - } - - func testError() { - let player = Player(item: .simple(url: Stream.unavailable.url)) - expectAtLeastSimilarPublished(values: [ - [], - [.anyMetadata], - [.anyMetadata, .anyFailure] - ], from: player.metricEventsPublisher) - } - - func testPlaylist() { - let player = Player(items: [.simple(url: Stream.shortOnDemand.url), .simple(url: Stream.mediumOnDemand.url)]) - expectSimilarPublished( - values: [ - [], - [.anyMetadata], - [.anyMetadata, .anyAsset], - [.anyMetadata, .anyAsset] - ], - from: player.metricEventsPublisher, - during: .seconds(2) - ) { - player.play() - } - } -} diff --git a/Tests/PlayerTests/Tracking/PlayerItemTrackerSessionTests.swift b/Tests/PlayerTests/Tracking/PlayerItemTrackerSessionTests.swift deleted file mode 100644 index 1e02605f..00000000 --- a/Tests/PlayerTests/Tracking/PlayerItemTrackerSessionTests.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class PlayerItemTrackerSessionTests: TestCase { - func testEmpty() { - let player = Player() - expect(player.currentSessionIdentifiers(trackedBy: PlayerItemTrackerMock.self)).to(beEmpty()) - } - - func testSessions() { - let player = Player( - item: .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - PlayerItemTrackerMock.adapter(configuration: .init(sessionIdentifier: "A")), - PlayerItemTrackerMock.adapter(configuration: .init()), - PlayerItemTrackerMock.adapter(configuration: .init(sessionIdentifier: "B")) - ] - ) - ) - expect(player.currentSessionIdentifiers(trackedBy: PlayerItemTrackerMock.self)).to(equal(["A", "B"])) - } -} diff --git a/Tests/PlayerTests/Tracking/PlayerItemTrackerUpdateTests.swift b/Tests/PlayerTests/Tracking/PlayerItemTrackerUpdateTests.swift deleted file mode 100644 index 5bb52734..00000000 --- a/Tests/PlayerTests/Tracking/PlayerItemTrackerUpdateTests.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import PillarboxCircumspect -import PillarboxStreams - -final class PlayerItemTrackerUpdateTests: TestCase { - func testMetadata() { - let player = Player() - let publisher = TrackerUpdateMock.StatePublisher() - let item = PlayerItem.simple( - url: Stream.shortOnDemand.url, - metadata: AssetMetadataMock(title: "title"), - trackerAdapters: [ - TrackerUpdateMock.adapter(statePublisher: publisher) { $0.title } - ] - ) - expectAtLeastEqualPublished( - values: [ - .updatedMetadata("title"), - .enabled, - .updatedProperties, - .disabled - ], - from: publisher.removeDuplicates() - ) { - player.append(item) - player.play() - } - } - - func testMetadataUpdate() { - let player = Player() - let publisher = TrackerUpdateMock.StatePublisher() - let item = PlayerItem.mock(url: Stream.shortOnDemand.url, withMetadataUpdateAfter: 1, trackerAdapters: [ - TrackerUpdateMock.adapter(statePublisher: publisher) { $0.title } - ]) - expectAtLeastEqualPublished( - values: [ - .updatedMetadata("title0"), - .enabled, - .updatedProperties, - .updatedMetadata("title1"), - .updatedProperties, - .disabled - ], - from: publisher.removeDuplicates() - ) { - player.append(item) - player.play() - } - } -} diff --git a/Tests/PlayerTests/Tracking/PlayerTrackingTests.swift b/Tests/PlayerTests/Tracking/PlayerTrackingTests.swift deleted file mode 100644 index cb036711..00000000 --- a/Tests/PlayerTests/Tracking/PlayerTrackingTests.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import PillarboxCircumspect -import PillarboxStreams - -final class PlayerTrackingTests: TestCase { - func testTrackingDisabled() { - let player = Player() - player.isTrackingEnabled = false - let publisher = PlayerItemTrackerMock.StatePublisher() - - expectEqualPublished(values: [.initialized], from: publisher, during: .milliseconds(500)) { - player.append( - .simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [ - PlayerItemTrackerMock.adapter(statePublisher: publisher) - ] - ) - ) - player.play() - } - } - - func testTrackingEnabledDuringPlayback() { - let player = Player() - player.isTrackingEnabled = false - - let publisher = PlayerItemTrackerMock.StatePublisher() - - expectEqualPublished(values: [.initialized], from: publisher, during: .seconds(1)) { - player.append( - .simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [ - PlayerItemTrackerMock.adapter(statePublisher: publisher) - ] - ) - ) - } - - expectAtLeastEqualPublished( - values: [.enabled, .metricEvents, .disabled], - from: publisher - ) { - player.isTrackingEnabled = true - player.play() - } - } - - func testTrackingDisabledDuringPlayback() { - let player = Player() - player.isTrackingEnabled = true - - let publisher = PlayerItemTrackerMock.StatePublisher() - - expectEqualPublished(values: [.initialized, .enabled, .metricEvents, .metricEvents], from: publisher, during: .seconds(1)) { - player.append( - .simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [ - PlayerItemTrackerMock.adapter(statePublisher: publisher) - ] - ) - ) - } - - expectAtLeastEqualPublished( - values: [.disabled], - from: publisher - ) { - player.isTrackingEnabled = false - player.play() - } - } - - func testTrackingEnabledTwice() { - let publisher = PlayerItemTrackerMock.StatePublisher() - - let player = Player(item: .simple(url: Stream.shortOnDemand.url, trackerAdapters: [PlayerItemTrackerMock.adapter(statePublisher: publisher)])) - player.isTrackingEnabled = true - - expectEqualPublished(values: [.metricEvents, .metricEvents], from: publisher, during: .seconds(1)) { - player.isTrackingEnabled = true - } - } - - func testMandatoryTracker() { - let player = Player() - player.isTrackingEnabled = false - - let publisher = PlayerItemTrackerMock.StatePublisher() - - expectEqualPublished(values: [.initialized, .enabled, .metricEvents, .metricEvents], from: publisher, during: .seconds(1)) { - player.append( - .simple( - url: Stream.shortOnDemand.url, - trackerAdapters: [ - PlayerItemTrackerMock.adapter(statePublisher: publisher, behavior: .mandatory) - ] - ) - ) - } - } - - func testEnablingTrackingMustNotEmitMetricEventsAgainForMandatoryTracker() { - let player = Player() - player.isTrackingEnabled = false - - let publisher = PlayerItemTrackerMock.StatePublisher() - - expectEqualPublished(values: [.initialized, .enabled, .metricEvents, .metricEvents], from: publisher, during: .seconds(1)) { - player.append( - .simple( - url: Stream.onDemand.url, - trackerAdapters: [ - PlayerItemTrackerMock.adapter(statePublisher: publisher, behavior: .mandatory) - ] - ) - ) - } - - expectNothingPublished(from: publisher, during: .seconds(1)) { - player.isTrackingEnabled = true - } - } -} diff --git a/Tests/PlayerTests/Types/CMTimeRangeTests.swift b/Tests/PlayerTests/Types/CMTimeRangeTests.swift deleted file mode 100644 index 361b0f94..00000000 --- a/Tests/PlayerTests/Types/CMTimeRangeTests.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble - -final class CMTimeRangeTests: TestCase { - func testEmpty() { - expect(CMTimeRange.flatten([])).to(beEmpty()) - } - - func testNoOverlap() { - let timeRanges: [CMTimeRange] = [ - .init(start: .init(value: 1, timescale: 1), end: .init(value: 10, timescale: 1)), - .init(start: .init(value: 20, timescale: 1), end: .init(value: 30, timescale: 1)) - ] - expect(CMTimeRange.flatten(timeRanges)).to(equal(timeRanges)) - } - - func testOverlap() { - let timeRanges: [CMTimeRange] = [ - .init(start: .init(value: 1, timescale: 1), end: .init(value: 10, timescale: 1)), - .init(start: .init(value: 5, timescale: 1), end: .init(value: 30, timescale: 1)) - ] - expect(CMTimeRange.flatten(timeRanges)).to(equal( - [ - .init(start: .init(value: 1, timescale: 1), end: .init(value: 30, timescale: 1)) - ] - )) - } - - func testContained() { - let timeRanges: [CMTimeRange] = [ - .init(start: .init(value: 1, timescale: 1), end: .init(value: 10, timescale: 1)), - .init(start: .init(value: 2, timescale: 1), end: .init(value: 8, timescale: 1)) - ] - expect(CMTimeRange.flatten(timeRanges)).to(equal( - [ - .init(start: .init(value: 1, timescale: 1), end: .init(value: 10, timescale: 1)) - ] - )) - } -} diff --git a/Tests/PlayerTests/Types/CMTimeTests.swift b/Tests/PlayerTests/Types/CMTimeTests.swift deleted file mode 100644 index 4f4e8956..00000000 --- a/Tests/PlayerTests/Types/CMTimeTests.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble - -final class CMTimeTests: TestCase { - func testClampedWithNonEmptyRange() { - let range = CMTimeRange(start: CMTime(value: 1, timescale: 1), end: CMTime(value: 10, timescale: 1)) - expect(CMTime.zero.clamped(to: range)).to(equal(CMTime(value: 1, timescale: 1))) - expect(CMTime.invalid.clamped(to: range)).to(equal(.invalid)) - expect(CMTime(value: 1, timescale: 1).clamped(to: range)).to(equal(CMTime(value: 1, timescale: 1))) - expect(CMTime(value: 5, timescale: 1).clamped(to: range)).to(equal(CMTime(value: 5, timescale: 1))) - expect(CMTime(value: 10, timescale: 1).clamped(to: range)).to(equal(CMTime(value: 10, timescale: 1))) - expect(CMTime(value: 20, timescale: 1).clamped(to: range)).to(equal(CMTime(value: 10, timescale: 1))) - } - - func testClampedWithNonEmptyRangeAndOffset() { - let range = CMTimeRange(start: CMTime(value: 1, timescale: 1), end: CMTime(value: 10, timescale: 1)) - let offset = CMTime(value: 1, timescale: 10) - expect(CMTime.zero.clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) - expect(CMTime.invalid.clamped(to: range, offset: offset)).to(equal(.invalid)) - expect(CMTime(value: 1, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) - expect(CMTime(value: 5, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 5, timescale: 1))) - expect(CMTime(value: 10, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 99, timescale: 10))) - expect(CMTime(value: 20, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 99, timescale: 10))) - } - - func testClampedWithNonEmptyRangeAndLargeOffset() { - let range = CMTimeRange(start: CMTime(value: 1, timescale: 1), end: CMTime(value: 10, timescale: 1)) - let offset = CMTime(value: 100, timescale: 1) - expect(CMTime.zero.clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) - expect(CMTime.invalid.clamped(to: range, offset: offset)).to(equal(.invalid)) - expect(CMTime(value: 1, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) - expect(CMTime(value: 5, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) - expect(CMTime(value: 10, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) - expect(CMTime(value: 20, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) - } - - func testClampedWithEmptyRange() { - let range = CMTimeRange(start: CMTime(value: 1, timescale: 1), end: CMTime(value: 1, timescale: 1)) - expect(CMTime.zero.clamped(to: range)).to(equal(CMTime(value: 1, timescale: 1))) - expect(CMTime.invalid.clamped(to: range)).to(equal(.invalid)) - expect(CMTime(value: 1, timescale: 1).clamped(to: range)).to(equal(CMTime(value: 1, timescale: 1))) - expect(CMTime(value: 5, timescale: 1).clamped(to: range)).to(equal(CMTime(value: 1, timescale: 1))) - } - - func testClampedWithEmptyRangeAndOffset() { - let range = CMTimeRange(start: CMTime(value: 1, timescale: 1), end: CMTime(value: 1, timescale: 1)) - let offset = CMTime(value: 1, timescale: 10) - expect(CMTime.zero.clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) - expect(CMTime.invalid.clamped(to: range, offset: offset)).to(equal(.invalid)) - expect(CMTime(value: 1, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) - expect(CMTime(value: 5, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) - } - - func testClampedWithInvalidRange() { - let range = CMTimeRange.invalid - expect(CMTime.zero.clamped(to: range)).to(equal(.invalid)) - expect(CMTime.invalid.clamped(to: range)).to(equal(.invalid)) - expect(CMTime(value: 1, timescale: 1).clamped(to: range)).to(equal(.invalid)) - } - - func testClampedWithInvalidRangeAndOffset() { - let range = CMTimeRange.invalid - let offset = CMTime(value: 1, timescale: 10) - expect(CMTime.zero.clamped(to: range, offset: offset)).to(equal(.invalid)) - expect(CMTime.invalid.clamped(to: range, offset: offset)).to(equal(.invalid)) - expect(CMTime(value: 1, timescale: 1).clamped(to: range, offset: offset)).to(equal(.invalid)) - } - - func testAfterWithoutTimeRange() { - let time = CMTime(value: 5, timescale: 1) - expect(time.after(timeRanges: [])).to(beNil()) - } - - func testAfterWithMatchingTimeRange() { - let time = CMTime(value: 5, timescale: 1) - expect(time.after(timeRanges: [ - .init(start: CMTime(value: 2, timescale: 1), end: CMTime(value: 6, timescale: 1)) - ])).to(equal(CMTime(value: 6, timescale: 1))) - } - - func testAfterWithoutMatchingTimeRange() { - let time = CMTime(value: 5, timescale: 1) - expect(time.after(timeRanges: [ - .init(start: CMTime(value: 12, timescale: 1), end: CMTime(value: 16, timescale: 1)) - ])).to(beNil()) - } - - func testAfterWithMatchingTimeRanges() { - let time = CMTime(value: 5, timescale: 1) - expect(time.after(timeRanges: [ - .init(start: CMTime(value: 2, timescale: 1), end: CMTime(value: 6, timescale: 1)), - .init(start: CMTime(value: 4, timescale: 1), end: CMTime(value: 10, timescale: 1)) - ])).to(equal(CMTime(value: 10, timescale: 1))) - } -} diff --git a/Tests/PlayerTests/Types/ErrorsTests.swift b/Tests/PlayerTests/Types/ErrorsTests.swift deleted file mode 100644 index ebfad89e..00000000 --- a/Tests/PlayerTests/Types/ErrorsTests.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import XCTest - -private enum EnumError: LocalizedError { - case someError - - var errorDescription: String? { - "Enum error description" - } - - var failureReason: String? { - "Enum failure reason" - } - - var recoverySuggestion: String? { - "Enum recovery suggestion" - } - - var helpAnchor: String? { - "Enum help anchor" - } -} - -final class ErrorsTests: XCTestCase { - func testNSErrorFromNSError() { - let error = NSError(domain: "domain", code: 1012, userInfo: [ - NSLocalizedDescriptionKey: "Error description", - NSLocalizedFailureReasonErrorKey: "Failure reason", - NSUnderlyingErrorKey: NSError(domain: "inner.domain", code: 2024) - ]) - expect(NSError.error(from: error)).to(equal(error)) - } - - func testNSErrorFromSwiftError() { - let error = NSError.error(from: EnumError.someError)! - expect(error.localizedDescription).to(equal("Enum error description")) - expect(error.localizedFailureReason).to(equal("Enum failure reason")) - expect(error.localizedRecoverySuggestion).to(equal("Enum recovery suggestion")) - expect(error.helpAnchor).to(equal("Enum help anchor")) - } -} diff --git a/Tests/PlayerTests/Types/ImageSourceTests.swift b/Tests/PlayerTests/Types/ImageSourceTests.swift deleted file mode 100644 index df8b4c39..00000000 --- a/Tests/PlayerTests/Types/ImageSourceTests.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import PillarboxCircumspect -import UIKit - -final class ImageSourceTests: TestCase { - func testNone() { - expectEqualPublished( - values: [.none], - from: ImageSource.none.imageSourcePublisher(), - during: .milliseconds(100) - ) - } - - func testImage() { - let image = UIImage(systemName: "circle")! - expectEqualPublished( - values: [.image(image)], - from: ImageSource.image(image).imageSourcePublisher(), - during: .milliseconds(100) - ) - } - - func testNonLoadedImageForValidUrl() { - let url = Bundle.module.url(forResource: "pixel", withExtension: "jpg")! - let source = ImageSource.url(standardResolution: url) - expectSimilarPublished( - values: [.url(standardResolution: url)], - from: source.imageSourcePublisher(), - during: .milliseconds(100) - ) - } - - func testLoadedImageForValidUrl() { - let url = Bundle.module.url(forResource: "pixel", withExtension: "jpg")! - let image = UIImage(contentsOfFile: url.path())! - let source = ImageSource.url(standardResolution: url) - expectSimilarPublished( - values: [.url(standardResolution: url), .image(image)], - from: source.imageSourcePublisher(), - during: .milliseconds(100) - ) { - _ = source.image - } - } - - func testInvalidImageFormat() { - let url = Bundle.module.url(forResource: "invalid", withExtension: "jpg")! - let source = ImageSource.url(standardResolution: url) - expectSimilarPublished( - values: [.url(standardResolution: url), .none], - from: source.imageSourcePublisher(), - during: .milliseconds(100) - ) { - _ = source.image - } - } - - func testFailingUrl() { - let url = URL(string: "https://localhost:8123/missing.jpg")! - let source = ImageSource.url(standardResolution: url) - expectSimilarPublished( - values: [.url(standardResolution: url), .none], - from: source.imageSourcePublisher(), - during: .seconds(1) - ) { - _ = source.image - } - } -} diff --git a/Tests/PlayerTests/Types/ItemErrorTests.swift b/Tests/PlayerTests/Types/ItemErrorTests.swift deleted file mode 100644 index 865cbddf..00000000 --- a/Tests/PlayerTests/Types/ItemErrorTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble - -final class ItemErrorTests: TestCase { - func testNoInnerComment() { - expect(ItemError.innerComment(from: "The internet connection appears to be offline")) - .to(equal("The internet connection appears to be offline")) - } - - func testInnerComment() { - expect(ItemError.innerComment( - from: "The operation couldn’t be completed. (CoreBusiness.DataError error 1 - This content is not available anymore.)" - )).to(equal("This content is not available anymore.")) - - expect(ItemError.innerComment( - from: "The operation couldn’t be completed. (CoreBusiness.DataError error 1 - Not found)" - )).to(equal("Not found")) - - expect(ItemError.innerComment( - from: "The operation couldn't be completed. (CoreMediaErrorDomain error -16839 - Unable to get playlist before long download timer.)" - )).to(equal("Unable to get playlist before long download timer.")) - - expect(ItemError.innerComment( - from: "L’opération n’a pas pu s’achever. (CoreBusiness.DataError erreur 1 - Ce contenu n'est plus disponible.)" - )).to(equal("Ce contenu n'est plus disponible.")) - } - - func testNestedInnerComments() { - expect(ItemError.innerComment( - from: """ - The operation couldn’t be completed. (CoreMediaErrorDomain error -12660 - The operation couldn’t be completed. \ - (CoreMediaErrorDomain error -12660 - HTTP 403: Forbidden)) - """ - )).to(equal("HTTP 403: Forbidden")) - } -} diff --git a/Tests/PlayerTests/Types/PlaybackSpeedTests.swift b/Tests/PlayerTests/Types/PlaybackSpeedTests.swift deleted file mode 100644 index 38d94c40..00000000 --- a/Tests/PlayerTests/Types/PlaybackSpeedTests.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble - -final class PlaybackSpeedTests: TestCase { - func testNoValueClampingToIndefiniteRange() { - let speed = PlaybackSpeed(value: 2, range: nil) - expect(speed.value).to(equal(2)) - expect(speed.range).to(beNil()) - } - - func testValueClampingToDefiniteRange() { - let speed = PlaybackSpeed(value: 2, range: 1...1) - expect(speed.value).to(equal(1)) - expect(speed.range).to(equal(1...1)) - } - - func testEffectivePropertiesWhenIndefinite() { - let speed = PlaybackSpeed.indefinite - expect(speed.effectiveValue).to(equal(1)) - expect(speed.effectiveRange).to(equal(1...1)) - } - - func testEffectivePropertiesWhenDefinite() { - let speed = PlaybackSpeed(value: 2, range: 0...2) - expect(speed.effectiveValue).to(equal(2)) - expect(speed.effectiveRange).to(equal(0...2)) - } -} diff --git a/Tests/PlayerTests/Types/PlaybackStateTests.swift b/Tests/PlayerTests/Types/PlaybackStateTests.swift deleted file mode 100644 index 0d3ece45..00000000 --- a/Tests/PlayerTests/Types/PlaybackStateTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble - -final class PlaybackStateTests: TestCase { - func testAllCases() { - expect(PlaybackState(itemStatus: .unknown, rate: 0)).to(equal(.idle)) - expect(PlaybackState(itemStatus: .unknown, rate: 1)).to(equal(.idle)) - expect(PlaybackState(itemStatus: .readyToPlay, rate: 0)).to(equal(.paused)) - expect(PlaybackState(itemStatus: .readyToPlay, rate: 1)).to(equal(.playing)) - expect(PlaybackState(itemStatus: .ended, rate: 0)).to(equal(.ended)) - expect(PlaybackState(itemStatus: .ended, rate: 1)).to(equal(.ended)) - } -} diff --git a/Tests/PlayerTests/Types/PlayerConfigurationTests.swift b/Tests/PlayerTests/Types/PlayerConfigurationTests.swift deleted file mode 100644 index a6d39906..00000000 --- a/Tests/PlayerTests/Types/PlayerConfigurationTests.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble - -final class PlayerConfigurationTests: TestCase { - func testDefaultValues() { - let configuration = PlayerConfiguration() - expect(configuration.allowsExternalPlayback).to(beTrue()) - expect(configuration.usesExternalPlaybackWhileMirroring).to(beFalse()) - expect(configuration.preventsDisplaySleepDuringVideoPlayback).to(beTrue()) - expect(configuration.navigationMode).to(equal(.smart(interval: 3))) - expect(configuration.backwardSkipInterval).to(equal(10)) - expect(configuration.forwardSkipInterval).to(equal(10)) - expect(configuration.preloadedItems).to(equal(2)) - expect(configuration.allowsConstrainedNetworkAccess).to(beTrue()) - } - - func testCustomValues() { - let configuration = PlayerConfiguration( - allowsExternalPlayback: false, - usesExternalPlaybackWhileMirroring: true, - preventsDisplaySleepDuringVideoPlayback: false, - navigationMode: .immediate, - backwardSkipInterval: 42, - forwardSkipInterval: 47, - allowsConstrainedNetworkAccess: false - ) - expect(configuration.allowsExternalPlayback).to(beFalse()) - expect(configuration.usesExternalPlaybackWhileMirroring).to(beTrue()) - expect(configuration.preventsDisplaySleepDuringVideoPlayback).to(beFalse()) - expect(configuration.navigationMode).to(equal(.immediate)) - expect(configuration.backwardSkipInterval).to(equal(42)) - expect(configuration.forwardSkipInterval).to(equal(47)) - expect(configuration.allowsConstrainedNetworkAccess).to(beFalse()) - } -} diff --git a/Tests/PlayerTests/Types/PlayerLimitsTests.swift b/Tests/PlayerTests/Types/PlayerLimitsTests.swift deleted file mode 100644 index d2daa438..00000000 --- a/Tests/PlayerTests/Types/PlayerLimitsTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import PillarboxStreams - -final class PlayerLimitsTests: TestCase { - private static let limits = PlayerLimits( - preferredPeakBitRate: 100, - preferredPeakBitRateForExpensiveNetworks: 200, - preferredMaximumResolution: .init(width: 100, height: 200), - preferredMaximumResolutionForExpensiveNetworks: .init(width: 300, height: 400) - ) - - func testDefaultValues() { - let limits = PlayerLimits() - expect(limits.preferredPeakBitRate).to(equal(0)) - expect(limits.preferredPeakBitRateForExpensiveNetworks).to(equal(0)) - expect(limits.preferredMaximumResolution).to(equal(.zero)) - expect(limits.preferredMaximumResolutionForExpensiveNetworks).to(equal(.zero)) - } - - func testCustomValues() { - let limits = PlayerLimits( - preferredPeakBitRate: 100, - preferredPeakBitRateForExpensiveNetworks: 200, - preferredMaximumResolution: .init(width: 100, height: 200), - preferredMaximumResolutionForExpensiveNetworks: .init(width: 300, height: 400) - ) - expect(limits.preferredPeakBitRate).to(equal(100)) - expect(limits.preferredPeakBitRateForExpensiveNetworks).to(equal(200)) - expect(limits.preferredMaximumResolution).to(equal(.init(width: 100, height: 200))) - expect(limits.preferredMaximumResolutionForExpensiveNetworks).to(equal(.init(width: 300, height: 400))) - } - - func testAppliedDefaultValues() { - let player = Player(items: [ - .simple(url: Stream.onDemand.url), - .simple(url: Stream.mediumOnDemand.url) - ]) - player.queuePlayer.items().forEach { item in - expect(item.preferredPeakBitRate).to(equal(0)) - expect(item.preferredPeakBitRateForExpensiveNetworks).to(equal(0)) - expect(item.preferredMaximumResolution).to(equal(.zero)) - expect(item.preferredMaximumResolutionForExpensiveNetworks).to(equal(.zero)) - } - } - - func testAppliedInitialValues() { - let player = Player(items: [ - .simple(url: Stream.onDemand.url), - .simple(url: Stream.mediumOnDemand.url) - ]) - player.limits = Self.limits - player.queuePlayer.items().forEach { item in - expect(item.preferredPeakBitRate).to(equal(100)) - expect(item.preferredPeakBitRateForExpensiveNetworks).to(equal(200)) - expect(item.preferredMaximumResolution).to(equal(.init(width: 100, height: 200))) - expect(item.preferredMaximumResolutionForExpensiveNetworks).to(equal(.init(width: 300, height: 400))) - } - } - - func testLoadedItem() { - let player = Player(item: .mock(url: Stream.onDemand.url, loadedAfter: 0.1)) - player.limits = Self.limits - expect(player.playbackState).toEventually(equal(.paused)) - player.queuePlayer.items().forEach { item in - expect(item.preferredPeakBitRate).to(equal(100)) - expect(item.preferredPeakBitRateForExpensiveNetworks).to(equal(200)) - expect(item.preferredMaximumResolution).to(equal(.init(width: 100, height: 200))) - expect(item.preferredMaximumResolutionForExpensiveNetworks).to(equal(.init(width: 300, height: 400))) - } - } -} diff --git a/Tests/PlayerTests/Types/PlayerMetadataTests.swift b/Tests/PlayerTests/Types/PlayerMetadataTests.swift deleted file mode 100644 index 70402784..00000000 --- a/Tests/PlayerTests/Types/PlayerMetadataTests.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import AVFoundation -import MediaPlayer -import Nimble - -final class PlayerMetadataTests: TestCase { - private static func value(for identifier: AVMetadataIdentifier, in items: [AVMetadataItem]) async throws -> Any? { - guard let item = AVMetadataItem.metadataItems(from: items, filteredByIdentifier: identifier).first else { return nil } - return try await item.load(.value) - } - - func testNowPlayingInfo() { - let metadata = PlayerMetadata( - title: "title", - subtitle: "subtitle", - imageSource: .image(.init(systemName: "circle")!) - ) - let nowPlayingInfo = metadata.nowPlayingInfo - expect(nowPlayingInfo[MPMediaItemPropertyTitle] as? String).to(equal("title")) - expect(nowPlayingInfo[MPMediaItemPropertyArtist] as? String).to(equal("subtitle")) - expect(nowPlayingInfo[MPMediaItemPropertyArtwork]).notTo(beNil()) - } - - func testExternalMetadata() async { - let metadata = PlayerMetadata( - identifier: "identifier", - title: "title", - subtitle: "subtitle", - description: "description", - imageSource: .image(.init(systemName: "circle")!), - episodeInformation: .long(season: 2, episode: 3) - ) - let externalMetadata = metadata.externalMetadata - await expect { - try await Self.value(for: .commonIdentifierAssetIdentifier, in: externalMetadata) as? String - }.to(equal("identifier")) - await expect { - try await Self.value(for: .commonIdentifierTitle, in: externalMetadata) as? String - }.to(equal("title")) - await expect { - try await Self.value(for: .iTunesMetadataTrackSubTitle, in: externalMetadata) as? String - }.to(equal("subtitle")) - await expect { - try await Self.value(for: .commonIdentifierDescription, in: externalMetadata) as? String - }.to(equal("description")) - await expect { - try await Self.value(for: .quickTimeUserDataCreationDate, in: externalMetadata) as? String - }.to(equal("S2, E3")) - - await expect { - try await Self.value(for: .commonIdentifierArtwork, in: externalMetadata) - } -#if os(tvOS) - .notTo(beNil()) -#else - .to(beNil()) -#endif - } -} diff --git a/Tests/PlayerTests/Types/PositionTests.swift b/Tests/PlayerTests/Types/PositionTests.swift deleted file mode 100644 index f4216f2d..00000000 --- a/Tests/PlayerTests/Types/PositionTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble - -final class PositionTests: TestCase { - func testPositionTo() { - let position = to(CMTime(value: 1, timescale: 1), toleranceBefore: CMTime(value: 2, timescale: 1), toleranceAfter: CMTime(value: 3, timescale: 1)) - expect(position.time).to(equal(CMTime(value: 1, timescale: 1))) - expect(position.toleranceBefore).to(equal(CMTime(value: 2, timescale: 1))) - expect(position.toleranceAfter).to(equal(CMTime(value: 3, timescale: 1))) - } - - func testPositionAt() { - let position = at(CMTime(value: 1, timescale: 1)) - expect(position.time).to(equal(CMTime(value: 1, timescale: 1))) - expect(position.toleranceBefore).to(equal(.zero)) - expect(position.toleranceAfter).to(equal(.zero)) - } - - func testPositionNear() { - let position = near(CMTime(value: 1, timescale: 1)) - expect(position.time).to(equal(CMTime(value: 1, timescale: 1))) - expect(position.toleranceBefore).to(equal(.positiveInfinity)) - expect(position.toleranceAfter).to(equal(.positiveInfinity)) - } - - func testPositionBefore() { - let position = before(CMTime(value: 1, timescale: 1)) - expect(position.time).to(equal(CMTime(value: 1, timescale: 1))) - expect(position.toleranceBefore).to(equal(.positiveInfinity)) - expect(position.toleranceAfter).to(equal(.zero)) - } - - func testPositionAfter() { - let position = after(CMTime(value: 1, timescale: 1)) - expect(position.time).to(equal(CMTime(value: 1, timescale: 1))) - expect(position.toleranceBefore).to(equal(.zero)) - expect(position.toleranceAfter).to(equal(.positiveInfinity)) - } -} diff --git a/Tests/PlayerTests/Types/StreamTypeTests.swift b/Tests/PlayerTests/Types/StreamTypeTests.swift deleted file mode 100644 index 4e7ef6ac..00000000 --- a/Tests/PlayerTests/Types/StreamTypeTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble - -final class StreamTypeTests: TestCase { - func testAllCases() { - expect(StreamType(for: .zero, duration: .invalid)).to(equal(.unknown)) - expect(StreamType(for: .zero, duration: .indefinite)).to(equal(.live)) - expect(StreamType(for: .finite, duration: .indefinite)).to(equal(.dvr)) - expect(StreamType(for: .zero, duration: .zero)).to(equal(.onDemand)) - } -} - -private extension CMTimeRange { - static let finite = Self(start: .zero, duration: .init(value: 1, timescale: 1)) -} diff --git a/Tests/PlayerTests/Types/TimePropertiesTests.swift b/Tests/PlayerTests/Types/TimePropertiesTests.swift deleted file mode 100644 index d8f92d49..00000000 --- a/Tests/PlayerTests/Types/TimePropertiesTests.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import CoreMedia -import Nimble - -final class TimePropertiesTests: TestCase { - func testWithoutTimeRange() { - expect(TimeProperties.timeRange(from: [])).to(beNil()) - } - - func testTimeRange() { - expect(TimeProperties.timeRange(from: [NSValue(timeRange: .finite)])).to(equal(.finite)) - } - - func testTimeRanges() { - expect(TimeProperties.timeRange(from: [ - NSValue(timeRange: .init(start: .init(value: 1, timescale: 1), duration: .init(value: 3, timescale: 1))), - NSValue(timeRange: .init(start: .init(value: 10, timescale: 1), duration: .init(value: 5, timescale: 1))) - ])).to(equal( - .init(start: .init(value: 1, timescale: 1), duration: .init(value: 14, timescale: 1)) - )) - } - - func testInvalidTimeRange() { - expect(TimeProperties.timeRange(from: [NSValue(timeRange: .invalid)])).to(equal(.invalid)) - } - - func testSeekableTimeRangeFallback() { - expect( - TimeProperties.timeRange( - loadedTimeRanges: [NSValue(timeRange: .finite)], - seekableTimeRanges: [] - ) - ) - .to(equal(.zero)) - } - - func testBufferEmptyLoadedTimeRanges() { - expect( - TimeProperties( - loadedTimeRanges: [], - seekableTimeRanges: [NSValue(timeRange: .finite)], - isPlaybackLikelyToKeepUp: true - ).buffer - ) - .to(equal(0)) - } - - func testBuffer() { - expect( - TimeProperties( - loadedTimeRanges: [NSValue(timeRange: .finite)], - seekableTimeRanges: [NSValue(timeRange: .finite)], - isPlaybackLikelyToKeepUp: true - ).buffer - ) - .to(equal(1)) - } -} - -private extension CMTimeRange { - static let finite = Self(start: .zero, duration: .init(value: 1, timescale: 1)) -} diff --git a/Tests/PlayerTests/UserInterface/VisibilityTrackerTests.swift b/Tests/PlayerTests/UserInterface/VisibilityTrackerTests.swift deleted file mode 100644 index a02d80ee..00000000 --- a/Tests/PlayerTests/UserInterface/VisibilityTrackerTests.swift +++ /dev/null @@ -1,184 +0,0 @@ -// -// Copyright (c) SRG SSR. All rights reserved. -// -// License information is available from the LICENSE file. -// - -@testable import PillarboxPlayer - -import Nimble -import ObjectiveC -import PillarboxCircumspect -import PillarboxStreams - -#if os(iOS) -final class VisibilityTrackerTests: TestCase { - func testInitiallyVisible() { - let visibilityTracker = VisibilityTracker() - expect(visibilityTracker.isUserInterfaceHidden).to(beFalse()) - } - - func testInitiallyHidden() { - let visibilityTracker = VisibilityTracker(isUserInterfaceHidden: true) - expect(visibilityTracker.isUserInterfaceHidden).to(beTrue()) - } - - func testNoToggleWithoutPlayer() { - let visibilityTracker = VisibilityTracker() - visibilityTracker.toggle() - expect(visibilityTracker.isUserInterfaceHidden).to(beFalse()) - } - - func testToggle() { - let visibilityTracker = VisibilityTracker() - visibilityTracker.player = Player() - visibilityTracker.toggle() - expect(visibilityTracker.isUserInterfaceHidden).to(beTrue()) - } - - func testInitiallyVisibleIfPaused() { - let visibilityTracker = VisibilityTracker(delay: 0.5, isUserInterfaceHidden: true) - visibilityTracker.player = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) - expect(visibilityTracker.isUserInterfaceHidden).toEventually(beFalse()) - } - - func testVisibleWhenPaused() { - let visibilityTracker = VisibilityTracker(delay: 0.5, isUserInterfaceHidden: true) - let player = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) - visibilityTracker.player = player - player.play() - expect(player.playbackState).toEventually(equal(.playing)) - expect(visibilityTracker.isUserInterfaceHidden).to(beTrue()) - player.pause() - expect(visibilityTracker.isUserInterfaceHidden).toEventually(beFalse()) - } - - func testNoAutoHideWhileIdle() { - let visibilityTracker = VisibilityTracker(delay: 0.5) - visibilityTracker.player = Player() - expect(visibilityTracker.isUserInterfaceHidden).toNever(beTrue(), until: .seconds(1)) - } - - func testAutoHideWhilePlaying() { - let visibilityTracker = VisibilityTracker(delay: 0.5) - let player = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) - player.play() - visibilityTracker.player = player - expect(visibilityTracker.isUserInterfaceHidden).toEventually(beTrue()) - } - - func testNoAutoHideWhilePaused() { - let visibilityTracker = VisibilityTracker(delay: 0.5) - visibilityTracker.player = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) - expect(visibilityTracker.isUserInterfaceHidden).toNever(beTrue(), until: .seconds(1)) - } - - func testNoAutoHideWhileEnded() { - let visibilityTracker = VisibilityTracker(delay: Stream.shortOnDemand.duration.seconds + 0.5) - let player = Player(item: PlayerItem.simple(url: Stream.shortOnDemand.url)) - player.play() - visibilityTracker.player = player - expect(visibilityTracker.isUserInterfaceHidden).toNever(beTrue(), until: .seconds(1)) - } - - func testNoAutoHideWhileFailed() { - let visibilityTracker = VisibilityTracker(delay: 0.5) - visibilityTracker.player = Player(item: PlayerItem.simple(url: Stream.unavailable.url)) - expect(visibilityTracker.isUserInterfaceHidden).toNever(beTrue(), until: .seconds(1)) - } - - func testNoAutoHideWithEmptyPlayer() { - let visibilityTracker = VisibilityTracker(delay: 0.5) - visibilityTracker.player = Player() - expect(visibilityTracker.isUserInterfaceHidden).toNever(beTrue(), until: .seconds(1)) - } - - func testNoAutoHideWithoutPlayer() { - let visibilityTracker = VisibilityTracker(delay: 0.5) - expect(visibilityTracker.isUserInterfaceHidden).toNever(beTrue(), until: .seconds(1)) - } - - func testResetAutoHide() { - let visibilityTracker = VisibilityTracker(delay: 0.3) - let player = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) - visibilityTracker.player = player - player.play() - expect(player.playbackState).toEventually(equal(.playing)) - expect(visibilityTracker.isUserInterfaceHidden).toAlways(beFalse(), until: .milliseconds(200)) - visibilityTracker.reset() - expect(visibilityTracker.isUserInterfaceHidden).toAlways(beFalse(), until: .milliseconds(200)) - } - - func testResetDoesNotShowControls() { - let visibilityTracker = VisibilityTracker(isUserInterfaceHidden: true) - visibilityTracker.reset() - expect(visibilityTracker.isUserInterfaceHidden).to(beTrue()) - } - - func testAutoHideAfterUnhide() { - let visibilityTracker = VisibilityTracker(delay: 0.5, isUserInterfaceHidden: true) - let player = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) - player.play() - visibilityTracker.player = player - visibilityTracker.toggle() - expect(visibilityTracker.isUserInterfaceHidden).toEventually(beTrue()) - } - - func testInvalidDelay() { - guard nimbleThrowAssertionsAvailable() else { return } - expect(VisibilityTracker(delay: -5)).to(throwAssertion()) - } - - func testPlayerChangeDoesNotHideUserInterface() { - let visibilityTracker = VisibilityTracker() - visibilityTracker.player = Player() - expect(visibilityTracker.isUserInterfaceHidden).to(beFalse()) - } - - func testPlayerChangeDoesNotShowUserInterface() { - let visibilityTracker = VisibilityTracker(isUserInterfaceHidden: true) - visibilityTracker.player = Player() - expect(visibilityTracker.isUserInterfaceHidden).to(beTrue()) - } - - func testPlayerChangeResetsAutoHide() { - let player1 = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) - player1.play() - expect(player1.playbackState).toEventually(equal(.playing)) - - let player2 = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) - player2.play() - expect(player2.playbackState).toEventually(equal(.playing)) - - let visibilityTracker = VisibilityTracker(delay: 0.5) - visibilityTracker.player = player1 - expect(visibilityTracker.isUserInterfaceHidden).toAlways(beFalse(), until: .milliseconds(400)) - - visibilityTracker.player = player2 - expect(visibilityTracker.isUserInterfaceHidden).toAlways(beFalse(), until: .milliseconds(400)) - } - - func testDeallocation() { - var visibilityTracker: VisibilityTracker? = VisibilityTracker() - weak var weakVisibilityTracker = visibilityTracker - autoreleasepool { - visibilityTracker = nil - } - expect(weakVisibilityTracker).to(beNil()) - } - - func testDeallocationWhilePlaying() { - var visibilityTracker: VisibilityTracker? = VisibilityTracker() - let player = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) - player.play() - visibilityTracker?.player = player - expect(player.playbackState).toEventually(equal(.playing)) - - weak var weakVisibilityTracker = visibilityTracker - autoreleasepool { - visibilityTracker = nil - } - expect(weakVisibilityTracker).to(beNil()) - } -} -#endif From 3b7f9e3dda989aba8440c7ac806614908d9aca7e Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 26 Nov 2024 07:19:24 +0100 Subject: [PATCH 35/68] Use pkgx bundle exec for fastlane --- Makefile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 71a5b49a..924dd1bd 100644 --- a/Makefile +++ b/Makefile @@ -65,18 +65,20 @@ test-streams-stop: install-pkgx @echo "... done.\n" .PHONY: test-ios -test-ios: install-pkgx +test-ios: install-pkgx install-bundler @echo "Running unit tests..." @Scripts/test-streams.sh -s - @pkgx +xcodes fastlane test_ios + @pkgx xcodes + @pkgx bundle exec fastlane test_ios @Scripts/test-streams.sh -k @echo "... done.\n" .PHONY: test-tvos -test-tvos: install-pkgx +test-tvos: install-pkgx install-bundler @echo "Running unit tests..." @Scripts/test-streams.sh -s - @pkgx fastlane test_tvos + @pkgx xcodes + @pkgx bundle exec fastlane test_tvos @Scripts/test-streams.sh -k @echo "... done.\n" From 4fc8f919628390e1105e9dc14424c6e7251f4550 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 26 Nov 2024 08:07:45 +0100 Subject: [PATCH 36/68] Remove xcodes --- Makefile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Makefile b/Makefile index 924dd1bd..249a7560 100644 --- a/Makefile +++ b/Makefile @@ -68,7 +68,6 @@ test-streams-stop: install-pkgx test-ios: install-pkgx install-bundler @echo "Running unit tests..." @Scripts/test-streams.sh -s - @pkgx xcodes @pkgx bundle exec fastlane test_ios @Scripts/test-streams.sh -k @echo "... done.\n" @@ -77,7 +76,6 @@ test-ios: install-pkgx install-bundler test-tvos: install-pkgx install-bundler @echo "Running unit tests..." @Scripts/test-streams.sh -s - @pkgx xcodes @pkgx bundle exec fastlane test_tvos @Scripts/test-streams.sh -k @echo "... done.\n" From f545418973b2ce3376fc155846d4642ccff0a64d Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 26 Nov 2024 08:33:44 +0100 Subject: [PATCH 37/68] Run on GitHub runner --- .github/workflows/pull-request.yml | 47 +++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 2eebad66..0c282561 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -4,9 +4,29 @@ name: Pull Request on: [push] jobs: + check-quality: + name: "🔎 Check quality" + runs-on: [macos-latest] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run the quality check + run: make check-quality + + build-documentation: + name: "📚 Build documentation" + runs-on: [macos-latest] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build the documentation + run: make doc + tests: name: "🧪 Tests" - runs-on: tart + runs-on: [macos-latest] strategy: matrix: platform: [ios, tvos] @@ -19,3 +39,28 @@ jobs: - name: Run tests run: make test-${{ matrix.platform }} + + archive-demos: + name: "📦 Archives" + runs-on: [macos-latest] + strategy: + matrix: + platform: [ios, tvos] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Add Apple certificate + run: | + Scripts/add-apple-certificate.sh \ + ${{ secrets.SRGSSR_APPLE_DEV_CERTIFICATE_B64 }} + + - name: Configure environment + run: | + Scripts/configure-environment.sh \ + ${{ secrets.APP_STORE_CONNECT_API_KEY_B64 }} \ + ${{ secrets.APPLE_ACCOUNT_INFO_B64 }} + + - name: Archive the demo + run: | + make archive-demo-${{ matrix.platform }} From 21d07479da52c180edab614e66d25d5737bb9305 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:03:42 +0100 Subject: [PATCH 38/68] Fix archiving with GitHub runners --- .github/workflows/pull-request.yml | 28 +++++++++++++++------------- Scripts/add-apple-certificate.sh | 27 +++++++++++++++++---------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 0c282561..ae2781f0 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -24,21 +24,21 @@ jobs: - name: Build the documentation run: make doc - tests: - name: "🧪 Tests" - runs-on: [macos-latest] - strategy: - matrix: - platform: [ios, tvos] - steps: - - name: Checkout code - uses: actions/checkout@v4 + # tests: + # name: "🧪 Tests" + # runs-on: [macos-latest] + # strategy: + # matrix: + # platform: [ios, tvos] + # steps: + # - name: Checkout code + # uses: actions/checkout@v4 - - name: Authorize microphone access for simulator - run: Scripts/bypass-simulator-trampoline.sh + # - name: Authorize microphone access for simulator + # run: Scripts/bypass-simulator-trampoline.sh - - name: Run tests - run: make test-${{ matrix.platform }} + # - name: Run tests + # run: make test-${{ matrix.platform }} archive-demos: name: "📦 Archives" @@ -53,6 +53,8 @@ jobs: - name: Add Apple certificate run: | Scripts/add-apple-certificate.sh \ + $RUNNER_TEMP \ + ${{ secrets.KEYCHAIN_PASSWORD }} \ ${{ secrets.SRGSSR_APPLE_DEV_CERTIFICATE_B64 }} - name: Configure environment diff --git a/Scripts/add-apple-certificate.sh b/Scripts/add-apple-certificate.sh index d647ea0c..54515f8a 100755 --- a/Scripts/add-apple-certificate.sh +++ b/Scripts/add-apple-certificate.sh @@ -1,22 +1,29 @@ #!/bin/bash -apple_certificate_b64="$1" +root_dir="$1" +keychain_password="$2" +apple_certificate_b64="$3" -if [[ -z $apple_certificate_b64 ]] +if [[ -z $root_dir || -z $keychain_password || -z $apple_certificate_b64 ]] then - echo "[!] Usage: $0 " + echo "[!] Usage: $0 " exit 1 fi -apple_certificate_password="" -apple_certificate_decoded_path="/tmp/certificate.p12" +keychain_path="$root_dir/app-signing.keychain-db" -keychain_password="admin" -keychain_path="$HOME/Library/Keychains/login.keychain-db" +# Should we put a password? +apple_certificate_password="6YXTQTG8JJ" +apple_certificate="$root_dir/certificate.p12" -echo "$apple_certificate_b64" | base64 --decode > "$apple_certificate_decoded_path" +echo -n "$apple_certificate_b64" | base64 --decode -o "$apple_certificate" + +# Create a temporary keychain (https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners) +security create-keychain -p "$keychain_password" "$keychain_path" +security set-keychain-settings -lut 21600 "$keychain_path" +security unlock-keychain -p "$keychain_password" "$keychain_path" # Import certificate -security import "$apple_certificate_decoded_path" -k "$keychain_path" -P "$apple_certificate_password" -T /usr/bin/security -T /usr/bin/codesign +security import "$apple_certificate" -k "$keychain_path" -P "$apple_certificate_password" -A -t cert -f pkcs12 # Authorize access to certificate private key -security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain_path" \ No newline at end of file +security set-key-partition-list -S apple-tool:,apple: -k "$keychain_password" "$keychain_path" \ No newline at end of file From d7a4bbddd9099330eb6eef325f006f7500afb335 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:11:17 +0100 Subject: [PATCH 39/68] Put back tests --- .github/workflows/pull-request.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index ae2781f0..a7d269bc 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -24,21 +24,21 @@ jobs: - name: Build the documentation run: make doc - # tests: - # name: "🧪 Tests" - # runs-on: [macos-latest] - # strategy: - # matrix: - # platform: [ios, tvos] - # steps: - # - name: Checkout code - # uses: actions/checkout@v4 + tests: + name: "🧪 Tests" + runs-on: [macos-latest] + strategy: + matrix: + platform: [ios, tvos] + steps: + - name: Checkout code + uses: actions/checkout@v4 - # - name: Authorize microphone access for simulator - # run: Scripts/bypass-simulator-trampoline.sh + - name: Authorize microphone access for simulator + run: Scripts/bypass-simulator-trampoline.sh - # - name: Run tests - # run: make test-${{ matrix.platform }} + - name: Run tests + run: make test-${{ matrix.platform }} archive-demos: name: "📦 Archives" From 5978f29626ee78a7d011ff8cf5658d468c8a95a6 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:40:30 +0100 Subject: [PATCH 40/68] Display user keychain --- Scripts/add-apple-certificate.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Scripts/add-apple-certificate.sh b/Scripts/add-apple-certificate.sh index 54515f8a..a2f1e252 100755 --- a/Scripts/add-apple-certificate.sh +++ b/Scripts/add-apple-certificate.sh @@ -26,4 +26,6 @@ security unlock-keychain -p "$keychain_password" "$keychain_path" # Import certificate security import "$apple_certificate" -k "$keychain_path" -P "$apple_certificate_password" -A -t cert -f pkcs12 # Authorize access to certificate private key -security set-key-partition-list -S apple-tool:,apple: -k "$keychain_password" "$keychain_path" \ No newline at end of file +security set-key-partition-list -S apple-tool:,apple: -k "$keychain_password" "$keychain_path" +# Set the default keychain +security list-keychain -d user -s "$keychain_path" \ No newline at end of file From 4fad4509f4e7cee099b3d5ed8f535f9eee66c03c Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:46:40 +0100 Subject: [PATCH 41/68] Remove log --- Scripts/add-apple-certificate.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/Scripts/add-apple-certificate.sh b/Scripts/add-apple-certificate.sh index a2f1e252..835d8e34 100755 --- a/Scripts/add-apple-certificate.sh +++ b/Scripts/add-apple-certificate.sh @@ -12,7 +12,6 @@ fi keychain_path="$root_dir/app-signing.keychain-db" -# Should we put a password? apple_certificate_password="6YXTQTG8JJ" apple_certificate="$root_dir/certificate.p12" From d9ea141fc4c407625876422347513f850a839c81 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:47:35 +0100 Subject: [PATCH 42/68] Remove simulator trampoline step --- .github/workflows/pull-request.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index a7d269bc..8421d411 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -34,9 +34,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Authorize microphone access for simulator - run: Scripts/bypass-simulator-trampoline.sh - - name: Run tests run: make test-${{ matrix.platform }} From 905a31e70b35a4bcbaa0ab938ad8d005b89a6206 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:48:16 +0100 Subject: [PATCH 43/68] Remove useless scripts --- Scripts/bypass-simulator-trampoline.sh | 9 ---- Scripts/checkout-configuration.sh | 58 -------------------------- Scripts/configure-environment.sh | 2 - 3 files changed, 69 deletions(-) delete mode 100755 Scripts/bypass-simulator-trampoline.sh delete mode 100755 Scripts/checkout-configuration.sh diff --git a/Scripts/bypass-simulator-trampoline.sh b/Scripts/bypass-simulator-trampoline.sh deleted file mode 100755 index b1680d78..00000000 --- a/Scripts/bypass-simulator-trampoline.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -# The manipulation of the following database is only possible if the System Integraty Protection (SIP) is disabled. -# SIP can only be updated (enabled/disabled) in recovery mode. -# Check the status of SIP: csrutil status - -## Simulator Trampoline (access to microphone) -## When using freshly cloned virtual machines, running our tests leads to a popup asking for access to the microphone. To avoid that popup, we can preventively write to the database. -sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db "INSERT OR REPLACE INTO 'main'.'access' ('service', 'client', 'client_type', 'auth_value', 'auth_reason', 'auth_version', 'csreq', 'policy_id', 'indirect_object_identifier_type', 'indirect_object_identifier', 'indirect_object_code_identity', 'flags', 'last_modified', 'pid', 'pid_version', 'boot_uuid', 'last_reminded') VALUES ('kTCCServiceMicrophone', 'com.apple.CoreSimulator.SimulatorTrampoline', '0', '2', '2', '1', X'fade0c00000000480000000100000006000000020000002b636f6d2e6170706c652e436f726553696d756c61746f722e53696d756c61746f725472616d706f6c696e650000000003', NULL, NULL, 'UNUSED', NULL, '0', strftime('%s','now'), NULL, NULL, 'UNUSED', '0');" \ No newline at end of file diff --git a/Scripts/checkout-configuration.sh b/Scripts/checkout-configuration.sh deleted file mode 100755 index fdc5a858..00000000 --- a/Scripts/checkout-configuration.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash - -# This script attempts to checkout a configuration repository URL and to switch to a specific commit. - -CONFIGURATION_REPOSITORY_URL=$1 -CONFIGURATION_COMMIT_SHA1=$2 -CONFIGURATION_FOLDER=$3 - -if [[ -z "$CONFIGURATION_REPOSITORY_URL" ]]; then - echo "A configuration repository URL must be provided." - exit 1 -fi - -if [[ -z "$CONFIGURATION_COMMIT_SHA1" ]]; then - echo "A configuration commit SHA1 must be provided." - exit 1 -fi - -if [[ -z "$CONFIGURATION_FOLDER" ]]; then - echo "A configuration destination folder must be provided." - exit 1 -fi - -if [[ ! -d "$CONFIGURATION_FOLDER" ]]; then - if git clone "$CONFIGURATION_REPOSITORY_URL" "$CONFIGURATION_FOLDER" &> /dev/null; then - echo "Private configuration details were successfully cloned under the '$CONFIGURATION_FOLDER' folder." - else - echo "Your GitHub account cannot access private project configuration details. Skipped." - exit 0 - fi -else - echo "A '$CONFIGURATION_FOLDER' folder is already available." -fi - -pushd "$CONFIGURATION_FOLDER" > /dev/null || exit - -if ! git status &> /dev/null; then - echo "The '$CONFIGURATION_FOLDER' folder is not a valid git repository." - exit 1 -fi - -if [[ $(git status --porcelain 2> /dev/null) ]]; then - echo "The repository '$CONFIGURATION_FOLDER' contains changes. Please commit or discard these changes and retry." - exit 1 -fi - -git fetch &> /dev/null - -if git checkout -q "$CONFIGURATION_COMMIT_SHA1" &> /dev/null; then - echo "The '$CONFIGURATION_FOLDER' repository has been switched to commit $CONFIGURATION_COMMIT_SHA1." -else - echo "The repository '$CONFIGURATION_FOLDER' could not be switched to commit $CONFIGURATION_COMMIT_SHA1. Does this commit exist?" - exit 1 -fi - -popd > /dev/null || exit - -ln -fs "$CONFIGURATION_FOLDER/.env" . diff --git a/Scripts/configure-environment.sh b/Scripts/configure-environment.sh index 167d164e..de87f2a2 100755 --- a/Scripts/configure-environment.sh +++ b/Scripts/configure-environment.sh @@ -1,7 +1,5 @@ #!/bin/bash -set -x - apple_api_key_b64="$1" apple_account_info_b64="$2" From fd6986e4a2448246989069f4b5c19054930d49c7 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:57:09 +0100 Subject: [PATCH 44/68] Add workflow for nightlies --- .github/workflows/nightlies.yaml | 32 ++++++++++++++++++++++++++++++++ Makefile | 8 ++++---- fastlane/Fastfile | 3 +-- 3 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/nightlies.yaml diff --git a/.github/workflows/nightlies.yaml b/.github/workflows/nightlies.yaml new file mode 100644 index 00000000..93a1da21 --- /dev/null +++ b/.github/workflows/nightlies.yaml @@ -0,0 +1,32 @@ +--- +name: Nightlies + +on: [push] + +jobs: + deliver-demo-nightlies: + name: "🌙 Nightlies" + runs-on: [macos-latest] + strategy: + matrix: + platform: [ios, tvos] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Add Apple certificate + run: | + Scripts/add-apple-certificate.sh \ + $RUNNER_TEMP \ + ${{ secrets.KEYCHAIN_PASSWORD }} \ + ${{ secrets.SRGSSR_APPLE_DEV_CERTIFICATE_B64 }} + + - name: Configure environment + run: | + Scripts/configure-environment.sh \ + ${{ secrets.APP_STORE_CONNECT_API_KEY_B64 }} \ + ${{ secrets.APPLE_ACCOUNT_INFO_B64 }} + + - name: Archive the demo + run: | + make deliver-demo-nightly-${{ matrix.platform }} diff --git a/Makefile b/Makefile index 249a7560..ad12fa6f 100644 --- a/Makefile +++ b/Makefile @@ -29,15 +29,15 @@ archive-demo-tvos: install-pkgx install-bundler @pkgx bundle exec fastlane archive_demo_tvos .PHONY: deliver-demo-nightly-ios -deliver-demo-nightly-ios: install-pkgx +deliver-demo-nightly-ios: install-pkgx install-bundler @echo "Delivering demo nightly build for iOS..." - @bundle exec fastlane deliver_demo_nightly_ios + @pkgx +magick +rsvg-convert bundle exec fastlane deliver_demo_nightly_ios @echo "... done.\n" .PHONY: deliver-demo-nightly-tvos -deliver-demo-nightly-tvos: install-pkgx +deliver-demo-nightly-tvos: install-pkgx install-bundler @echo "Delivering demo nightly build for tvOS..." - @bundle exec fastlane deliver_demo_nightly_tvos + @pkgx +magick +rsvg-convert bundle exec fastlane deliver_demo_nightly_tvos @echo "... done.\n" .PHONY: deliver-demo-release-ios diff --git a/fastlane/Fastfile b/fastlane/Fastfile index b55ee902..34fe358a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -176,8 +176,7 @@ def deliver_demo_nightly(platform_id) add_version_badge(platform_id, last_git_tag, build_number, 'orange') build_and_sign_app(platform_id, :nightly) reset_git_repo(skip_clean: true) - upload_app_to_testflight - distribute_app_to_testers(platform_id, :nightly, build_number) + login_to_app_store_connect end def deliver_demo_release(platform_id) From e4803ee615ce87eedc0e0ce1be9c4bb7bada649f Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:56:53 +0100 Subject: [PATCH 45/68] Try using GitHub cache --- .github/workflows/nightlies.yaml | 2 +- .github/workflows/pull-request.yml | 26 ++++++++++++++++++++++++++ fastlane/Fastfile | 8 ++++++-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nightlies.yaml b/.github/workflows/nightlies.yaml index 93a1da21..dee50816 100644 --- a/.github/workflows/nightlies.yaml +++ b/.github/workflows/nightlies.yaml @@ -1,7 +1,7 @@ --- name: Nightlies -on: [push] +#on: [push] jobs: deliver-demo-nightlies: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 8421d411..b8047f3f 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -34,8 +34,20 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Cache packages + uses: actions/cache@v3 + id: packages-cache + with: + path: packages_cache + key: ${{ runner.os }}-packages-${{ hashFiles('Package.resolved') }} + restore-keys: | + ${{ runner.os }}-packages- + - name: Run tests run: make test-${{ matrix.platform }} + env: + HAS_PACKAGES_CACHE_HIT: > + ${{ steps.packages-cache.outputs.cache-hit == 'true' }} archive-demos: name: "📦 Archives" @@ -60,6 +72,20 @@ jobs: ${{ secrets.APP_STORE_CONNECT_API_KEY_B64 }} \ ${{ secrets.APPLE_ACCOUNT_INFO_B64 }} + - name: Cache packages + uses: actions/cache@v3 + id: packages-demo-cache + with: + path: packages_cache + key: | + ${{ runner.os }}-demo-packages-\ + ${{ hashFiles('Demo/**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-demo-packages- + - name: Archive the demo run: | make archive-demo-${{ matrix.platform }} + env: + HAS_PACKAGES_CACHE_HIT: | + ${{ steps.packages-demo-cache.outputs.cache-hit == 'true' }} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 34fe358a..a62b8d60 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -117,7 +117,9 @@ def build_and_sign_app(platform_id, configuration_id) export_team_id: ENV.fetch('TEAM_ID'), output_directory: 'Binaries', xcargs: "-authenticationKeyIssuerID #{ENV.fetch('KEY_ISSUER_ID')} -authenticationKeyID #{ENV.fetch('KEY_ID')} " \ - "-authenticationKeyPath #{api_key_filepath} -allowProvisioningUpdates" + "-authenticationKeyPath #{api_key_filepath} -allowProvisioningUpdates", + cloned_source_packages_path: 'Packages', + skip_package_dependencies_resolution: ENV.fetch('HAS_PACKAGES_CACHE_HIT') ) end @@ -197,7 +199,9 @@ def run_package_tests(platform_id, scheme_name) number_of_retries: 3, clean: true, fail_build: false, - xcargs: '-testLanguage en -testRegion en_US' + xcargs: '-testLanguage en -testRegion en_US', + cloned_source_packages_path: 'Packages', + skip_package_dependencies_resolution: ENV.fetch('HAS_PACKAGES_CACHE_HIT') ) trainer( path: 'fastlane/test_output', From cdc1742597fa402a5bfc210289288c63f82cc4cf Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:33:28 +0100 Subject: [PATCH 46/68] Use a default value --- .github/workflows/pull-request.yml | 13 ++++++++++--- fastlane/Fastfile | 8 ++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index b8047f3f..f9cdce5b 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -38,13 +38,17 @@ jobs: uses: actions/cache@v3 id: packages-cache with: - path: packages_cache + path: ~/Packages key: ${{ runner.os }}-packages-${{ hashFiles('Package.resolved') }} restore-keys: | ${{ runner.os }}-packages- - name: Run tests - run: make test-${{ matrix.platform }} + run: | + echo "==================" + pwd + echo "==================" + make test-${{ matrix.platform }} env: HAS_PACKAGES_CACHE_HIT: > ${{ steps.packages-cache.outputs.cache-hit == 'true' }} @@ -76,7 +80,7 @@ jobs: uses: actions/cache@v3 id: packages-demo-cache with: - path: packages_cache + path: ~/Packages key: | ${{ runner.os }}-demo-packages-\ ${{ hashFiles('Demo/**/Package.resolved') }} @@ -85,6 +89,9 @@ jobs: - name: Archive the demo run: | + echo "==================" + pwd + echo "==================" make archive-demo-${{ matrix.platform }} env: HAS_PACKAGES_CACHE_HIT: | diff --git a/fastlane/Fastfile b/fastlane/Fastfile index a62b8d60..3a09002e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -118,8 +118,8 @@ def build_and_sign_app(platform_id, configuration_id) output_directory: 'Binaries', xcargs: "-authenticationKeyIssuerID #{ENV.fetch('KEY_ISSUER_ID')} -authenticationKeyID #{ENV.fetch('KEY_ID')} " \ "-authenticationKeyPath #{api_key_filepath} -allowProvisioningUpdates", - cloned_source_packages_path: 'Packages', - skip_package_dependencies_resolution: ENV.fetch('HAS_PACKAGES_CACHE_HIT') + cloned_source_packages_path: '~/Packages', + skip_package_dependencies_resolution: ENV['HAS_PACKAGES_CACHE_HIT'] == 'true' ) end @@ -200,8 +200,8 @@ def run_package_tests(platform_id, scheme_name) clean: true, fail_build: false, xcargs: '-testLanguage en -testRegion en_US', - cloned_source_packages_path: 'Packages', - skip_package_dependencies_resolution: ENV.fetch('HAS_PACKAGES_CACHE_HIT') + cloned_source_packages_path: '~/Packages', + skip_package_dependencies_resolution: ENV['HAS_PACKAGES_CACHE_HIT'] == 'true' ) trainer( path: 'fastlane/test_output', From bf3afd6e1947bb5a9530a9095e139539d2af6d71 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 08:44:45 +0100 Subject: [PATCH 47/68] Update cache paths --- .github/workflows/pull-request.yml | 10 ++-------- fastlane/Fastfile | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index f9cdce5b..d6ea7047 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -45,12 +45,9 @@ jobs: - name: Run tests run: | - echo "==================" - pwd - echo "==================" make test-${{ matrix.platform }} env: - HAS_PACKAGES_CACHE_HIT: > + HAS_PACKAGES_CACHE_HIT: | ${{ steps.packages-cache.outputs.cache-hit == 'true' }} archive-demos: @@ -80,7 +77,7 @@ jobs: uses: actions/cache@v3 id: packages-demo-cache with: - path: ~/Packages + path: ~/Demo/Packages key: | ${{ runner.os }}-demo-packages-\ ${{ hashFiles('Demo/**/Package.resolved') }} @@ -89,9 +86,6 @@ jobs: - name: Archive the demo run: | - echo "==================" - pwd - echo "==================" make archive-demo-${{ matrix.platform }} env: HAS_PACKAGES_CACHE_HIT: | diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 3a09002e..6f606f02 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -118,7 +118,7 @@ def build_and_sign_app(platform_id, configuration_id) output_directory: 'Binaries', xcargs: "-authenticationKeyIssuerID #{ENV.fetch('KEY_ISSUER_ID')} -authenticationKeyID #{ENV.fetch('KEY_ID')} " \ "-authenticationKeyPath #{api_key_filepath} -allowProvisioningUpdates", - cloned_source_packages_path: '~/Packages', + cloned_source_packages_path: '~/Demo/Packages', skip_package_dependencies_resolution: ENV['HAS_PACKAGES_CACHE_HIT'] == 'true' ) end From bbf214cae9fe546ae3d42db7e735ef4a56364c53 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 09:01:39 +0100 Subject: [PATCH 48/68] Inline steps --- .github/workflows/pull-request.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index d6ea7047..eddde88d 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -40,8 +40,7 @@ jobs: with: path: ~/Packages key: ${{ runner.os }}-packages-${{ hashFiles('Package.resolved') }} - restore-keys: | - ${{ runner.os }}-packages- + restore-keys: ${{ runner.os }}-packages- - name: Run tests run: | @@ -78,11 +77,8 @@ jobs: id: packages-demo-cache with: path: ~/Demo/Packages - key: | - ${{ runner.os }}-demo-packages-\ - ${{ hashFiles('Demo/**/Package.resolved') }} - restore-keys: | - ${{ runner.os }}-demo-packages- + key: ${{ runner.os }}-demo-packages-${{ hashFiles('Demo/**/Package.resolved') }} + restore-keys: ${{ runner.os }}-demo-packages- - name: Archive the demo run: | From a4e827a33e315b113f0d19344318254eafffd3d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20De=CC=81fago?= Date: Wed, 27 Nov 2024 09:11:40 +0100 Subject: [PATCH 49/68] Use Commanders Act binaries delivered by SRG SSR mirror --- Package.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index bf92bd00..f0985a5c 100644 --- a/Package.swift +++ b/Package.swift @@ -36,7 +36,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/comScore/Comscore-Swift-Package-Manager.git", .upToNextMinor(from: "6.14.0")), - .package(url: "https://github.com/CommandersAct/iOSV5.git", .upToNextMinor(from: "5.4.0")), + .package(url: "https://github.com/SRGSSR/commanders-act-apple.git", .upToNextMinor(from: "5.4.0")), .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/krzysztofzablocki/Difference.git", exact: "1.0.1"), .package(url: "https://github.com/Quick/Nimble.git", .upToNextMajor(from: "13.0.0")) @@ -47,8 +47,8 @@ let package = Package( dependencies: [ .target(name: "PillarboxPlayer"), .product(name: "ComScore", package: "Comscore-Swift-Package-Manager"), - .product(name: "TCCore", package: "iOSV5"), - .product(name: "TCServerSide", package: "iOSV5") + .product(name: "TCCore", package: "commanders-act-apple"), + .product(name: "TCServerSide", package: "commanders-act-apple") ], path: "Sources/Analytics", resources: [ From 6ef310e02596ac0997cb155917e2395c761123a4 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 09:17:42 +0100 Subject: [PATCH 50/68] Use recursive wild card for tests cache --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index eddde88d..90fca061 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -39,7 +39,7 @@ jobs: id: packages-cache with: path: ~/Packages - key: ${{ runner.os }}-packages-${{ hashFiles('Package.resolved') }} + key: ${{ runner.os }}-packages-${{ hashFiles('**/Package.resolved') }} restore-keys: ${{ runner.os }}-packages- - name: Run tests From 17615d154ae9ea65a1771fce34251e5d79d1ce72 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 09:37:53 +0100 Subject: [PATCH 51/68] Use another path for cache --- .github/workflows/pull-request.yml | 2 +- fastlane/Fastfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 90fca061..61825c40 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -38,7 +38,7 @@ jobs: uses: actions/cache@v3 id: packages-cache with: - path: ~/Packages + path: ~/Library/Caches/org.swift.swiftpm key: ${{ runner.os }}-packages-${{ hashFiles('**/Package.resolved') }} restore-keys: ${{ runner.os }}-packages- diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 6f606f02..f6023785 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -200,7 +200,7 @@ def run_package_tests(platform_id, scheme_name) clean: true, fail_build: false, xcargs: '-testLanguage en -testRegion en_US', - cloned_source_packages_path: '~/Packages', + cloned_source_packages_path: '~/Library/Caches/org.swift.swiftpm', skip_package_dependencies_resolution: ENV['HAS_PACKAGES_CACHE_HIT'] == 'true' ) trainer( From e6f93862ecb37813d6542f61b334211cd9aa5e9b Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 10:03:41 +0100 Subject: [PATCH 52/68] Fix quality --- .github/workflows/pull-request.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 61825c40..e6c5a773 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -77,7 +77,9 @@ jobs: id: packages-demo-cache with: path: ~/Demo/Packages - key: ${{ runner.os }}-demo-packages-${{ hashFiles('Demo/**/Package.resolved') }} + key: | + ${{ runner.os }}-demo-packages-\ + ${{ hashFiles('Demo/**/Package.resolved') }} restore-keys: ${{ runner.os }}-demo-packages- - name: Archive the demo From 2ac51ff0afa160bf0d23943dde4ebee54f01a9de Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 10:15:08 +0100 Subject: [PATCH 53/68] Remove cache management --- .github/workflows/pull-request.yml | 18 ------------------ fastlane/Fastfile | 8 ++------ 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e6c5a773..9bccf524 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -34,14 +34,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Cache packages - uses: actions/cache@v3 - id: packages-cache - with: - path: ~/Library/Caches/org.swift.swiftpm - key: ${{ runner.os }}-packages-${{ hashFiles('**/Package.resolved') }} - restore-keys: ${{ runner.os }}-packages- - - name: Run tests run: | make test-${{ matrix.platform }} @@ -72,16 +64,6 @@ jobs: ${{ secrets.APP_STORE_CONNECT_API_KEY_B64 }} \ ${{ secrets.APPLE_ACCOUNT_INFO_B64 }} - - name: Cache packages - uses: actions/cache@v3 - id: packages-demo-cache - with: - path: ~/Demo/Packages - key: | - ${{ runner.os }}-demo-packages-\ - ${{ hashFiles('Demo/**/Package.resolved') }} - restore-keys: ${{ runner.os }}-demo-packages- - - name: Archive the demo run: | make archive-demo-${{ matrix.platform }} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index f6023785..34fe358a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -117,9 +117,7 @@ def build_and_sign_app(platform_id, configuration_id) export_team_id: ENV.fetch('TEAM_ID'), output_directory: 'Binaries', xcargs: "-authenticationKeyIssuerID #{ENV.fetch('KEY_ISSUER_ID')} -authenticationKeyID #{ENV.fetch('KEY_ID')} " \ - "-authenticationKeyPath #{api_key_filepath} -allowProvisioningUpdates", - cloned_source_packages_path: '~/Demo/Packages', - skip_package_dependencies_resolution: ENV['HAS_PACKAGES_CACHE_HIT'] == 'true' + "-authenticationKeyPath #{api_key_filepath} -allowProvisioningUpdates" ) end @@ -199,9 +197,7 @@ def run_package_tests(platform_id, scheme_name) number_of_retries: 3, clean: true, fail_build: false, - xcargs: '-testLanguage en -testRegion en_US', - cloned_source_packages_path: '~/Library/Caches/org.swift.swiftpm', - skip_package_dependencies_resolution: ENV['HAS_PACKAGES_CACHE_HIT'] == 'true' + xcargs: '-testLanguage en -testRegion en_US' ) trainer( path: 'fastlane/test_output', From 5235e3d4f52dbddffdd0f1d8c301805a746678cb Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 10:34:37 +0100 Subject: [PATCH 54/68] Try irgaly/xcode-cache@v1 --- .github/workflows/pull-request.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 9bccf524..09dbfe8a 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -34,6 +34,14 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Cache packages + uses: irgaly/xcode-cache@v1 + with: + key: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.sha }} + restore-keys: xcode-cache-deriveddata-${{ github.workflow }}- + swiftpm-cache-key: pillarbox-spm-${{ hashFiles('Package.resolved') }} + swiftpm-cache-restore-keys: pillarbox-spm- + - name: Run tests run: | make test-${{ matrix.platform }} @@ -64,6 +72,14 @@ jobs: ${{ secrets.APP_STORE_CONNECT_API_KEY_B64 }} \ ${{ secrets.APPLE_ACCOUNT_INFO_B64 }} + - name: Cache packages + uses: irgaly/xcode-cache@v1 + with: + key: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.sha }} + restore-keys: xcode-cache-deriveddata-${{ github.workflow }}- + swiftpm-cache-key: pillarbox-demo-spm-${{ hashFiles('Demo/**/Package.resolved') }} + swiftpm-cache-restore-keys: pillarbox-demo-spm- + - name: Archive the demo run: | make archive-demo-${{ matrix.platform }} From c9f3a8bd027906ca70ad3b9ee2ac54aacf601107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20De=CC=81fago?= Date: Wed, 27 Nov 2024 11:01:35 +0100 Subject: [PATCH 55/68] Remove cleanup --- fastlane/Fastfile | 1 - 1 file changed, 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 34fe358a..ec1fe330 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -195,7 +195,6 @@ def run_package_tests(platform_id, scheme_name) package_path: '.', result_bundle: true, number_of_retries: 3, - clean: true, fail_build: false, xcargs: '-testLanguage en -testRegion en_US' ) From 0b5cae0384a58ad0d434b698fd853b8a3a560237 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 11:10:01 +0100 Subject: [PATCH 56/68] Revert "Remove cleanup" This reverts commit 81ac46e203b20347924d833f2770301737189e29. --- fastlane/Fastfile | 1 + 1 file changed, 1 insertion(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index ec1fe330..34fe358a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -195,6 +195,7 @@ def run_package_tests(platform_id, scheme_name) package_path: '.', result_bundle: true, number_of_retries: 3, + clean: true, fail_build: false, xcargs: '-testLanguage en -testRegion en_US' ) From 2d306c53ed2780a62e0e6c8ff467dad255ee9560 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 11:11:47 +0100 Subject: [PATCH 57/68] Remove irgaly/xcode-cache@v1 --- .github/workflows/pull-request.yml | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 09dbfe8a..c4b1f979 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -34,20 +34,8 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Cache packages - uses: irgaly/xcode-cache@v1 - with: - key: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.sha }} - restore-keys: xcode-cache-deriveddata-${{ github.workflow }}- - swiftpm-cache-key: pillarbox-spm-${{ hashFiles('Package.resolved') }} - swiftpm-cache-restore-keys: pillarbox-spm- - - name: Run tests - run: | - make test-${{ matrix.platform }} - env: - HAS_PACKAGES_CACHE_HIT: | - ${{ steps.packages-cache.outputs.cache-hit == 'true' }} + run: make test-${{ matrix.platform }} archive-demos: name: "📦 Archives" @@ -72,17 +60,5 @@ jobs: ${{ secrets.APP_STORE_CONNECT_API_KEY_B64 }} \ ${{ secrets.APPLE_ACCOUNT_INFO_B64 }} - - name: Cache packages - uses: irgaly/xcode-cache@v1 - with: - key: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.sha }} - restore-keys: xcode-cache-deriveddata-${{ github.workflow }}- - swiftpm-cache-key: pillarbox-demo-spm-${{ hashFiles('Demo/**/Package.resolved') }} - swiftpm-cache-restore-keys: pillarbox-demo-spm- - - name: Archive the demo - run: | - make archive-demo-${{ matrix.platform }} - env: - HAS_PACKAGES_CACHE_HIT: | - ${{ steps.packages-demo-cache.outputs.cache-hit == 'true' }} + run: make archive-demo-${{ matrix.platform }} From 7dbb2cc378fee6fc4edb5a1128f8ace106c59f62 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 11:22:08 +0100 Subject: [PATCH 58/68] Split configurations --- .github/workflows/pull-request.yml | 4 +++- Makefile | 20 ++++++++++++++------ fastlane/Fastfile | 28 +++++++++++++++++++++------- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index c4b1f979..e072f046 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -43,6 +43,7 @@ jobs: strategy: matrix: platform: [ios, tvos] + configuration: [nightly, release] steps: - name: Checkout code uses: actions/checkout@v4 @@ -61,4 +62,5 @@ jobs: ${{ secrets.APPLE_ACCOUNT_INFO_B64 }} - name: Archive the demo - run: make archive-demo-${{ matrix.platform }} + run: | + make archive-demo-${{ matrix.configuration }}-${{ matrix.platform }} diff --git a/Makefile b/Makefile index ad12fa6f..b86713ce 100644 --- a/Makefile +++ b/Makefile @@ -20,13 +20,21 @@ install-bundler: fastlane: install-pkgx install-bundler @pkgx bundle exec fastlane -.PHONY: archive-demo-ios -archive-demo-ios: install-pkgx install-bundler - @pkgx bundle exec fastlane archive_demo_ios +.PHONY: archive-demo-nightly-ios +archive-demo-nightly-ios: install-pkgx install-bundler + @pkgx bundle exec fastlane archive_demo_nightly_ios -.PHONY: archive-demo-tvos -archive-demo-tvos: install-pkgx install-bundler - @pkgx bundle exec fastlane archive_demo_tvos +.PHONY: archive-demo-nightly-tvos +archive-demo-nightly-tvos: install-pkgx install-bundler + @pkgx bundle exec fastlane archive_demo_nightly_tvos + +.PHONY: archive-demo-release-ios +archive-demo-release-ios: install-pkgx install-bundler + @pkgx bundle exec fastlane archive_demo_release_ios + +.PHONY: archive-demo-release-tvos +archive-demo-release-tvos: install-pkgx install-bundler + @pkgx bundle exec fastlane archive_demo_release_tvos .PHONY: deliver-demo-nightly-ios deliver-demo-nightly-ios: install-pkgx install-bundler diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 34fe358a..d6e9d1b4 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -164,9 +164,13 @@ rescue StandardError => e UI.important('TestFlight external delivery was skipped because a build is already in review') end -def archive_demo(platform_id) +def archive_demo_nightly(platform_id) ensure_configuration_availability build_and_sign_app(platform_id, :nightly) +end + +def archive_demo_release(platform_id) + ensure_configuration_availability build_and_sign_app(platform_id, :release) end @@ -223,14 +227,24 @@ platform :ios do reset_git_repo(skip_clean: true) end - desc 'Archive the iOS demo app' - lane :archive_demo_ios do - archive_demo(:ios) + desc 'Archive the iOS nightly demo app' + lane :archive_demo_nightly_ios do + archive_demo_nightly(:ios) + end + + desc 'Archive the iOS release demo app' + lane :archive_demo_release_ios do + archive_demo_release(:ios) + end + + desc 'Archive the tvOS nightly demo app' + lane :archive_demo_nightly_tvos do + archive_demo_nightly(:tvos) end - desc 'Archive the tvOS demo app' - lane :archive_demo_tvos do - archive_demo(:tvos) + desc 'Archive the tvOS release demo app' + lane :archive_demo_release_tvos do + archive_demo_release(:tvos) end desc 'Deliver an iOS demo app nightly build' From 9947447f3156618505cb7698aab3003749e2e044 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 11:51:16 +0100 Subject: [PATCH 59/68] Revert "Remove tests" This reverts commit 8855d1beac76b9f188cdb2e98e5602bcf7ad44b8. --- .../xcshareddata/xcschemes/Pillarbox.xcscheme | 72 + Package.resolved | 18 +- Package.swift | 36 + .../ComScore/ComScoreHitExpectation.swift | 52 + .../ComScore/ComScorePageViewTests.swift | 332 +++++ .../ComScore/ComScoreTestCase.swift | 78 + .../ComScoreTrackerDvrPropertiesTests.swift | 93 ++ .../ComScoreTrackerMetadataTests.swift | 60 + .../ComScoreTrackerPlaybackSpeedTests.swift | 31 + .../ComScore/ComScoreTrackerRateTests.swift | 65 + .../ComScore/ComScoreTrackerSeekTests.swift | 60 + .../ComScore/ComScoreTrackerTests.swift | 237 ++++ .../CommandersActEventTests.swift | 100 ++ .../CommandersActHeartbeatTests.swift | 88 ++ .../CommandersActHitExpectation.swift | 68 + .../CommandersActPageViewTests.swift | 183 +++ .../CommandersAct/CommandersActTestCase.swift | 74 + ...mmandersActTrackerDvrPropertiesTests.swift | 126 ++ .../CommandersActTrackerMetadataTests.swift | 143 ++ .../CommandersActTrackerPositionTests.swift | 139 ++ .../CommandersActTrackerSeekTests.swift | 83 ++ .../CommandersActTrackerTests.swift | 208 +++ .../Extensions/Dictionary.swift | 9 + Tests/AnalyticsTests/TestCase.swift | 45 + Tests/CircumspectTests/ComparatorTests.swift | 20 + Tests/CircumspectTests/Counter.swift | 20 + .../ExpectAtLeastPublishedTests.swift | 64 + .../ExpectNothingPublishedTests.swift | 24 + .../ExpectNotificationsTests.swift | 34 + .../ExpectOnlyPublishedTests.swift | 51 + .../Expectations/ExpectPublishedTests.swift | 56 + .../Expectations/ExpectResultTests.swift | 24 + .../Expectations/ExpectValueTests.swift | 40 + .../ObservableObjectTests.swift | 70 + Tests/CircumspectTests/PublishersTests.swift | 114 ++ Tests/CircumspectTests/SimilarityTests.swift | 55 + .../CircumspectTests/TimeIntervalTests.swift | 20 + Tests/CircumspectTests/Tools.swift | 22 + .../AkamaiURLCodingTests.swift | 82 ++ .../CoreBusinessTests/DataProviderTests.swift | 25 + Tests/CoreBusinessTests/ErrorsTests.swift | 20 + .../HTTPURLResponseTests.swift | 28 + .../MediaMetadataTests.swift | 76 + Tests/CoreBusinessTests/Mock.swift | 27 + Tests/CoreBusinessTests/PublishersTests.swift | 20 + .../Resources.xcassets/Contents.json | 6 + .../Contents.json | 12 + .../urn_rts_audio_13598743.json | 752 ++++++++++ .../Contents.json | 12 + .../MediaComposition_drm.json | 315 +++++ .../Contents.json | 12 + .../urn_rts_audio_3262320.json | 143 ++ .../Contents.json | 12 + .../urn_rts_video_13360574.json | 179 +++ .../Contents.json | 12 + .../urn_rts_video_14827796.json | 1258 +++++++++++++++++ .../Contents.json | 12 + .../urn_rts_video_13360574.json | 210 +++ .../Contents.json | 13 + .../urn_rts_video_13763072.json | 690 +++++++++ .../CoreTests/AccumulatePublisherTests.swift | 186 +++ Tests/CoreTests/ArrayTests.swift | 24 + Tests/CoreTests/CombineLatestTests.swift | 64 + Tests/CoreTests/ComparableTests.swift | 20 + Tests/CoreTests/DemandBufferTests.swift | 81 ++ Tests/CoreTests/DispatchPublisherTests.swift | 93 ++ Tests/CoreTests/LimitedBufferTests.swift | 31 + Tests/CoreTests/MeasurePublisherTests.swift | 36 + ...tificationPublisherDeallocationTests.swift | 43 + .../NotificationPublisherTests.swift | 47 + .../PublishAndRepeatOnOutputFromTests.swift | 38 + .../CoreTests/PublishOnOutputFromTests.swift | 38 + .../RangeReplaceableCollectionTests.swift | 51 + Tests/CoreTests/ReplaySubjectTests.swift | 170 +++ Tests/CoreTests/SlicePublisherTests.swift | 27 + Tests/CoreTests/StopwatchTests.swift | 70 + Tests/CoreTests/TimeTests.swift | 78 + Tests/CoreTests/Tools.swift | 21 + Tests/CoreTests/TriggerTests.swift | 47 + Tests/CoreTests/WaitPublisherTests.swift | 25 + .../CoreTests/WeakCapturePublisherTests.swift | 39 + .../WithPreviousPublisherTests.swift | 45 + .../MetricHitExpectation.swift | 67 + Tests/MonitoringTests/MetricPayload.swift | 13 + Tests/MonitoringTests/MetricsTracker.swift | 27 + .../MonitoringTests/MetricsTrackerTests.swift | 225 +++ .../MonitoringTests/MonitoringTestCase.swift | 75 + .../TrackingSessionTests.swift | 41 + .../AVPlayerItemRepeatAllUpdateTests.swift | 189 +++ .../AVPlayerItemRepeatOffUpdateTests.swift | 189 +++ .../AVPlayerItemRepeatOneUpdateTests.swift | 189 +++ .../AVPlayer/AVPlayerItemTests.swift | 101 ++ .../PlayerTests/AVPlayer/AVPlayerTests.swift | 50 + .../Asset/AssetCreationTests.swift | 29 + .../PlayerTests/Asset/AssetMetadataMock.swift | 23 + .../PlayerTests/Asset/ResourceItemTests.swift | 41 + .../AVAudioSessionNotificationTests.swift | 71 + .../PlayerTests/Extensions/AVPlayerItem.swift | 13 + .../PlayerTests/Extensions/AssetContent.swift | 16 + .../PlayerTests/Extensions/MetricEvent.swift | 16 + Tests/PlayerTests/Extensions/Player.swift | 15 + Tests/PlayerTests/Extensions/UUID.swift | 21 + .../AVMediaSelectionGroupTests.swift | 58 + .../AVMediaSelectionOptionTests.swift | 30 + .../MediaSelection/MediaSelectionTests.swift | 297 ++++ ...erredLanguagesForMediaSelectionTests.swift | 144 ++ .../Metrics/AccessLogEventTests.swift | 59 + .../Metrics/MetricsCollectorEventsTests.swift | 43 + .../Metrics/MetricsCollectorTests.swift | 78 + .../Metrics/MetricsStateTests.swift | 115 ++ .../Player/BlockedTimeRangeTests.swift | 80 ++ Tests/PlayerTests/Player/ErrorTests.swift | 38 +- .../Player/PlaybackSpeedUpdateTests.swift | 39 + Tests/PlayerTests/Player/PlaybackTests.swift | 48 + Tests/PlayerTests/Player/PlayerTests.swift | 76 + Tests/PlayerTests/Player/QueueTests.swift | 184 +++ .../Player/ReplayChecksTests.swift | 76 + Tests/PlayerTests/Player/ReplayTests.swift | 77 + .../PlayerTests/Player/SeekChecksTests.swift | 54 + Tests/PlayerTests/Player/SeekTests.swift | 120 ++ Tests/PlayerTests/Player/SpeedTests.swift | 205 +++ .../Player/TextStyleRulesTests.swift | 48 + .../PlayerItemAssetPublisherTests.swift | 51 + .../PlayerItem/PlayerItemTests.swift | 129 ++ .../Playlist/CurrentItemTests.swift | 191 +++ .../Playlist/ItemInsertionAfterTests.swift | 88 ++ .../Playlist/ItemInsertionBeforeTests.swift | 88 ++ .../Playlist/ItemMoveAfterTests.swift | 147 ++ .../Playlist/ItemMoveBeforeTests.swift | 147 ++ .../ItemNavigationBackwardChecksTests.swift | 38 + .../ItemNavigationBackwardTests.swift | 48 + .../ItemNavigationForwardChecksTests.swift | 38 + .../Playlist/ItemNavigationForwardTests.swift | 63 + .../Playlist/ItemRemovalTests.swift | 62 + Tests/PlayerTests/Playlist/ItemsTests.swift | 70 + .../Playlist/ItemsUpdateTests.swift | 49 + .../NavigationBackwardChecksTests.swift | 111 ++ .../Playlist/NavigationBackwardTests.swift | 137 ++ .../NavigationForwardChecksTests.swift | 72 + .../Playlist/NavigationForwardTests.swift | 80 ++ .../NavigationSmartBackwardChecksTests.swift | 107 ++ .../NavigationSmartBackwardTests.swift | 118 ++ .../NavigationSmartForwardChecksTests.swift | 72 + .../NavigationSmartForwardTests.swift | 80 ++ .../Playlist/RepeatModeTests.swift | 53 + .../ProgressTrackerPlaybackStateTests.swift | 65 + ...ressTrackerProgressAvailabilityTests.swift | 132 ++ .../ProgressTrackerProgressTests.swift | 157 ++ .../ProgressTrackerRangeTests.swift | 132 ++ .../ProgressTrackerSeekBehaviorTests.swift | 68 + .../ProgressTrackerTimeTests.swift | 153 ++ .../ProgressTrackerValueTests.swift | 56 + ...etMediaSelectionGroupsPublisherTests.swift | 37 + ...hronousKeyValueLoadingPublisherTests.swift | 62 + .../AVPlayerBoundaryTimePublisherTests.swift | 74 + .../AVPlayerItemErrorPublisherTests.swift | 41 + ...VPlayerItemMetricEventPublisherTests.swift | 54 + .../AVPlayerPeriodicTimePublisherTests.swift | 60 + .../Publishers/MetadataPublisherTests.swift | 94 ++ .../NowPlayingInfoPublisherTests.swift | 47 + .../PeriodicMetricsPublisherTests.swift | 72 + .../PlayerItemMetricEventPublisherTests.swift | 28 + .../Publishers/PlayerPublisherTests.swift | 159 +++ .../QueuePlayer/QueuePlayerItemsTests.swift | 88 ++ .../QueuePlayer/QueuePlayerSeekTests.swift | 286 ++++ .../QueuePlayerSeekTimePublisherTests.swift | 94 ++ .../QueuePlayerSmoothSeekTests.swift | 173 +++ Tests/PlayerTests/Resources/invalid.jpg | 0 Tests/PlayerTests/Resources/pixel.jpg | Bin 0 -> 373 bytes .../Skips/SkipBackwardChecksTests.swift | 35 + .../PlayerTests/Skips/SkipBackwardTests.swift | 79 ++ .../Skips/SkipForwardChecksTests.swift | 35 + .../PlayerTests/Skips/SkipForwardTests.swift | 126 ++ .../Skips/SkipToDefaultChecksTests.swift | 55 + .../Skips/SkipToDefaultTests.swift | 91 ++ .../Tools/AVMediaSelectionOptionMock.swift | 32 + .../Tools/ContentKeySessionDelegateMock.swift | 11 + .../Tools/LanguageIdentifiable.swift | 29 + Tests/PlayerTests/Tools/Matchers.swift | 19 + .../Tools/MediaAccessibilityDisplayType.swift | 25 + Tests/PlayerTests/Tools/PlayerItem.swift | 79 ++ .../Tools/PlayerItemTrackerMock.swift | 69 + .../Tools/ResourceLoaderDelegateMock.swift | 24 + Tests/PlayerTests/Tools/Similarity.swift | 78 + Tests/PlayerTests/Tools/TestCase.swift | 22 + Tests/PlayerTests/Tools/Tools.swift | 13 + .../PlayerTests/Tools/TrackerUpdateMock.swift | 55 + .../PlayerItemTrackerLifeCycleTests.swift | 98 ++ ...layerItemTrackerMetricPublisherTests.swift | 54 + .../PlayerItemTrackerSessionTests.swift | 31 + .../PlayerItemTrackerUpdateTests.swift | 58 + .../Tracking/PlayerTrackingTests.swift | 133 ++ .../PlayerTests/Types/CMTimeRangeTests.swift | 48 + Tests/PlayerTests/Types/CMTimeTests.swift | 103 ++ Tests/PlayerTests/Types/ErrorsTests.swift | 49 + .../PlayerTests/Types/ImageSourceTests.swift | 76 + Tests/PlayerTests/Types/ItemErrorTests.swift | 43 + .../Types/PlaybackSpeedTests.swift | 35 + .../Types/PlaybackStateTests.swift | 20 + .../Types/PlayerConfigurationTests.swift | 42 + .../PlayerTests/Types/PlayerLimitsTests.swift | 79 ++ .../Types/PlayerMetadataTests.swift | 66 + Tests/PlayerTests/Types/PositionTests.swift | 47 + Tests/PlayerTests/Types/StreamTypeTests.swift | 23 + .../Types/TimePropertiesTests.swift | 69 + .../VisibilityTrackerTests.swift | 184 +++ 206 files changed, 17777 insertions(+), 13 deletions(-) create mode 100644 Tests/AnalyticsTests/ComScore/ComScoreHitExpectation.swift create mode 100644 Tests/AnalyticsTests/ComScore/ComScorePageViewTests.swift create mode 100644 Tests/AnalyticsTests/ComScore/ComScoreTestCase.swift create mode 100644 Tests/AnalyticsTests/ComScore/ComScoreTrackerDvrPropertiesTests.swift create mode 100644 Tests/AnalyticsTests/ComScore/ComScoreTrackerMetadataTests.swift create mode 100644 Tests/AnalyticsTests/ComScore/ComScoreTrackerPlaybackSpeedTests.swift create mode 100644 Tests/AnalyticsTests/ComScore/ComScoreTrackerRateTests.swift create mode 100644 Tests/AnalyticsTests/ComScore/ComScoreTrackerSeekTests.swift create mode 100644 Tests/AnalyticsTests/ComScore/ComScoreTrackerTests.swift create mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActEventTests.swift create mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActHeartbeatTests.swift create mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActHitExpectation.swift create mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActPageViewTests.swift create mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActTestCase.swift create mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActTrackerDvrPropertiesTests.swift create mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActTrackerMetadataTests.swift create mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActTrackerPositionTests.swift create mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActTrackerSeekTests.swift create mode 100644 Tests/AnalyticsTests/CommandersAct/CommandersActTrackerTests.swift create mode 100644 Tests/AnalyticsTests/Extensions/Dictionary.swift create mode 100644 Tests/AnalyticsTests/TestCase.swift create mode 100644 Tests/CircumspectTests/ComparatorTests.swift create mode 100644 Tests/CircumspectTests/Counter.swift create mode 100644 Tests/CircumspectTests/Expectations/ExpectAtLeastPublishedTests.swift create mode 100644 Tests/CircumspectTests/Expectations/ExpectNothingPublishedTests.swift create mode 100644 Tests/CircumspectTests/Expectations/ExpectNotificationsTests.swift create mode 100644 Tests/CircumspectTests/Expectations/ExpectOnlyPublishedTests.swift create mode 100644 Tests/CircumspectTests/Expectations/ExpectPublishedTests.swift create mode 100644 Tests/CircumspectTests/Expectations/ExpectResultTests.swift create mode 100644 Tests/CircumspectTests/Expectations/ExpectValueTests.swift create mode 100644 Tests/CircumspectTests/ObservableObjectTests.swift create mode 100644 Tests/CircumspectTests/PublishersTests.swift create mode 100644 Tests/CircumspectTests/SimilarityTests.swift create mode 100644 Tests/CircumspectTests/TimeIntervalTests.swift create mode 100644 Tests/CircumspectTests/Tools.swift create mode 100644 Tests/CoreBusinessTests/AkamaiURLCodingTests.swift create mode 100644 Tests/CoreBusinessTests/DataProviderTests.swift create mode 100644 Tests/CoreBusinessTests/ErrorsTests.swift create mode 100644 Tests/CoreBusinessTests/HTTPURLResponseTests.swift create mode 100644 Tests/CoreBusinessTests/MediaMetadataTests.swift create mode 100644 Tests/CoreBusinessTests/Mock.swift create mode 100644 Tests/CoreBusinessTests/PublishersTests.swift create mode 100644 Tests/CoreBusinessTests/Resources.xcassets/Contents.json create mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_audioChapters.dataset/Contents.json create mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_audioChapters.dataset/urn_rts_audio_13598743.json create mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_drm.dataset/Contents.json create mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_drm.dataset/MediaComposition_drm.json create mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_live.dataset/Contents.json create mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_live.dataset/urn_rts_audio_3262320.json create mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_missingAnalytics.dataset/Contents.json create mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_missingAnalytics.dataset/urn_rts_video_13360574.json create mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_mixed.dataset/Contents.json create mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_mixed.dataset/urn_rts_video_14827796.json create mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_onDemand.dataset/Contents.json create mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_onDemand.dataset/urn_rts_video_13360574.json create mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_redundant.dataset/Contents.json create mode 100644 Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_redundant.dataset/urn_rts_video_13763072.json create mode 100644 Tests/CoreTests/AccumulatePublisherTests.swift create mode 100644 Tests/CoreTests/ArrayTests.swift create mode 100644 Tests/CoreTests/CombineLatestTests.swift create mode 100644 Tests/CoreTests/ComparableTests.swift create mode 100644 Tests/CoreTests/DemandBufferTests.swift create mode 100644 Tests/CoreTests/DispatchPublisherTests.swift create mode 100644 Tests/CoreTests/LimitedBufferTests.swift create mode 100644 Tests/CoreTests/MeasurePublisherTests.swift create mode 100644 Tests/CoreTests/NotificationPublisherDeallocationTests.swift create mode 100644 Tests/CoreTests/NotificationPublisherTests.swift create mode 100644 Tests/CoreTests/PublishAndRepeatOnOutputFromTests.swift create mode 100644 Tests/CoreTests/PublishOnOutputFromTests.swift create mode 100644 Tests/CoreTests/RangeReplaceableCollectionTests.swift create mode 100644 Tests/CoreTests/ReplaySubjectTests.swift create mode 100644 Tests/CoreTests/SlicePublisherTests.swift create mode 100644 Tests/CoreTests/StopwatchTests.swift create mode 100644 Tests/CoreTests/TimeTests.swift create mode 100644 Tests/CoreTests/Tools.swift create mode 100644 Tests/CoreTests/TriggerTests.swift create mode 100644 Tests/CoreTests/WaitPublisherTests.swift create mode 100644 Tests/CoreTests/WeakCapturePublisherTests.swift create mode 100644 Tests/CoreTests/WithPreviousPublisherTests.swift create mode 100644 Tests/MonitoringTests/MetricHitExpectation.swift create mode 100644 Tests/MonitoringTests/MetricPayload.swift create mode 100644 Tests/MonitoringTests/MetricsTracker.swift create mode 100644 Tests/MonitoringTests/MetricsTrackerTests.swift create mode 100644 Tests/MonitoringTests/MonitoringTestCase.swift create mode 100644 Tests/MonitoringTests/TrackingSessionTests.swift create mode 100644 Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatAllUpdateTests.swift create mode 100644 Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatOffUpdateTests.swift create mode 100644 Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatOneUpdateTests.swift create mode 100644 Tests/PlayerTests/AVPlayer/AVPlayerItemTests.swift create mode 100644 Tests/PlayerTests/AVPlayer/AVPlayerTests.swift create mode 100644 Tests/PlayerTests/Asset/AssetCreationTests.swift create mode 100644 Tests/PlayerTests/Asset/AssetMetadataMock.swift create mode 100644 Tests/PlayerTests/Asset/ResourceItemTests.swift create mode 100644 Tests/PlayerTests/AudioSession/AVAudioSessionNotificationTests.swift create mode 100644 Tests/PlayerTests/Extensions/AVPlayerItem.swift create mode 100644 Tests/PlayerTests/Extensions/AssetContent.swift create mode 100644 Tests/PlayerTests/Extensions/MetricEvent.swift create mode 100644 Tests/PlayerTests/Extensions/Player.swift create mode 100644 Tests/PlayerTests/Extensions/UUID.swift create mode 100644 Tests/PlayerTests/MediaSelection/AVMediaSelectionGroupTests.swift create mode 100644 Tests/PlayerTests/MediaSelection/AVMediaSelectionOptionTests.swift create mode 100644 Tests/PlayerTests/MediaSelection/MediaSelectionTests.swift create mode 100644 Tests/PlayerTests/MediaSelection/PreferredLanguagesForMediaSelectionTests.swift create mode 100644 Tests/PlayerTests/Metrics/AccessLogEventTests.swift create mode 100644 Tests/PlayerTests/Metrics/MetricsCollectorEventsTests.swift create mode 100644 Tests/PlayerTests/Metrics/MetricsCollectorTests.swift create mode 100644 Tests/PlayerTests/Metrics/MetricsStateTests.swift create mode 100644 Tests/PlayerTests/Player/BlockedTimeRangeTests.swift create mode 100644 Tests/PlayerTests/Player/PlaybackSpeedUpdateTests.swift create mode 100644 Tests/PlayerTests/Player/PlaybackTests.swift create mode 100644 Tests/PlayerTests/Player/PlayerTests.swift create mode 100644 Tests/PlayerTests/Player/QueueTests.swift create mode 100644 Tests/PlayerTests/Player/ReplayChecksTests.swift create mode 100644 Tests/PlayerTests/Player/ReplayTests.swift create mode 100644 Tests/PlayerTests/Player/SeekChecksTests.swift create mode 100644 Tests/PlayerTests/Player/SeekTests.swift create mode 100644 Tests/PlayerTests/Player/SpeedTests.swift create mode 100644 Tests/PlayerTests/Player/TextStyleRulesTests.swift create mode 100644 Tests/PlayerTests/PlayerItem/PlayerItemAssetPublisherTests.swift create mode 100644 Tests/PlayerTests/PlayerItem/PlayerItemTests.swift create mode 100644 Tests/PlayerTests/Playlist/CurrentItemTests.swift create mode 100644 Tests/PlayerTests/Playlist/ItemInsertionAfterTests.swift create mode 100644 Tests/PlayerTests/Playlist/ItemInsertionBeforeTests.swift create mode 100644 Tests/PlayerTests/Playlist/ItemMoveAfterTests.swift create mode 100644 Tests/PlayerTests/Playlist/ItemMoveBeforeTests.swift create mode 100644 Tests/PlayerTests/Playlist/ItemNavigationBackwardChecksTests.swift create mode 100644 Tests/PlayerTests/Playlist/ItemNavigationBackwardTests.swift create mode 100644 Tests/PlayerTests/Playlist/ItemNavigationForwardChecksTests.swift create mode 100644 Tests/PlayerTests/Playlist/ItemNavigationForwardTests.swift create mode 100644 Tests/PlayerTests/Playlist/ItemRemovalTests.swift create mode 100644 Tests/PlayerTests/Playlist/ItemsTests.swift create mode 100644 Tests/PlayerTests/Playlist/ItemsUpdateTests.swift create mode 100644 Tests/PlayerTests/Playlist/NavigationBackwardChecksTests.swift create mode 100644 Tests/PlayerTests/Playlist/NavigationBackwardTests.swift create mode 100644 Tests/PlayerTests/Playlist/NavigationForwardChecksTests.swift create mode 100644 Tests/PlayerTests/Playlist/NavigationForwardTests.swift create mode 100644 Tests/PlayerTests/Playlist/NavigationSmartBackwardChecksTests.swift create mode 100644 Tests/PlayerTests/Playlist/NavigationSmartBackwardTests.swift create mode 100644 Tests/PlayerTests/Playlist/NavigationSmartForwardChecksTests.swift create mode 100644 Tests/PlayerTests/Playlist/NavigationSmartForwardTests.swift create mode 100644 Tests/PlayerTests/Playlist/RepeatModeTests.swift create mode 100644 Tests/PlayerTests/ProgressTracker/ProgressTrackerPlaybackStateTests.swift create mode 100644 Tests/PlayerTests/ProgressTracker/ProgressTrackerProgressAvailabilityTests.swift create mode 100644 Tests/PlayerTests/ProgressTracker/ProgressTrackerProgressTests.swift create mode 100644 Tests/PlayerTests/ProgressTracker/ProgressTrackerRangeTests.swift create mode 100644 Tests/PlayerTests/ProgressTracker/ProgressTrackerSeekBehaviorTests.swift create mode 100644 Tests/PlayerTests/ProgressTracker/ProgressTrackerTimeTests.swift create mode 100644 Tests/PlayerTests/ProgressTracker/ProgressTrackerValueTests.swift create mode 100644 Tests/PlayerTests/Publishers/AVAssetMediaSelectionGroupsPublisherTests.swift create mode 100644 Tests/PlayerTests/Publishers/AVAsynchronousKeyValueLoadingPublisherTests.swift create mode 100644 Tests/PlayerTests/Publishers/AVPlayerBoundaryTimePublisherTests.swift create mode 100644 Tests/PlayerTests/Publishers/AVPlayerItemErrorPublisherTests.swift create mode 100644 Tests/PlayerTests/Publishers/AVPlayerItemMetricEventPublisherTests.swift create mode 100644 Tests/PlayerTests/Publishers/AVPlayerPeriodicTimePublisherTests.swift create mode 100644 Tests/PlayerTests/Publishers/MetadataPublisherTests.swift create mode 100644 Tests/PlayerTests/Publishers/NowPlayingInfoPublisherTests.swift create mode 100644 Tests/PlayerTests/Publishers/PeriodicMetricsPublisherTests.swift create mode 100644 Tests/PlayerTests/Publishers/PlayerItemMetricEventPublisherTests.swift create mode 100644 Tests/PlayerTests/Publishers/PlayerPublisherTests.swift create mode 100644 Tests/PlayerTests/QueuePlayer/QueuePlayerItemsTests.swift create mode 100644 Tests/PlayerTests/QueuePlayer/QueuePlayerSeekTests.swift create mode 100644 Tests/PlayerTests/QueuePlayer/QueuePlayerSeekTimePublisherTests.swift create mode 100644 Tests/PlayerTests/QueuePlayer/QueuePlayerSmoothSeekTests.swift create mode 100644 Tests/PlayerTests/Resources/invalid.jpg create mode 100644 Tests/PlayerTests/Resources/pixel.jpg create mode 100644 Tests/PlayerTests/Skips/SkipBackwardChecksTests.swift create mode 100644 Tests/PlayerTests/Skips/SkipBackwardTests.swift create mode 100644 Tests/PlayerTests/Skips/SkipForwardChecksTests.swift create mode 100644 Tests/PlayerTests/Skips/SkipForwardTests.swift create mode 100644 Tests/PlayerTests/Skips/SkipToDefaultChecksTests.swift create mode 100644 Tests/PlayerTests/Skips/SkipToDefaultTests.swift create mode 100644 Tests/PlayerTests/Tools/AVMediaSelectionOptionMock.swift create mode 100644 Tests/PlayerTests/Tools/ContentKeySessionDelegateMock.swift create mode 100644 Tests/PlayerTests/Tools/LanguageIdentifiable.swift create mode 100644 Tests/PlayerTests/Tools/Matchers.swift create mode 100644 Tests/PlayerTests/Tools/MediaAccessibilityDisplayType.swift create mode 100644 Tests/PlayerTests/Tools/PlayerItem.swift create mode 100644 Tests/PlayerTests/Tools/PlayerItemTrackerMock.swift create mode 100644 Tests/PlayerTests/Tools/ResourceLoaderDelegateMock.swift create mode 100644 Tests/PlayerTests/Tools/Similarity.swift create mode 100644 Tests/PlayerTests/Tools/TestCase.swift create mode 100644 Tests/PlayerTests/Tools/Tools.swift create mode 100644 Tests/PlayerTests/Tools/TrackerUpdateMock.swift create mode 100644 Tests/PlayerTests/Tracking/PlayerItemTrackerLifeCycleTests.swift create mode 100644 Tests/PlayerTests/Tracking/PlayerItemTrackerMetricPublisherTests.swift create mode 100644 Tests/PlayerTests/Tracking/PlayerItemTrackerSessionTests.swift create mode 100644 Tests/PlayerTests/Tracking/PlayerItemTrackerUpdateTests.swift create mode 100644 Tests/PlayerTests/Tracking/PlayerTrackingTests.swift create mode 100644 Tests/PlayerTests/Types/CMTimeRangeTests.swift create mode 100644 Tests/PlayerTests/Types/CMTimeTests.swift create mode 100644 Tests/PlayerTests/Types/ErrorsTests.swift create mode 100644 Tests/PlayerTests/Types/ImageSourceTests.swift create mode 100644 Tests/PlayerTests/Types/ItemErrorTests.swift create mode 100644 Tests/PlayerTests/Types/PlaybackSpeedTests.swift create mode 100644 Tests/PlayerTests/Types/PlaybackStateTests.swift create mode 100644 Tests/PlayerTests/Types/PlayerConfigurationTests.swift create mode 100644 Tests/PlayerTests/Types/PlayerLimitsTests.swift create mode 100644 Tests/PlayerTests/Types/PlayerMetadataTests.swift create mode 100644 Tests/PlayerTests/Types/PositionTests.swift create mode 100644 Tests/PlayerTests/Types/StreamTypeTests.swift create mode 100644 Tests/PlayerTests/Types/TimePropertiesTests.swift create mode 100644 Tests/PlayerTests/UserInterface/VisibilityTrackerTests.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Pillarbox.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Pillarbox.xcscheme index 9787d909..2fe926da 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Pillarbox.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Pillarbox.xcscheme @@ -185,6 +185,78 @@ region = "CH" codeCoverageEnabled = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + Void + + fileprivate init(name: ComScoreHit.Name, evaluate: @escaping (ComScoreLabels) -> Void) { + self.name = name + self.evaluate = evaluate + } + + static func match(hit: ComScoreHit, with expectation: Self) -> Bool { + guard hit.name == expectation.name else { return false } + expectation.evaluate(hit.labels) + return true + } +} + +extension ComScoreHitExpectation: CustomDebugStringConvertible { + var debugDescription: String { + name.rawValue + } +} + +extension ComScoreTestCase { + func play(evaluate: @escaping (ComScoreLabels) -> Void = { _ in }) -> ComScoreHitExpectation { + ComScoreHitExpectation(name: .play, evaluate: evaluate) + } + + func playrt(evaluate: @escaping (ComScoreLabels) -> Void = { _ in }) -> ComScoreHitExpectation { + ComScoreHitExpectation(name: .playrt, evaluate: evaluate) + } + + func pause(evaluate: @escaping (ComScoreLabels) -> Void = { _ in }) -> ComScoreHitExpectation { + ComScoreHitExpectation(name: .pause, evaluate: evaluate) + } + + func end(evaluate: @escaping (ComScoreLabels) -> Void = { _ in }) -> ComScoreHitExpectation { + ComScoreHitExpectation(name: .end, evaluate: evaluate) + } + + func view(evaluate: @escaping (ComScoreLabels) -> Void = { _ in }) -> ComScoreHitExpectation { + ComScoreHitExpectation(name: .view, evaluate: evaluate) + } +} diff --git a/Tests/AnalyticsTests/ComScore/ComScorePageViewTests.swift b/Tests/AnalyticsTests/ComScore/ComScorePageViewTests.swift new file mode 100644 index 00000000..4e8e80da --- /dev/null +++ b/Tests/AnalyticsTests/ComScore/ComScorePageViewTests.swift @@ -0,0 +1,332 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import Nimble +import PillarboxCircumspect +import UIKit + +private class AutomaticMockViewController: UIViewController, PageViewTracking { + private var pageName: String { + title ?? "automatic" + } + + var comScorePageView: ComScorePageView { + .init(name: pageName) + } + + var commandersActPageView: CommandersActPageView { + .init(name: pageName, type: "type") + } + + init(title: String? = nil) { + super.init(nibName: nil, bundle: nil) + self.title = title + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private class ManualMockViewController: UIViewController, PageViewTracking { + private var pageName: String { + "manual" + } + + var comScorePageView: ComScorePageView { + .init(name: pageName) + } + + var commandersActPageView: CommandersActPageView { + .init(name: pageName, type: "type") + } + + var isTrackedAutomatically: Bool { + false + } +} + +final class ComScorePageViewTests: ComScoreTestCase { + func testGlobals() { + expectAtLeastHits( + view { labels in + expect(labels.c2).to(equal("6036016")) + expect(labels.ns_ap_an).to(equal("xctest")) + expect(labels.c8).to(equal("name")) + expect(labels.ns_st_mp).to(beNil()) + expect(labels.ns_st_mv).to(beNil()) + expect(labels.mp_brand).to(equal("SRG")) + expect(labels.mp_v).notTo(beEmpty()) + expect(labels.cs_ucfr).to(beEmpty()) + } + ) { + Analytics.shared.trackPageView( + comScore: .init(name: "name"), + commandersAct: .init(name: "name", type: "type") + ) + } + } + + func testBlankTitle() { + guard nimbleThrowAssertionsAvailable() else { return } + expect(Analytics.shared.trackPageView( + comScore: .init(name: " "), + commandersAct: .init(name: "name", type: "type") + )).to(throwAssertion()) + } + + func testCustomLabels() { + expectAtLeastHits( + view { labels in + expect(labels["key"]).to(equal("value")) + } + ) { + Analytics.shared.trackPageView( + comScore: .init(name: "name", labels: ["key": "value"]), + commandersAct: .init(name: "name", type: "type") + ) + } + } + + func testCustomLabelsForbiddenOverrides() { + expectAtLeastHits( + view { labels in + expect(labels.c8).to(equal("name")) + expect(labels.cs_ucfr).to(beEmpty()) + } + ) { + Analytics.shared.trackPageView( + comScore: .init(name: "name", labels: [ + "c8": "overridden_title", + "cs_ucfr": "42" + ]), + commandersAct: .init(name: "name", type: "type") + ) + } + } + + func testDefaultProtocolImplementation() { + let viewController = AutomaticMockViewController() + expect(viewController.isTrackedAutomatically).to(beTrue()) + } + + func testCustomProtocolImplementation() { + let viewController = ManualMockViewController() + expect(viewController.isTrackedAutomatically).to(beFalse()) + } + + func testAutomaticTrackingWithoutProtocolImplementation() { + let viewController = UIViewController() + expectNoHits(during: .seconds(2)) { + viewController.simulateViewAppearance() + } + } + + func testManualTrackingWithoutProtocolImplementation() { + let viewController = UIViewController() + expectNoHits(during: .seconds(2)) { + viewController.trackPageView() + } + } + + func testAutomaticTrackingWhenViewAppears() { + let viewController = AutomaticMockViewController() + expectAtLeastHits( + view { labels in + expect(labels.c8).to(equal("automatic")) + } + ) { + viewController.simulateViewAppearance() + } + } + + func testSinglePageViewWhenContainerViewAppears() { + let viewController = AutomaticMockViewController() + let navigationController = UINavigationController(rootViewController: viewController) + expectHits( + view { labels in + expect(labels.c8).to(equal("automatic")) + }, + during: .seconds(2) + ) { + navigationController.simulateViewAppearance() + viewController.simulateViewAppearance() + } + } + + func testAutomaticTrackingWhenActiveViewInParentAppears() { + let viewController1 = AutomaticMockViewController(title: "title1") + let viewController2 = AutomaticMockViewController(title: "title2") + let tabBarController = UITabBarController() + tabBarController.viewControllers = [viewController1, viewController2] + expectAtLeastHits( + view { labels in + expect(labels.c8).to(equal("title1")) + } + ) { + viewController1.simulateViewAppearance() + } + } + + func testAutomaticTrackingWhenInactiveViewInParentAppears() { + let viewController1 = AutomaticMockViewController(title: "title1") + let viewController2 = AutomaticMockViewController(title: "title2") + let tabBarController = UITabBarController() + tabBarController.viewControllers = [viewController1, viewController2] + expectNoHits(during: .seconds(2)) { + viewController2.simulateViewAppearance() + } + } + + func testAutomaticTrackingWhenViewAppearsInActiveHierarchy() { + let viewController1 = AutomaticMockViewController(title: "title1") + let viewController2 = AutomaticMockViewController(title: "title2") + let tabBarController = UITabBarController() + tabBarController.viewControllers = [ + UINavigationController(rootViewController: viewController1), + UINavigationController(rootViewController: viewController2) + ] + expectAtLeastHits( + view { labels in + expect(labels.c8).to(equal("title1")) + } + ) { + viewController1.simulateViewAppearance() + } + } + + func testAutomaticTrackingWhenViewAppearsInInactiveHierarchy() { + let viewController1 = AutomaticMockViewController(title: "title1") + let viewController2 = AutomaticMockViewController(title: "title2") + let tabBarController = UITabBarController() + tabBarController.viewControllers = [ + UINavigationController(rootViewController: viewController1), + UINavigationController(rootViewController: viewController2) + ] + expectNoHits(during: .seconds(2)) { + viewController2.simulateViewAppearance() + } + } + + func testManualTracking() { + let viewController = ManualMockViewController() + expectNoHits(during: .seconds(2)) { + viewController.simulateViewAppearance() + } + expectAtLeastHits( + view { labels in + expect(labels.c8).to(equal("manual")) + } + ) { + viewController.trackPageView() + } + } + + func testRecursiveAutomaticTrackingOnViewController() { + let viewController = AutomaticMockViewController() + expectAtLeastHits( + view { labels in + expect(labels.c8).to(equal("automatic")) + } + ) { + viewController.recursivelyTrackAutomaticPageViews() + } + } + + func testRecursiveAutomaticTrackingOnNavigationController() { + let viewController = UINavigationController(rootViewController: AutomaticMockViewController(title: "root")) + viewController.pushViewController(AutomaticMockViewController(title: "pushed"), animated: false) + expectAtLeastHits( + view { labels in + expect(labels.c8).to(equal("pushed")) + } + ) { + viewController.recursivelyTrackAutomaticPageViews() + } + } + + func testRecursiveAutomaticTrackingOnPageViewController() { + let viewController = UIPageViewController() + viewController.setViewControllers([AutomaticMockViewController()], direction: .forward, animated: false) + expectAtLeastHits( + view { labels in + expect(labels.c8).to(equal("automatic")) + } + ) { + viewController.recursivelyTrackAutomaticPageViews() + } + } + + func testRecursiveAutomaticTrackingOnSplitViewController() { + let viewController = UISplitViewController() + viewController.viewControllers = [ + AutomaticMockViewController(title: "title1"), + AutomaticMockViewController(title: "title2") + ] + expectAtLeastHits( + view { labels in + expect(labels.c8).to(equal("title1")) + } + ) { + viewController.recursivelyTrackAutomaticPageViews() + } + } + + func testRecursiveAutomaticTrackingOnTabBarController() { + let viewController = UITabBarController() + viewController.viewControllers = [ + AutomaticMockViewController(title: "title1"), + AutomaticMockViewController(title: "title2"), + AutomaticMockViewController(title: "title3") + ] + expectAtLeastHits( + view { labels in + expect(labels.c8).to(equal("title1")) + } + ) { + viewController.recursivelyTrackAutomaticPageViews() + } + } + + func testRecursiveAutomaticTrackingOnWindow() { + let window = UIWindow() + window.makeKeyAndVisible() + window.rootViewController = AutomaticMockViewController() + expectAtLeastHits( + view { labels in + expect(labels.c8).to(equal("automatic")) + } + ) { + window.recursivelyTrackAutomaticPageViews() + } + } + + func testRecursiveAutomaticTrackingOnWindowWithModalPresentation() { + let window = UIWindow() + let rootViewController = AutomaticMockViewController(title: "root") + window.makeKeyAndVisible() + window.rootViewController = rootViewController + rootViewController.present(AutomaticMockViewController(title: "modal"), animated: false) + expectHits( + view { labels in + expect(labels.c8).to(equal("modal")) + }, + during: .seconds(2) + ) { + window.recursivelyTrackAutomaticPageViews() + } + } +} + +private extension UIViewController { + func simulateViewAppearance() { + beginAppearanceTransition(true, animated: false) + endAppearanceTransition() + } +} diff --git a/Tests/AnalyticsTests/ComScore/ComScoreTestCase.swift b/Tests/AnalyticsTests/ComScore/ComScoreTestCase.swift new file mode 100644 index 00000000..5c76107d --- /dev/null +++ b/Tests/AnalyticsTests/ComScore/ComScoreTestCase.swift @@ -0,0 +1,78 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import Dispatch +import PillarboxCircumspect + +class ComScoreTestCase: TestCase {} + +extension ComScoreTestCase { + /// Collects hits emitted by comScore during some time interval and matches them against expectations. + /// + /// A network connection is required by the comScore SDK to properly emit hits. + func expectHits( + _ expectations: ComScoreHitExpectation..., + during interval: DispatchTimeInterval = .seconds(20), + file: StaticString = #file, + line: UInt = #line, + while executing: (() -> Void)? = nil + ) { + AnalyticsListener.captureComScoreHits { publisher in + expectPublished( + values: expectations, + from: publisher, + to: ComScoreHitExpectation.match, + during: interval, + file: file, + line: line, + while: executing + ) + } + } + + /// Expects hits emitted by comScore during some time interval and matches them against expectations. + /// + /// A network connection is required by the comScore SDK to properly emit hits. + func expectAtLeastHits( + _ expectations: ComScoreHitExpectation..., + timeout: DispatchTimeInterval = .seconds(20), + file: StaticString = #file, + line: UInt = #line, + while executing: (() -> Void)? = nil + ) { + AnalyticsListener.captureComScoreHits { publisher in + expectAtLeastPublished( + values: expectations, + from: publisher, + to: ComScoreHitExpectation.match, + timeout: timeout, + file: file, + line: line, + while: executing + ) + } + } + + /// Expects no hits emitted by comScore during some time interval. + func expectNoHits( + during interval: DispatchTimeInterval = .seconds(20), + file: StaticString = #file, + line: UInt = #line, + while executing: (() -> Void)? = nil + ) { + AnalyticsListener.captureComScoreHits { publisher in + expectNothingPublished( + from: publisher, + during: interval, + file: file, + line: line, + while: executing + ) + } + } +} diff --git a/Tests/AnalyticsTests/ComScore/ComScoreTrackerDvrPropertiesTests.swift b/Tests/AnalyticsTests/ComScore/ComScoreTrackerDvrPropertiesTests.swift new file mode 100644 index 00000000..b391dd75 --- /dev/null +++ b/Tests/AnalyticsTests/ComScore/ComScoreTrackerDvrPropertiesTests.swift @@ -0,0 +1,93 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import CoreMedia +import Nimble +import PillarboxPlayer +import PillarboxStreams + +final class ComScoreTrackerDvrPropertiesTests: ComScoreTestCase { + func testOnDemand() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + + expectAtLeastHits( + play { labels in + expect(labels.ns_st_ldw).to(equal(0)) + expect(labels.ns_st_ldo).to(equal(0)) + } + ) { + player.play() + } + } + + func testLive() { + let player = Player(item: .simple( + url: Stream.live.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + + expectAtLeastHits( + play { labels in + expect(labels.ns_st_ldw).to(equal(0)) + expect(labels.ns_st_ldo).to(equal(0)) + } + ) { + player.play() + } + } + + func testDvrAtLiveEdge() { + let player = Player(item: .simple( + url: Stream.dvr.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + + expectAtLeastHits( + play { labels in + expect(labels.ns_st_ldo).to(equal(0)) + expect(labels.ns_st_ldw).to(equal(Stream.dvr.duration.seconds)) + } + ) { + player.play() + } + } + + func testDvrAwayFromLiveEdge() { + let player = Player(item: .simple( + url: Stream.dvr.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + + player.play() + expect(player.playbackState).toEventually(equal(.playing)) + + expectAtLeastHits( + pause { labels in + expect(labels.ns_st_ldo).to(equal(0)) + expect(labels.ns_st_ldw).to(equal(Stream.dvr.duration.seconds)) + }, + play { labels in + expect(labels.ns_st_ldo).to(beCloseTo(10, within: 3)) + expect(labels.ns_st_ldw).to(equal(Stream.dvr.duration.seconds)) + } + ) { + player.seek(at(player.time() - CMTime(value: 10, timescale: 1))) + } + } +} diff --git a/Tests/AnalyticsTests/ComScore/ComScoreTrackerMetadataTests.swift b/Tests/AnalyticsTests/ComScore/ComScoreTrackerMetadataTests.swift new file mode 100644 index 00000000..92e43ce9 --- /dev/null +++ b/Tests/AnalyticsTests/ComScore/ComScoreTrackerMetadataTests.swift @@ -0,0 +1,60 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import Nimble +import PillarboxPlayer +import PillarboxStreams + +final class ComScoreTrackerMetadataTests: ComScoreTestCase { + func testMetadata() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in + ["meta_1": "custom-1", "meta_2": "42"] + } + ] + )) + + expectAtLeastHits( + play { labels in + expect(labels["meta_1"]).to(equal("custom-1")) + expect(labels["meta_2"]).to(equal(42)) + expect(labels["cs_ucfr"]).to(beEmpty()) + } + ) { + player.play() + } + } + + func testEmptyMetadata() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in [:] } + ] + )) + + expectNoHits(during: .seconds(3)) { + player.play() + } + } + + func testNoMetadata() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in [:] } + ] + )) + + expectNoHits(during: .seconds(3)) { + player.play() + } + } +} diff --git a/Tests/AnalyticsTests/ComScore/ComScoreTrackerPlaybackSpeedTests.swift b/Tests/AnalyticsTests/ComScore/ComScoreTrackerPlaybackSpeedTests.swift new file mode 100644 index 00000000..5ccc00f1 --- /dev/null +++ b/Tests/AnalyticsTests/ComScore/ComScoreTrackerPlaybackSpeedTests.swift @@ -0,0 +1,31 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import Nimble +import PillarboxPlayer +import PillarboxStreams + +final class ComScoreTrackerPlaybackSpeedTests: ComScoreTestCase { + func testRateAtStart() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + player.setDesiredPlaybackSpeed(0.5) + + expectAtLeastHits( + play { labels in + expect(labels.ns_st_rt).to(equal(50)) + } + ) { + player.play() + } + } +} diff --git a/Tests/AnalyticsTests/ComScore/ComScoreTrackerRateTests.swift b/Tests/AnalyticsTests/ComScore/ComScoreTrackerRateTests.swift new file mode 100644 index 00000000..fb88e1be --- /dev/null +++ b/Tests/AnalyticsTests/ComScore/ComScoreTrackerRateTests.swift @@ -0,0 +1,65 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import Nimble +import PillarboxPlayer +import PillarboxStreams + +final class ComScoreTrackerRateTests: ComScoreTestCase { + func testInitialRate() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + + expectAtLeastHits( + play { labels in + expect(labels.ns_st_rt).to(equal(200)) + } + ) { + player.setDesiredPlaybackSpeed(2) + player.play() + } + } + + func testWhenDifferentRateApplied() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + player.play() + expect(player.playbackState).toEventually(equal(.playing)) + + expectAtLeastHits( + playrt { labels in + expect(labels.ns_st_rt).to(equal(200)) + } + ) { + player.setDesiredPlaybackSpeed(2) + } + } + + func testWhenSameRateApplied() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + player.play() + expect(player.playbackState).toEventually(equal(.playing)) + + expectNoHits(during: .seconds(2)) { + player.setDesiredPlaybackSpeed(1) + } + } +} diff --git a/Tests/AnalyticsTests/ComScore/ComScoreTrackerSeekTests.swift b/Tests/AnalyticsTests/ComScore/ComScoreTrackerSeekTests.swift new file mode 100644 index 00000000..e7e3d963 --- /dev/null +++ b/Tests/AnalyticsTests/ComScore/ComScoreTrackerSeekTests.swift @@ -0,0 +1,60 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import CoreMedia +import Nimble +import PillarboxPlayer +import PillarboxStreams + +final class ComScoreTrackerSeekTests: ComScoreTestCase { + func testSeekWhilePlaying() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + + player.play() + expect(player.playbackState).toEventually(equal(.playing)) + + expectAtLeastHits( + pause { labels in + expect(labels.ns_st_po).to(beCloseTo(0, within: 0.5)) + }, + play { labels in + expect(labels.ns_st_po).to(beCloseTo(7, within: 0.5)) + } + ) { + player.seek(at(.init(value: 7, timescale: 1))) + } + } + + func testSeekWhilePaused() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + + expect(player.playbackState).toEventually(equal(.paused)) + + expectNoHits(during: .seconds(2)) { + player.seek(at(.init(value: 7, timescale: 1))) + } + + expectAtLeastHits( + play { labels in + expect(labels.ns_st_po).to(beCloseTo(7, within: 0.5)) + } + ) { + player.play() + } + } +} diff --git a/Tests/AnalyticsTests/ComScore/ComScoreTrackerTests.swift b/Tests/AnalyticsTests/ComScore/ComScoreTrackerTests.swift new file mode 100644 index 00000000..a9082760 --- /dev/null +++ b/Tests/AnalyticsTests/ComScore/ComScoreTrackerTests.swift @@ -0,0 +1,237 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import CoreMedia +import Nimble +import PillarboxPlayer +import PillarboxStreams + +// Testing comScore end events is a bit tricky: +// 1. Apparently comScore will never emit events if a play event is followed by an end event within ~5 seconds. For +// this reason all tests checking end events must wait ~5 seconds after a play event. +// 2. End events are emitted automatically to close a session when the `SCORStreamingAnalytics` is destroyed. Since +// we are not notifying the end event ourselves in such cases we cannot customize the end event labels directly. +// Fortunately we can customize them indirectly, though, since the end event inherits labels from a former event. +// Thus, to test end events resulting from tracker deallocation we need to have another event sent within the same +// expectation first so that the end event is provided a listener identifier. +final class ComScoreTrackerTests: ComScoreTestCase { + func testGlobals() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + expectAtLeastHits( + play { labels in + expect(labels.ns_st_mp).to(equal("Pillarbox")) + expect(labels.ns_st_mv).to(equal(PackageInfo.version)) + expect(labels.cs_ucfr).to(beEmpty()) + } + ) { + player.play() + } + } + + func testInitiallyPlaying() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + expectAtLeastHits( + play { labels in + expect(labels.ns_st_po).to(beCloseTo(0, within: 2)) + } + ) { + player.play() + } + } + + func testInitiallyPaused() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + expectNoHits(during: .seconds(2)) { + player.pause() + } + } + + func testPauseDuringPlayback() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + + player.play() + expect(player.time().seconds).toEventually(beGreaterThan(1)) + + expectAtLeastHits( + pause { labels in + expect(labels.ns_st_po).to(beCloseTo(1, within: 0.5)) + } + ) { + player.pause() + } + } + + func testPlaybackEnd() { + let player = Player(item: .simple( + // See 1. at the top of this file. + url: Stream.mediumOnDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + expectAtLeastHits( + play(), + end { labels in + expect(labels.ns_st_po).to(beCloseTo(Stream.mediumOnDemand.duration.seconds, within: 0.5)) + } + ) { + player.play() + } + } + + func testDestroyPlayerDuringPlayback() { + var player: Player? = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + expectAtLeastHits( + play(), + end { labels in + expect(labels.ns_st_po).to(beCloseTo(5, within: 0.5)) + } + ) { + // See 2. at the top of this file. + player?.play() + // See 1. at the top of this file. + expect(player?.time().seconds).toEventually(beGreaterThan(5)) + player = nil + } + } + + func testFailure() { + let player = Player(item: .simple( + url: Stream.unavailable.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + expectNoHits(during: .seconds(3)) { + player.play() + } + } + + func testDisableTrackingDuringPlayback() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + expectAtLeastHits(play(), end()) { + // See 2. at the top of this file. + player.play() + // See 1. at the top of this file. + expect(player.time().seconds).toEventually(beGreaterThan(5)) + player.isTrackingEnabled = false + } + } + + func testEnableTrackingDuringPlayback() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + + player.isTrackingEnabled = false + + expectNoHits(during: .seconds(2)) { + player.play() + } + + expectAtLeastHits(play()) { + player.isTrackingEnabled = true + } + } + + func testInitialPlaybackRate() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + expectAtLeastHits( + play { labels in + expect(labels.ns_st_rt).to(equal(200)) + } + ) { + player.setDesiredPlaybackSpeed(2) + player.play() + } + } + + func testOnDemandStartAtGivenPosition() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ], + configuration: .init(position: at(.init(value: 100, timescale: 1))) + )) + expectAtLeastHits( + play { labels in + expect(labels.ns_st_po).to(beCloseTo(100, within: 5)) + } + ) { + player.play() + } + } + + func testReplay() { + let player = Player(item: .simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [ + ComScoreTracker.adapter { _ in .test } + ] + )) + var ns_st_id: String? + expectAtLeastHits( + play { labels in + expect(labels["media_title"]).to(equal("name")) + expect(labels.ns_st_id).notTo(beNil()) + ns_st_id = labels.ns_st_id + }, + end() + ) { + player.play() + } + expectAtLeastHits( + play { labels in + expect(labels["media_title"]).to(equal("name")) + expect(labels.ns_st_id).notTo(beNil()) + expect(labels.ns_st_id).notTo(equal(ns_st_id)) + } + ) { + player.replay() + } + } +} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActEventTests.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActEventTests.swift new file mode 100644 index 00000000..0bee15fd --- /dev/null +++ b/Tests/AnalyticsTests/CommandersAct/CommandersActEventTests.swift @@ -0,0 +1,100 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import Nimble +import PillarboxCircumspect +import TCServerSide + +final class CommandersActEventTests: CommandersActTestCase { + func testMergingWithGlobals() { + let event = CommandersActEvent( + name: "name", + labels: [ + "event-label": "event", + "common-label": "event" + ] + ) + let globals = CommandersActGlobals( + consentServices: ["service1,service2,service3"], + labels: [ + "globals-label": "globals", + "common-label": "globals" + ] + ) + + expect(event.merging(globals: globals).labels).to(equal([ + "consent_services": "service1,service2,service3", + "globals-label": "globals", + "event-label": "event", + "common-label": "globals" + ])) + } + + func testBlankName() { + guard nimbleThrowAssertionsAvailable() else { return } + expect(Analytics.shared.sendEvent(commandersAct: .init(name: " "))).to(throwAssertion()) + } + + func testName() { + expectAtLeastHits(custom(name: "name")) { + Analytics.shared.sendEvent(commandersAct: .init(name: "name")) + } + } + + func testCustomLabels() { + expectAtLeastHits( + custom(name: "name") { labels in + // Use `media_player_display`, a media-only key, so that its value can be parsed. + expect(labels.media_player_display).to(equal("value")) + } + ) { + Analytics.shared.sendEvent(commandersAct: .init( + name: "name", + labels: ["media_player_display": "value"] + )) + } + } + + func testUniqueIdentifier() { + let identifier = TCPredefinedVariables.sharedInstance().uniqueIdentifier() + expectAtLeastHits( + custom(name: "name") { labels in + expect(labels.context.device.sdk_id).to(equal(identifier)) + expect(labels.user.consistent_anonymous_id).to(equal(identifier)) + } + ) { + Analytics.shared.sendEvent(commandersAct: .init(name: "name")) + } + } + + func testGlobals() { + expectAtLeastHits( + custom(name: "name") { labels in + expect(labels.consent_services).to(equal("service1,service2,service3")) + } + ) { + Analytics.shared.sendEvent(commandersAct: .init(name: "name")) + } + } + + func testCustomLabelsForbiddenOverrides() { + expectAtLeastHits( + custom(name: "name") { labels in + expect(labels.consent_services).to(equal("service1,service2,service3")) + } + ) { + Analytics.shared.sendEvent(commandersAct: .init( + name: "name", + labels: [ + "event_name": "overridden_name", + "consent_services": "service42" + ] + )) + } + } +} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActHeartbeatTests.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActHeartbeatTests.swift new file mode 100644 index 00000000..497cc2f8 --- /dev/null +++ b/Tests/AnalyticsTests/CommandersAct/CommandersActHeartbeatTests.swift @@ -0,0 +1,88 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import Combine +import Nimble +import PillarboxPlayer +import PillarboxStreams + +final class CommandersActHeartbeatTests: CommandersActTestCase { + private var cancellables = Set() + + private static func player(from stream: PillarboxStreams.Stream, into cancellables: inout Set) -> Player { + let heartbeat = CommandersActHeartbeat(delay: 0.1, posInterval: 0.1, uptimeInterval: 0.2) + let player = Player(item: .simple(url: stream.url)) + player.propertiesPublisher + .sink { properties in + heartbeat.update(with: properties) { properties in + ["media_volume": properties.isMuted ? "0" : "100"] + } + } + .store(in: &cancellables) + return player + } + + override func tearDown() { + super.tearDown() + cancellables = [] + } + + func testNoHeartbeatInitially() { + _ = Self.player(from: .onDemand, into: &cancellables) + expectNoHits(during: .milliseconds(300)) + } + + func testOnDemandHeartbeatAfterPlay() { + let player = Self.player(from: .onDemand, into: &cancellables) + expectAtLeastHits(pos(), pos()) { + player.play() + } + } + + func testLiveHeartbeatAfterPlay() { + let player = Self.player(from: .live, into: &cancellables) + expectAtLeastHits(pos(), uptime(), pos(), pos(), uptime()) { + player.play() + } + } + + func testDvrHeartbeatAfterPlay() { + let player = Self.player(from: .dvr, into: &cancellables) + expectAtLeastHits(pos(), uptime(), pos(), pos(), uptime()) { + player.play() + } + } + + func testNoHeartbeatAfterPause() { + let player = Self.player(from: .onDemand, into: &cancellables) + expectAtLeastHits(pos()) { + player.play() + } + expectNoHits(during: .milliseconds(300)) { + player.pause() + } + } + + func testHeartbeatPropertyUpdate() { + let player = Self.player(from: .onDemand, into: &cancellables) + expectAtLeastHits( + pos { labels in + expect(labels.media_volume).notTo(equal(0)) + } + ) { + player.play() + } + expectAtLeastHits( + pos { labels in + expect(labels.media_volume).to(equal(0)) + } + ) { + player.isMuted = true + } + } +} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActHitExpectation.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActHitExpectation.swift new file mode 100644 index 00000000..3ba8bcb5 --- /dev/null +++ b/Tests/AnalyticsTests/CommandersAct/CommandersActHitExpectation.swift @@ -0,0 +1,68 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import PillarboxAnalytics + +/// Describes a Commanders Act stream hit expectation. +struct CommandersActHitExpectation { + private let name: CommandersActHit.Name + private let evaluate: (CommandersActLabels) -> Void + + fileprivate init(name: CommandersActHit.Name, evaluate: @escaping (CommandersActLabels) -> Void) { + self.name = name + self.evaluate = evaluate + } + + static func match(hit: CommandersActHit, with expectation: Self) -> Bool { + guard hit.name == expectation.name else { return false } + expectation.evaluate(hit.labels) + return true + } +} + +extension CommandersActHitExpectation: CustomDebugStringConvertible { + var debugDescription: String { + name.rawValue + } +} + +extension CommandersActTestCase { + func play(evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { + CommandersActHitExpectation(name: .play, evaluate: evaluate) + } + + func pause(evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { + CommandersActHitExpectation(name: .pause, evaluate: evaluate) + } + + func seek(evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { + CommandersActHitExpectation(name: .seek, evaluate: evaluate) + } + + func stop(evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { + CommandersActHitExpectation(name: .stop, evaluate: evaluate) + } + + func eof(evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { + CommandersActHitExpectation(name: .eof, evaluate: evaluate) + } + + func pos(evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { + CommandersActHitExpectation(name: .pos, evaluate: evaluate) + } + + func uptime(evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { + CommandersActHitExpectation(name: .uptime, evaluate: evaluate) + } + + func page_view(evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { + CommandersActHitExpectation(name: .page_view, evaluate: evaluate) + } + + func custom(name: String, evaluate: @escaping (CommandersActLabels) -> Void = { _ in }) -> CommandersActHitExpectation { + CommandersActHitExpectation(name: .custom(name), evaluate: evaluate) + } +} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActPageViewTests.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActPageViewTests.swift new file mode 100644 index 00000000..7aa5c7fa --- /dev/null +++ b/Tests/AnalyticsTests/CommandersAct/CommandersActPageViewTests.swift @@ -0,0 +1,183 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import Nimble +import PillarboxCircumspect + +final class CommandersActPageViewTests: CommandersActTestCase { + func testMergingWithGlobals() { + let pageView = CommandersActPageView( + name: "name", + type: "type", + labels: [ + "pageview-label": "pageview", + "common-label": "pageview" + ] + ) + let globals = CommandersActGlobals( + consentServices: ["service1,service2,service3"], + labels: [ + "globals-label": "globals", + "common-label": "globals" + ] + ) + + expect(pageView.merging(globals: globals).labels).to(equal([ + "consent_services": "service1,service2,service3", + "globals-label": "globals", + "pageview-label": "pageview", + "common-label": "globals" + ])) + } + + func testLabels() { + expectAtLeastHits( + page_view { labels in + expect(labels.page_type).to(equal("type")) + expect(labels.page_name).to(equal("name")) + expect(labels.navigation_level_0).to(beNil()) + expect(labels.navigation_level_1).to(equal("level_1")) + expect(labels.navigation_level_2).to(equal("level_2")) + expect(labels.navigation_level_3).to(equal("level_3")) + expect(labels.navigation_level_4).to(equal("level_4")) + expect(labels.navigation_level_5).to(equal("level_5")) + expect(labels.navigation_level_6).to(equal("level_6")) + expect(labels.navigation_level_7).to(equal("level_7")) + expect(labels.navigation_level_8).to(equal("level_8")) + expect(labels.navigation_level_9).to(beNil()) + expect(["phone", "tablet", "tvbox", "phone"]).to(contain([labels.navigation_device])) + expect(labels.app_library_version).to(equal(Analytics.version)) + expect(labels.navigation_app_site_name).to(equal("site")) + expect(labels.navigation_property_type).to(equal("app")) + expect(labels.navigation_bu_distributer).to(equal("SRG")) + expect(labels.consent_services).to(equal("service1,service2,service3")) + } + ) { + Analytics.shared.trackPageView( + comScore: .init(name: "name"), + commandersAct: .init( + name: "name", + type: "type", + levels: [ + "level_1", + "level_2", + "level_3", + "level_4", + "level_5", + "level_6", + "level_7", + "level_8" + ] + ) + ) + } + } + + func testBlankTitle() { + guard nimbleThrowAssertionsAvailable() else { return } + expect(Analytics.shared.trackPageView( + comScore: .init(name: "name"), + commandersAct: .init(name: " ", type: "type") + )).to(throwAssertion()) + } + + func testBlankType() { + guard nimbleThrowAssertionsAvailable() else { return } + expect(Analytics.shared.trackPageView( + comScore: .init(name: "name"), + commandersAct: .init(name: "name", type: " ") + )).to(throwAssertion()) + } + + func testBlankLevels() { + expectAtLeastHits( + page_view { labels in + expect(labels.page_type).to(equal("type")) + expect(labels.page_name).to(equal("name")) + expect(labels.navigation_level_1).to(beNil()) + expect(labels.navigation_level_2).to(beNil()) + expect(labels.navigation_level_3).to(beNil()) + expect(labels.navigation_level_4).to(beNil()) + expect(labels.navigation_level_5).to(beNil()) + expect(labels.navigation_level_6).to(beNil()) + expect(labels.navigation_level_7).to(beNil()) + expect(labels.navigation_level_8).to(beNil()) + } + ) { + Analytics.shared.trackPageView( + comScore: .init(name: "name"), + commandersAct: .init( + name: "name", + type: "type", + levels: [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ] + ) + ) + } + } + + func testCustomLabels() { + expectAtLeastHits( + page_view { labels in + // Use `media_player_display`, a media-only key, so that its value can be parsed. + expect(labels.media_player_display).to(equal("value")) + } + ) { + Analytics.shared.trackPageView( + comScore: .init(name: "name"), + commandersAct: .init( + name: "name", + type: "type", + labels: ["media_player_display": "value"] + ) + ) + } + } + + func testGlobals() { + expectAtLeastHits( + page_view { labels in + expect(labels.consent_services).to(equal("service1,service2,service3")) + } + ) { + Analytics.shared.trackPageView( + comScore: .init(name: "name"), + commandersAct: .init(name: "name", type: "type") + ) + } + } + + func testLabelsForbiddenOverrides() { + expectAtLeastHits( + page_view { labels in + expect(labels.page_name).to(equal("name")) + expect(labels.consent_services).to(equal("service1,service2,service3")) + } + ) { + Analytics.shared.trackPageView( + comScore: .init(name: "name"), + commandersAct: .init( + name: "name", + type: "type", + labels: [ + "page_name": "overridden_title", + "consent_services": "service42" + ] + ) + ) + } + } +} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActTestCase.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActTestCase.swift new file mode 100644 index 00000000..b43edc44 --- /dev/null +++ b/Tests/AnalyticsTests/CommandersAct/CommandersActTestCase.swift @@ -0,0 +1,74 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import Dispatch +import PillarboxCircumspect + +class CommandersActTestCase: TestCase {} + +extension CommandersActTestCase { + /// Collects hits emitted by Commanders Act during some time interval and matches them against expectations. + func expectHits( + _ expectations: CommandersActHitExpectation..., + during interval: DispatchTimeInterval = .seconds(20), + file: StaticString = #file, + line: UInt = #line, + while executing: (() -> Void)? = nil + ) { + AnalyticsListener.captureCommandersActHits { publisher in + expectPublished( + values: expectations, + from: publisher, + to: CommandersActHitExpectation.match, + during: interval, + file: file, + line: line, + while: executing + ) + } + } + + /// Expects hits emitted by Commanders Act during some time interval and matches them against expectations. + func expectAtLeastHits( + _ expectations: CommandersActHitExpectation..., + timeout: DispatchTimeInterval = .seconds(20), + file: StaticString = #file, + line: UInt = #line, + while executing: (() -> Void)? = nil + ) { + AnalyticsListener.captureCommandersActHits { publisher in + expectAtLeastPublished( + values: expectations, + from: publisher, + to: CommandersActHitExpectation.match, + timeout: timeout, + file: file, + line: line, + while: executing + ) + } + } + + /// Expects no hits emitted by Commanders Act during some time interval. + func expectNoHits( + during interval: DispatchTimeInterval = .seconds(20), + file: StaticString = #file, + line: UInt = #line, + while executing: (() -> Void)? = nil + ) { + AnalyticsListener.captureCommandersActHits { publisher in + expectNothingPublished( + from: publisher, + during: interval, + file: file, + line: line, + while: executing + ) + } + } +} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerDvrPropertiesTests.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerDvrPropertiesTests.swift new file mode 100644 index 00000000..529456ec --- /dev/null +++ b/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerDvrPropertiesTests.swift @@ -0,0 +1,126 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import CoreMedia +import Nimble +import PillarboxCircumspect +import PillarboxPlayer +import PillarboxStreams + +final class CommandersActTrackerDvrPropertiesTests: CommandersActTestCase { + func testOnDemand() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + expectAtLeastHits( + play { labels in + expect(labels.media_timeshift).to(beNil()) + } + ) { + player.play() + } + } + + func testLive() { + let player = Player(item: .simple( + url: Stream.live.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + expectAtLeastHits( + play { labels in + expect(labels.media_timeshift).to(equal(0)) + } + ) { + player.play() + } + } + + func testDvrAtLiveEdge() { + let player = Player(item: .simple( + url: Stream.dvr.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + expectAtLeastHits( + play { labels in + expect(labels.media_timeshift).to(equal(0)) + } + ) { + player.play() + } + } + + func testDvrAwayFromLiveEdge() { + let player = Player(item: .simple( + url: Stream.dvr.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + player.play() + expect(player.playbackState).toEventually(equal(.playing)) + + expectAtLeastHits( + seek { labels in + expect(labels.media_timeshift).to(equal(0)) + }, + play { labels in + expect(labels.media_timeshift).to(beCloseTo(4, within: 2)) + } + ) { + player.seek(at(player.time() - CMTime(value: 4, timescale: 1))) + } + } + + func testDestroyPlayerDuringPlayback() { + var player: Player? = Player(item: .simple( + url: Stream.dvr.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + player?.play() + expect(player?.playbackState).toEventually(equal(.playing)) + + player?.pause() + wait(for: .seconds(2)) + + expectAtLeastHits( + stop { labels in + expect(labels.media_position).to(equal(0)) + } + ) { + player = nil + } + } + + func testDestroyPlayerWhileInitiallyPaused() { + var player: Player? = Player(item: .simple( + url: Stream.dvr.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + expect(player?.playbackState).toEventually(equal(.paused)) + + expectNoHits(during: .seconds(5)) { + player = nil + } + } +} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerMetadataTests.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerMetadataTests.swift new file mode 100644 index 00000000..4735135e --- /dev/null +++ b/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerMetadataTests.swift @@ -0,0 +1,143 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import Nimble +import PillarboxPlayer +import PillarboxStreams + +final class CommandersActTrackerMetadataTests: CommandersActTestCase { + func testWhenInitialized() { + var player: Player? + expectAtLeastHits( + play { labels in + expect(labels.media_player_display).to(equal("Pillarbox")) + expect(labels.media_player_version).to(equal(PackageInfo.version)) + expect(labels.media_volume).notTo(beNil()) + expect(labels.media_title).to(equal("name")) + expect(labels.media_audio_track).to(equal("UND")) + expect(labels.consent_services).to(equal("service1,service2,service3")) + } + ) { + player = Player(item: .simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + player?.setDesiredPlaybackSpeed(0.5) + player?.play() + } + } + + func testWhenDestroyed() { + var player: Player? = Player(item: .simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + player?.play() + expect(player?.playbackState).toEventually(equal(.playing)) + + expectAtLeastHits( + stop { labels in + expect(labels.media_player_display).to(equal("Pillarbox")) + expect(labels.media_player_version).to(equal(PackageInfo.version)) + expect(labels.media_volume).notTo(beNil()) + expect(labels.media_title).to(equal("name")) + expect(labels.media_audio_track).to(equal("UND")) + } + ) { + player = nil + } + } + + func testMuted() { + var player: Player? + expectAtLeastHits( + play { labels in + expect(labels.media_volume).to(equal(0)) + } + ) { + player = Player(item: .simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + player?.isMuted = true + player?.play() + } + } + + func testAudioTrack() { + let player = Player(item: .simple( + url: Stream.onDemandWithOptions.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + player.setMediaSelection(preferredLanguages: ["fr"], for: .audible) + player.play() + expect(player.playbackState).toEventually(equal(.playing)) + + expectAtLeastHits( + pause { labels in + expect(labels.media_audio_track).to(equal("FR")) + } + ) { + player.pause() + } + } + + func testSubtitlesOff() { + let player = Player(item: .simple( + url: Stream.onDemandWithOptions.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + player.play() + expect(player.playbackState).toEventually(equal(.playing)) + player.select(mediaOption: .off, for: .legible) + expect(player.currentMediaOption(for: .legible)).toEventually(equal(.off)) + + expectAtLeastHits( + pause { labels in + expect(labels.media_subtitles_on).to(beFalse()) + } + ) { + player.pause() + } + } + + func testSubtitlesOn() { + let player = Player(item: .simple( + url: Stream.onDemandWithOptions.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + player.setMediaSelection(preferredLanguages: ["fr"], for: .legible) + player.play() + expect(player.playbackState).toEventually(equal(.playing)) + + expectAtLeastHits( + pause { labels in + expect(labels.media_subtitles_on).to(beTrue()) + expect(labels.media_subtitle_selection).to(equal("FR")) + } + ) { + player.pause() + } + } +} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerPositionTests.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerPositionTests.swift new file mode 100644 index 00000000..5c870d27 --- /dev/null +++ b/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerPositionTests.swift @@ -0,0 +1,139 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import CoreMedia +import Nimble +import PillarboxCircumspect +import PillarboxPlayer +import PillarboxStreams + +final class CommandersActTrackerPositionTests: CommandersActTestCase { + func testLivePlayback() { + let player = Player(item: .simple( + url: Stream.live.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + player.play() + expect(player.playbackState).toEventually(equal(.playing)) + wait(for: .seconds(2)) + + expectAtLeastHits( + pause { labels in + expect(labels.media_position).to(equal(2)) + } + ) { + player.pause() + } + } + + func testDvrPlayback() { + let player = Player(item: .simple( + url: Stream.dvr.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + player.play() + expect(player.playbackState).toEventually(equal(.playing)) + wait(for: .seconds(2)) + + expectAtLeastHits( + pause { labels in + expect(labels.media_position).to(equal(2)) + } + ) { + player.pause() + } + } + + func testSeekDuringDvrPlayback() { + let player = Player(item: .simple( + url: Stream.dvr.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + player.play() + expect(player.playbackState).toEventually(equal(.playing)) + + expectAtLeastHits( + seek { labels in + expect(labels.media_position).to(equal(0)) + }, + play { labels in + expect(labels.media_position).to(equal(0)) + } + ) { + player.seek(at(.init(value: 7, timescale: 1))) + } + } + + func testDestroyDuringLivePlayback() { + var player: Player? = Player(item: .simple( + url: Stream.live.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + player?.play() + expect(player?.playbackState).toEventually(equal(.playing)) + wait(for: .seconds(2)) + + expectAtLeastHits( + stop { labels in + expect(labels.media_position).to(equal(2)) + } + ) { + player = nil + } + } + + func testDestroyDuringDvrPlayback() { + var player: Player? = Player(item: .simple( + url: Stream.dvr.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + player?.play() + expect(player?.playbackState).toEventually(equal(.playing)) + wait(for: .seconds(2)) + + expectAtLeastHits( + stop { labels in + expect(labels.media_position).to(equal(2)) + } + ) { + player = nil + } + } + + func testOnDemandStartAtGivenPosition() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ], + configuration: .init(position: at(.init(value: 100, timescale: 1))) + )) + expectAtLeastHits( + play { labels in + expect(labels.media_position).to(equal(100)) + } + ) { + player.play() + } + } +} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerSeekTests.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerSeekTests.swift new file mode 100644 index 00000000..599d8aa6 --- /dev/null +++ b/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerSeekTests.swift @@ -0,0 +1,83 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import Nimble +import PillarboxPlayer +import PillarboxStreams + +final class CommandersActTrackerSeekTests: CommandersActTestCase { + func testSeekWhilePlaying() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + player.play() + expect(player.playbackState).toEventually(equal(.playing)) + + expectAtLeastHits( + seek { labels in + expect(labels.media_position).to(equal(0)) + }, + play { labels in + expect(labels.media_position).to(equal(7)) + } + ) { + player.seek(at(.init(value: 7, timescale: 1))) + } + } + + func testSeekWhilePaused() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + expect(player.playbackState).toEventually(equal(.paused)) + + expectNoHits(during: .seconds(2)) { + player.seek(at(.init(value: 7, timescale: 1))) + } + + expectAtLeastHits( + play { labels in + expect(labels.media_position).to(equal(7)) + } + ) { + player.play() + } + } + + func testDestroyPlayerWhileSeeking() { + var player: Player? = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + player?.play() + expect(player?.playbackState).toEventually(equal(.playing)) + + expectAtLeastHits( + seek { labels in + expect(labels.media_position).to(equal(0)) + }, + stop { labels in + expect(labels.media_position).to(equal(7)) + } + ) { + player?.seek(at(.init(value: 7, timescale: 1))) + player = nil + } + } +} diff --git a/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerTests.swift b/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerTests.swift new file mode 100644 index 00000000..b5e47067 --- /dev/null +++ b/Tests/AnalyticsTests/CommandersAct/CommandersActTrackerTests.swift @@ -0,0 +1,208 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxAnalytics + +import Nimble +import PillarboxPlayer +import PillarboxStreams + +final class CommandersActTrackerTests: CommandersActTestCase { + func testInitiallyPlaying() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + expectAtLeastHits( + play { labels in + expect(labels.media_position).to(equal(0)) + } + ) { + player.play() + } + } + + func testInitiallyPaused() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in [:] } + ] + )) + expectNoHits(during: .seconds(2)) { + player.pause() + } + } + + func testPauseDuringPlayback() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + player.play() + expect(player.time().seconds).toEventually(beGreaterThan(1)) + + expectAtLeastHits( + pause { labels in + expect(labels.media_position).to(equal(1)) + } + ) { + player.pause() + } + } + + func testPlaybackEnd() { + let player = Player(item: .simple( + url: Stream.mediumOnDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + expectAtLeastHits( + play(), + eof { labels in + expect(labels.media_position).to(equal(Int(Stream.mediumOnDemand.duration.seconds))) + } + ) { + player.play() + } + } + + func testDestroyPlayerDuringPlayback() { + var player: Player? = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + + player?.play() + expect(player?.time().seconds).toEventually(beGreaterThan(5)) + + expectAtLeastHits( + stop { labels in + expect(labels.media_position).to(equal(5)) + } + ) { + player = nil + } + } + + func testDestroyPlayerDuringPlaybackAtNonStandardPlaybackSpeed() { + var player: Player? = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in .test } + ] + )) + player?.setDesiredPlaybackSpeed(2) + + player?.play() + expect(player?.time().seconds).toEventually(beGreaterThan(2)) + + expectAtLeastHits( + stop { labels in + expect(labels.media_position).to(equal(2)) + } + ) { + player = nil + } + } + + func testDestroyPlayerAfterPlayback() { + var player: Player? = Player(item: .simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in [:] } + ] + )) + + expectAtLeastHits(play(), eof()) { + player?.play() + } + + expectNoHits(during: .seconds(2)) { + player = nil + } + } + + func testFailure() { + let player = Player(item: .simple( + url: Stream.unavailable.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in [:] } + ] + )) + expectNoHits(during: .seconds(3)) { + player.play() + } + } + + func testDisableTrackingDuringPlayback() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in [:] } + ] + )) + + player.play() + expect(player.time().seconds).toEventually(beGreaterThan(5)) + + expectAtLeastHits(stop()) { + player.isTrackingEnabled = false + } + } + + func testEnableTrackingDuringPlayback() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in [:] } + ] + )) + + player.isTrackingEnabled = false + + expectNoHits(during: .seconds(2)) { + player.play() + } + + expectAtLeastHits(play()) { + player.isTrackingEnabled = true + } + } + + func testEnableTrackingAgainWhilePaused() { + let player = Player() + player.append(.simple( + url: Stream.onDemand.url, + trackerAdapters: [ + CommandersActTracker.adapter { _ in [:] } + ] + )) + + expectAtLeastHits(play()) { + player.play() + } + expectAtLeastHits(stop()) { + player.isTrackingEnabled = false + } + + player.pause() + expect(player.playbackState).toEventually(equal(.paused)) + + expectAtLeastHits(play()) { + player.isTrackingEnabled = true + player.play() + } + } +} diff --git a/Tests/AnalyticsTests/Extensions/Dictionary.swift b/Tests/AnalyticsTests/Extensions/Dictionary.swift new file mode 100644 index 00000000..7a81bfee --- /dev/null +++ b/Tests/AnalyticsTests/Extensions/Dictionary.swift @@ -0,0 +1,9 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +extension [String: String] { + static let test = ["media_title": "name"] +} diff --git a/Tests/AnalyticsTests/TestCase.swift b/Tests/AnalyticsTests/TestCase.swift new file mode 100644 index 00000000..a6d6572e --- /dev/null +++ b/Tests/AnalyticsTests/TestCase.swift @@ -0,0 +1,45 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import Nimble +import PillarboxAnalytics +import XCTest + +private final class TestCaseDataSource: AnalyticsDataSource { + var comScoreGlobals: ComScoreGlobals { + .init(consent: .unknown, labels: [:]) + } + + var commandersActGlobals: CommandersActGlobals { + .init(consentServices: ["service1", "service2", "service3"], labels: [:]) + } +} + +/// A simple test suite with more tolerant Nimble settings. Beware that `toAlways` and `toNever` expectations appearing +/// in tests will use the same value by default and should likely always provide an explicit `until` parameter. +class TestCase: XCTestCase { + private static let dataSource = TestCaseDataSource() + + override class func setUp() { + PollingDefaults.timeout = .seconds(20) + PollingDefaults.pollInterval = .milliseconds(100) + try? Analytics.shared.start( + with: .init(vendor: .SRG, sourceKey: .developmentSourceKey, appSiteName: "site"), + dataSource: dataSource + ) + } + + override class func tearDown() { + PollingDefaults.timeout = .seconds(1) + PollingDefaults.pollInterval = .milliseconds(10) + } + + override func setUp() { + waitUntil { done in + AnalyticsListener.start(completion: done) + } + } +} diff --git a/Tests/CircumspectTests/ComparatorTests.swift b/Tests/CircumspectTests/ComparatorTests.swift new file mode 100644 index 00000000..d3124016 --- /dev/null +++ b/Tests/CircumspectTests/ComparatorTests.swift @@ -0,0 +1,20 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCircumspect + +import Nimble +import XCTest + +final class ComparatorTests: XCTestCase { + func testClose() { + expect(beClose(within: 0.1)(0.3, 0.3)).to(beTrue()) + } + + func testDistant() { + expect(beClose(within: 0.1)(0.3, 0.5)).to(beFalse()) + } +} diff --git a/Tests/CircumspectTests/Counter.swift b/Tests/CircumspectTests/Counter.swift new file mode 100644 index 00000000..5caae25d --- /dev/null +++ b/Tests/CircumspectTests/Counter.swift @@ -0,0 +1,20 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import Combine +import Foundation + +final class Counter: ObservableObject { + @Published var count = 0 + + init() { + Timer.publish(every: 0.2, on: .main, in: .common) + .autoconnect() + .map { _ in 1 } + .scan(0) { $0 + $1 } + .assign(to: &$count) + } +} diff --git a/Tests/CircumspectTests/Expectations/ExpectAtLeastPublishedTests.swift b/Tests/CircumspectTests/Expectations/ExpectAtLeastPublishedTests.swift new file mode 100644 index 00000000..28455cf7 --- /dev/null +++ b/Tests/CircumspectTests/Expectations/ExpectAtLeastPublishedTests.swift @@ -0,0 +1,64 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCircumspect + +import Combine +import XCTest + +final class ExpectAtLeastPublishedTests: XCTestCase { + func testExpectAtLeastEqualPublishedValues() { + expectAtLeastEqualPublished( + values: [1, 2, 3, 4, 5], + from: [1, 2, 3, 4, 5].publisher + ) + } + + func testExpectAtLeastEqualPublishedValuesWhileExecuting() { + let subject = PassthroughSubject() + expectAtLeastEqualPublished( + values: [4, 7], + from: subject + ) { + subject.send(4) + subject.send(7) + subject.send(completion: .finished) + } + } + + func testExpectAtLeastEqualPublishedNextValues() { + expectAtLeastEqualPublishedNext( + values: [2, 3, 4, 5], + from: [1, 2, 3, 4, 5].publisher + ) + } + + func testExpectAtLeastEqualPublishedNextValuesWhileExecuting() { + let subject = PassthroughSubject() + expectAtLeastEqualPublishedNext( + values: [7, 8], + from: subject + ) { + subject.send(4) + subject.send(7) + subject.send(8) + subject.send(completion: .finished) + } + } + + func testExpectAtLeastEqualFollowingExpectEqual() { + let publisher = PassthroughSubject() + expectEqualPublished(values: [1, 2], from: publisher, during: .milliseconds(100)) { + publisher.send(1) + publisher.send(2) + } + expectAtLeastEqualPublished(values: [3, 4, 5], from: publisher) { + publisher.send(3) + publisher.send(4) + publisher.send(5) + } + } +} diff --git a/Tests/CircumspectTests/Expectations/ExpectNothingPublishedTests.swift b/Tests/CircumspectTests/Expectations/ExpectNothingPublishedTests.swift new file mode 100644 index 00000000..71d7d9cd --- /dev/null +++ b/Tests/CircumspectTests/Expectations/ExpectNothingPublishedTests.swift @@ -0,0 +1,24 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCircumspect + +import Combine +import XCTest + +final class ExpectNothingPublishedTests: XCTestCase { + func testExpectNothingPublished() { + let subject = PassthroughSubject() + expectNothingPublished(from: subject, during: .seconds(1)) + } + + func testExpectNothingPublishedNext() { + let subject = PassthroughSubject() + expectNothingPublishedNext(from: subject, during: .seconds(1)) { + subject.send(4) + } + } +} diff --git a/Tests/CircumspectTests/Expectations/ExpectNotificationsTests.swift b/Tests/CircumspectTests/Expectations/ExpectNotificationsTests.swift new file mode 100644 index 00000000..302c4227 --- /dev/null +++ b/Tests/CircumspectTests/Expectations/ExpectNotificationsTests.swift @@ -0,0 +1,34 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCircumspect + +import XCTest + +final class ExpectNotificationsTests: XCTestCase { + func testExpectAtLeastReceivedNotifications() { + expectAtLeastReceived( + notifications: [ + Notification(name: .testNotification, object: self) + ], + for: [.testNotification] + ) { + NotificationCenter.default.post(name: .testNotification, object: self) + } + } + + func testExpectReceivedNotificationsDuringInterval() { + expectReceived( + notifications: [ + Notification(name: .testNotification, object: self) + ], + for: [.testNotification], + during: .milliseconds(500) + ) { + NotificationCenter.default.post(name: .testNotification, object: self) + } + } +} diff --git a/Tests/CircumspectTests/Expectations/ExpectOnlyPublishedTests.swift b/Tests/CircumspectTests/Expectations/ExpectOnlyPublishedTests.swift new file mode 100644 index 00000000..2fb55a85 --- /dev/null +++ b/Tests/CircumspectTests/Expectations/ExpectOnlyPublishedTests.swift @@ -0,0 +1,51 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCircumspect + +import Combine +import XCTest + +final class ExpectOnlyPublishedTests: XCTestCase { + func testExpectOnlyEqualPublishedValues() { + expectOnlyEqualPublished( + values: [1, 2, 3, 4, 5], + from: [1, 2, 3, 4, 5].publisher + ) + } + + func testExpectOnlyEqualPublishedValuesWhileExecuting() { + let subject = PassthroughSubject() + expectOnlyEqualPublished( + values: [4, 7], + from: subject + ) { + subject.send(4) + subject.send(7) + subject.send(completion: .finished) + } + } + + func testExpectOnlyEqualPublishedNextValues() { + expectOnlyEqualPublishedNext( + values: [2, 3, 4, 5], + from: [1, 2, 3, 4, 5].publisher + ) + } + + func testExpectOnlyEqualPublishedNextValuesWhileExecuting() { + let subject = PassthroughSubject() + expectOnlyEqualPublishedNext( + values: [7, 8], + from: subject + ) { + subject.send(4) + subject.send(7) + subject.send(8) + subject.send(completion: .finished) + } + } +} diff --git a/Tests/CircumspectTests/Expectations/ExpectPublishedTests.swift b/Tests/CircumspectTests/Expectations/ExpectPublishedTests.swift new file mode 100644 index 00000000..19460c7f --- /dev/null +++ b/Tests/CircumspectTests/Expectations/ExpectPublishedTests.swift @@ -0,0 +1,56 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCircumspect + +import Combine +import XCTest + +final class ExpectPublishedTests: XCTestCase { + func testExpectEqualPublishedValuesDuringInterval() { + let counter = Counter() + expectEqualPublished( + values: [0, 1, 2], + from: counter.$count, + during: .milliseconds(500) + ) + } + + func testExpectEqualPublishedValuesDuringIntervalWhileExecuting() { + let subject = PassthroughSubject() + expectEqualPublished( + values: [4, 7, 8], + from: subject, + during: .milliseconds(500) + ) { + subject.send(4) + subject.send(7) + subject.send(8) + } + } + + func testExpectEqualPublishedNextValuesDuringInterval() { + let counter = Counter() + expectEqualPublishedNext( + values: [1, 2], + from: counter.$count, + during: .milliseconds(500) + ) + } + + func testExpectEqualPublishedNextValuesDuringIntervalWhileExecuting() { + let subject = PassthroughSubject() + expectEqualPublishedNext( + values: [7, 8], + from: subject, + during: .milliseconds(500) + ) { + subject.send(4) + subject.send(7) + subject.send(8) + } + } +} diff --git a/Tests/CircumspectTests/Expectations/ExpectResultTests.swift b/Tests/CircumspectTests/Expectations/ExpectResultTests.swift new file mode 100644 index 00000000..bede2084 --- /dev/null +++ b/Tests/CircumspectTests/Expectations/ExpectResultTests.swift @@ -0,0 +1,24 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCircumspect + +import Combine +import XCTest + +final class ExpectResultTests: XCTestCase { + func testExpectSuccess() { + expectSuccess(from: Empty()) + } + + func testExpectFailure() { + expectFailure(from: Fail(error: StructError())) + } + + func testExpectFailureWithError() { + expectFailure(StructError(), from: Fail(error: StructError())) + } +} diff --git a/Tests/CircumspectTests/Expectations/ExpectValueTests.swift b/Tests/CircumspectTests/Expectations/ExpectValueTests.swift new file mode 100644 index 00000000..9fe7cdb0 --- /dev/null +++ b/Tests/CircumspectTests/Expectations/ExpectValueTests.swift @@ -0,0 +1,40 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCircumspect + +import Combine +import XCTest + +private class Object: ObservableObject { + @Published var value = 0 +} + +final class ExpectValueTests: XCTestCase { + func testSingleValue() { + expectValue(from: Just(1)) + } + + func testMultipleValues() { + expectValue(from: [1, 2, 3].publisher) + } + + func testSingleChange() { + let object = Object() + expectChange(from: object) { + object.value = 1 + } + } + + func testMultipleChanges() { + let object = Object() + expectChange(from: object) { + object.value = 1 + object.value = 2 + object.value = 3 + } + } +} diff --git a/Tests/CircumspectTests/ObservableObjectTests.swift b/Tests/CircumspectTests/ObservableObjectTests.swift new file mode 100644 index 00000000..5c23cfc0 --- /dev/null +++ b/Tests/CircumspectTests/ObservableObjectTests.swift @@ -0,0 +1,70 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCircumspect + +import Nimble +import XCTest + +private class TestObservableObject: ObservableObject { + @Published var publishedProperty1 = 1 + @Published var publishedProperty2 = "a" + + var nonPublishedProperty: Int { + publishedProperty1 * 2 + } +} + +final class ObservableObjectTests: XCTestCase { + func testNonPublishedPropertyInitialValue() { + let object = TestObservableObject() + expectAtLeastEqualPublished( + values: [2], + from: object.changePublisher(at: \.nonPublishedProperty) + ) + } + + func testPublishedPropertyInitialValue() { + let object = TestObservableObject() + expectAtLeastEqualPublished( + values: [1], + from: object.changePublisher(at: \.publishedProperty1) + ) + } + + func testNonPublishedPropertyChanges() { + let object = TestObservableObject() + expectAtLeastEqualPublished( + values: [2, 8, 8], + from: object.changePublisher(at: \.nonPublishedProperty) + ) { + object.publishedProperty1 = 4 + object.publishedProperty2 = "b" + } + } + + func testPublishedPropertyChanges() { + let object = TestObservableObject() + expectAtLeastEqualPublished( + values: [1, 3, 3, 3], + from: object.changePublisher(at: \.publishedProperty1) + ) { + object.publishedProperty1 = 2 + object.publishedProperty1 = 3 + object.publishedProperty2 = "b" + } + } + + func testDeallocation() { + var object: TestObservableObject? = TestObservableObject() + _ = object?.changePublisher(at: \.nonPublishedProperty) + weak var weakObject = object + autoreleasepool { + object = nil + } + expect(weakObject).to(beNil()) + } +} diff --git a/Tests/CircumspectTests/PublishersTests.swift b/Tests/CircumspectTests/PublishersTests.swift new file mode 100644 index 00000000..4878f69a --- /dev/null +++ b/Tests/CircumspectTests/PublishersTests.swift @@ -0,0 +1,114 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCircumspect + +import Combine +import Nimble +import XCTest + +final class PublisherTests: XCTestCase { + func testWaitForSuccessResult() { + let values = try? waitForResult(from: [1, 2, 3, 4, 5].publisher).get() + expect(values).to(equal([1, 2, 3, 4, 5])) + } + + func testWaitForSuccessResultWhileExecuting() { + let subject = PassthroughSubject() + let values = try? waitForResult(from: subject) { + subject.send(4) + subject.send(7) + subject.send(completion: .finished) + }.get() + expect(values).to(equal([4, 7])) + } + + func testWaitForFailureResult() { + let values = try? waitForResult(from: Fail(error: StructError())).get() + expect(values).to(beNil()) + } + + func testWaitForOutput() throws { + let values = try waitForOutput(from: [1, 2, 3].publisher) + expect(values).to(equal([1, 2, 3])) + } + + func testWaitForOutputWhileExecuting() throws { + let subject = PassthroughSubject() + let values = try waitForOutput(from: subject) { + subject.send(4) + subject.send(7) + subject.send(completion: .finished) + } + expect(values).to(equal([4, 7])) + } + + func testWaitForSingleOutput() throws { + let value = try waitForSingleOutput(from: [1].publisher) + expect(value).to(equal(1)) + } + + func testWaitForSingleOutputWhileExecuting() throws { + let subject = PassthroughSubject() + let value = try waitForSingleOutput(from: subject) { + subject.send(4) + subject.send(completion: .finished) + } + expect(value).to(equal(4)) + } + + func testWaitForFailure() throws { + let error = try waitForFailure(from: Fail(error: StructError())) + expect(error).notTo(beNil()) + } + + func testWaitForFailureWhileExecuting() throws { + let subject = PassthroughSubject() + let error = try waitForFailure(from: Fail(error: StructError())) { + subject.send(4) + subject.send(7) + subject.send(completion: .failure(StructError())) + } + expect(error).notTo(beNil()) + } + + func testCollectOutput() { + let counter = Counter() + let values = collectOutput(from: counter.$count, during: .milliseconds(500)) + expect(values).to(equal([0, 1, 2])) + } + + func testCollectOutputWhileExecuting() { + let subject = PassthroughSubject() + let values = collectOutput(from: subject, during: .milliseconds(500)) { + subject.send(4) + subject.send(7) + } + expect(values).to(equal([4, 7])) + } + + func testCollectOutputImmediately() { + let values = collectOutput( + from: [1, 2, 3, 4, 5].publisher, + during: .never + ) + expect(values).to(equal([1, 2, 3, 4, 5])) + } + + func testCollectFirst() throws { + let values = try waitForOutput( + from: [1, 2, 3, 4, 5].publisher.collectFirst(3) + ).flatMap { $0 } + expect(values).to(equal([1, 2, 3])) + } + + func testCollectNext() throws { + let values = try waitForOutput( + from: [1, 2, 3, 4, 5].publisher.collectNext(3) + ).flatMap { $0 } + expect(values).to(equal([2, 3, 4])) + } +} diff --git a/Tests/CircumspectTests/SimilarityTests.swift b/Tests/CircumspectTests/SimilarityTests.swift new file mode 100644 index 00000000..b359d9e7 --- /dev/null +++ b/Tests/CircumspectTests/SimilarityTests.swift @@ -0,0 +1,55 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCircumspect + +import Nimble +import XCTest + +final class SimilarityTests: XCTestCase { + func testOperatorForInstances() { + expect(NamedPerson(name: "Alice") ~~ NamedPerson(name: "alice")).to(beTrue()) + expect(NamedPerson(name: "Alice") ~~ NamedPerson(name: "bob")).to(beFalse()) + } + + func testOperatorForOptionals() { + let alice1: NamedPerson? = NamedPerson(name: "Alice") + let alice2: NamedPerson? = NamedPerson(name: "alice") + let bob = NamedPerson(name: "Bob") + expect(alice1 ~~ alice2).to(beTrue()) + expect(alice1 ~~ bob).to(beFalse()) + } + + func testOperatorForArrays() { + let array1 = [NamedPerson(name: "Alice"), NamedPerson(name: "Bob")] + let array2 = [NamedPerson(name: "alice"), NamedPerson(name: "bob")] + let array3 = [NamedPerson(name: "bob"), NamedPerson(name: "alice")] + expect(array1 ~~ array2).to(beTrue()) + expect(array1 ~~ array3).to(beFalse()) + } + + func testBeSimilarForInstances() { + expect(NamedPerson(name: "Alice")).to(beSimilarTo(NamedPerson(name: "alice"))) + expect(NamedPerson(name: "Alice")).notTo(beSimilarTo(NamedPerson(name: "bob"))) + } + + func testBeSimilarForOptionals() { + let alice1: NamedPerson? = NamedPerson(name: "Alice") + let alice2: NamedPerson? = NamedPerson(name: "alice") + let bob = NamedPerson(name: "Bob") + expect(alice1).to(beSimilarTo(alice2)) + expect(alice1).notTo(beNil()) + expect(alice1).notTo(beSimilarTo(bob)) + } + + func testBeSimilarForArrays() { + let array1 = [NamedPerson(name: "Alice"), NamedPerson(name: "Bob")] + let array2 = [NamedPerson(name: "alice"), NamedPerson(name: "bob")] + let array3 = [NamedPerson(name: "bob"), NamedPerson(name: "alice")] + expect(array1).to(beSimilarTo(array2)) + expect(array1).notTo(beSimilarTo(array3)) + } +} diff --git a/Tests/CircumspectTests/TimeIntervalTests.swift b/Tests/CircumspectTests/TimeIntervalTests.swift new file mode 100644 index 00000000..4f9f082f --- /dev/null +++ b/Tests/CircumspectTests/TimeIntervalTests.swift @@ -0,0 +1,20 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCircumspect + +import Dispatch +import Nimble +import XCTest + +final class TimeIntervalTests: XCTestCase { + func testDoubleConversion() { + expect(DispatchTimeInterval.seconds(1).double()).to(equal(1)) + expect(DispatchTimeInterval.milliseconds(1_000).double()).to(equal(1)) + expect(DispatchTimeInterval.microseconds(1_000_000).double()).to(equal(1)) + expect(DispatchTimeInterval.nanoseconds(1_000_000_000).double()).to(equal(1)) + } +} diff --git a/Tests/CircumspectTests/Tools.swift b/Tests/CircumspectTests/Tools.swift new file mode 100644 index 00000000..c35423dd --- /dev/null +++ b/Tests/CircumspectTests/Tools.swift @@ -0,0 +1,22 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import Foundation +import PillarboxCircumspect + +struct StructError: Error {} + +struct NamedPerson: Similar { + let name: String + + static func ~~ (lhs: Self, rhs: Self) -> Bool { + lhs.name.localizedCaseInsensitiveContains(rhs.name) + } +} + +extension Notification.Name { + static let testNotification = Notification.Name("TestNotification") +} diff --git a/Tests/CoreBusinessTests/AkamaiURLCodingTests.swift b/Tests/CoreBusinessTests/AkamaiURLCodingTests.swift new file mode 100644 index 00000000..ab3a58ab --- /dev/null +++ b/Tests/CoreBusinessTests/AkamaiURLCodingTests.swift @@ -0,0 +1,82 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCoreBusiness + +import Nimble +import XCTest + +final class AkamaiURLCodingTests: XCTestCase { + private static let uuid = "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" + + func testEncoding() { + expect(AkamaiURLCoding.encodeUrl( + URL(string: "http://www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, + id: UUID(uuidString: Self.uuid)! + )) + .to(equal(URL(string: "akamai+E621E1F8-C36C-495A-93FC-0C247A3E6E5F+http://www.server.com/playlist.m3u8?param1=value1¶m2=value2"))) + + expect(AkamaiURLCoding.encodeUrl( + URL(string: "https://www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, + id: UUID(uuidString: Self.uuid)! + )) + .to(equal(URL(string: "akamai+E621E1F8-C36C-495A-93FC-0C247A3E6E5F+https://www.server.com/playlist.m3u8?param1=value1¶m2=value2"))) + } + + func testFailedEncoding() { + expect(AkamaiURLCoding.encodeUrl( + URL(string: "//www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, + id: UUID(uuidString: Self.uuid)! + )) + .to(equal(URL(string: "//www.server.com/playlist.m3u8?param1=value1¶m2=value2"))) + } + + func testDecoding() { + expect(AkamaiURLCoding.decodeUrl( + URL(string: "akamai+E621E1F8-C36C-495A-93FC-0C247A3E6E5F+http://www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, + id: UUID(uuidString: Self.uuid)! + )) + .to(equal(URL(string: "http://www.server.com/playlist.m3u8?param1=value1¶m2=value2"))) + + expect(AkamaiURLCoding.decodeUrl( + URL(string: "akamai+E621E1F8-C36C-495A-93FC-0C247A3E6E5F+https://www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, + id: UUID(uuidString: Self.uuid)! + )) + .to(equal(URL(string: "https://www.server.com/playlist.m3u8?param1=value1¶m2=value2"))) + } + + func testFailedDecoding() { + expect(AkamaiURLCoding.decodeUrl( + URL(string: "http://www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, + id: UUID(uuidString: Self.uuid)! + )) + .to(beNil()) + + expect(AkamaiURLCoding.decodeUrl( + URL(string: "https://www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, + id: UUID(uuidString: Self.uuid)! + )) + .to(beNil()) + + expect(AkamaiURLCoding.decodeUrl( + URL(string: "custom://www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, + id: UUID(uuidString: Self.uuid)! + )) + .to(beNil()) + + expect(AkamaiURLCoding.decodeUrl( + URL(string: "//www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, + id: UUID(uuidString: Self.uuid)! + )) + .to(beNil()) + + expect(AkamaiURLCoding.decodeUrl( + URL(string: "akamai+1111111-1111-1111-1111-111111111111+https://www.server.com/playlist.m3u8?param1=value1¶m2=value2")!, + id: UUID(uuidString: Self.uuid)! + )) + .to(beNil()) + } +} diff --git a/Tests/CoreBusinessTests/DataProviderTests.swift b/Tests/CoreBusinessTests/DataProviderTests.swift new file mode 100644 index 00000000..8c95d437 --- /dev/null +++ b/Tests/CoreBusinessTests/DataProviderTests.swift @@ -0,0 +1,25 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCoreBusiness + +import PillarboxCircumspect +import XCTest + +final class DataProviderTests: XCTestCase { + func testExistingMediaMetadata() { + expectSuccess( + from: DataProvider().mediaCompositionPublisher(forUrn: "urn:rts:video:6820736") + ) + } + + func testNonExistingMediaMetadata() { + expectFailure( + DataError.http(withStatusCode: 404), + from: DataProvider().mediaCompositionPublisher(forUrn: "urn:rts:video:unknown") + ) + } +} diff --git a/Tests/CoreBusinessTests/ErrorsTests.swift b/Tests/CoreBusinessTests/ErrorsTests.swift new file mode 100644 index 00000000..50cb74ae --- /dev/null +++ b/Tests/CoreBusinessTests/ErrorsTests.swift @@ -0,0 +1,20 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCoreBusiness + +import Nimble +import XCTest + +final class ErrorTests: XCTestCase { + func testHttpError() { + expect(DataError.http(withStatusCode: 404)).notTo(beNil()) + } + + func testNotHttpNSError() { + expect(DataError.http(withStatusCode: 200)).to(beNil()) + } +} diff --git a/Tests/CoreBusinessTests/HTTPURLResponseTests.swift b/Tests/CoreBusinessTests/HTTPURLResponseTests.swift new file mode 100644 index 00000000..ef384e0f --- /dev/null +++ b/Tests/CoreBusinessTests/HTTPURLResponseTests.swift @@ -0,0 +1,28 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCoreBusiness + +import Nimble +import XCTest + +final class HTTPURLResponseTests: XCTestCase { + func testFixedLocalizedStringForValidStatusCode() { + expect(HTTPURLResponse.fixedLocalizedString(forStatusCode: 404)).to(equal("Not found")) + } + + func testFixedLocalizedStringForInvalidStatusCode() { + expect(HTTPURLResponse.fixedLocalizedString(forStatusCode: 956)).to(equal("Server error")) + } + + func testNetworkLocalizedStringForValidKey() { + expect(HTTPURLResponse.coreNetworkLocalizedString(forKey: "not found")).to(equal("Not found")) + } + + func testNetworkLocalizedStringForInvalidKey() { + expect(HTTPURLResponse.coreNetworkLocalizedString(forKey: "Some key which does not exist")).to(equal("Unknown error.")) + } +} diff --git a/Tests/CoreBusinessTests/MediaMetadataTests.swift b/Tests/CoreBusinessTests/MediaMetadataTests.swift new file mode 100644 index 00000000..6df3cf9e --- /dev/null +++ b/Tests/CoreBusinessTests/MediaMetadataTests.swift @@ -0,0 +1,76 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCoreBusiness + +import Nimble +import XCTest + +final class MediaMetadataTests: XCTestCase { + private static func metadata(_ kind: Mock.MediaCompositionKind) throws -> MediaMetadata { + try MediaMetadata( + mediaCompositionResponse: .init( + mediaComposition: Mock.mediaComposition(kind), + response: .init() + ), + dataProvider: DataProvider() + ) + } + + func testStandardMetadata() throws { + let metadata = try Self.metadata(.onDemand) + expect(metadata.title).to(equal("Yadebat")) + expect(metadata.subtitle).to(equal("On réunit des ex après leur rupture")) + expect(metadata.description).to(equal(""" + Dans ce nouvel épisode de YADEBAT, Mélissa réunit 3 couples qui se sont séparés récemment. \ + Elles les a questionné en face à face pour connaître leurs différents ressentis et réactions. + """)) + expect(metadata.episodeInformation).to(equal(.long(season: 2, episode: 12))) + } + + func testRedundantMetadata() throws { + let metadata = try Self.metadata(.redundant) + expect(metadata.title).to(equal("19h30")) + expect(metadata.subtitle).to(contain("February")) + expect(metadata.description).to(beNil()) + expect(metadata.episodeInformation).to(beNil()) + } + + func testLiveMetadata() throws { + let metadata = try Self.metadata(.live) + expect(metadata.title).to(equal("La 1ère en direct")) + expect(metadata.subtitle).to(beNil()) + expect(metadata.description).to(beNil()) + expect(metadata.episodeInformation).to(beNil()) + } + + func testMainChapter() throws { + let metadata = try Self.metadata(.onDemand) + expect(metadata.mainChapter.urn).to(equal(metadata.mediaComposition.chapterUrn)) + } + + func testChapters() throws { + let metadata = try Self.metadata(.mixed) + expect(metadata.chapters).to(haveCount(10)) + } + + func testAudioChapterRemoval() throws { + let metadata = try Self.metadata(.audioChapters) + expect(metadata.chapters).to(beEmpty()) + } + + func testAnalytics() throws { + let metadata = try Self.metadata(.onDemand) + expect(metadata.analyticsData).notTo(beEmpty()) + expect(metadata.analyticsMetadata).notTo(beEmpty()) + } + + func testMissingChapterAnalytics() throws { + let metadata = try Self.metadata(.missingAnalytics) + expect(metadata.analyticsData).to(beEmpty()) + expect(metadata.analyticsMetadata).to(beEmpty()) + } +} diff --git a/Tests/CoreBusinessTests/Mock.swift b/Tests/CoreBusinessTests/Mock.swift new file mode 100644 index 00000000..88ef7dbe --- /dev/null +++ b/Tests/CoreBusinessTests/Mock.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCoreBusiness + +import Foundation +import UIKit + +enum Mock { + enum MediaCompositionKind: String { + case missingAnalytics + case drm + case live + case onDemand + case redundant + case mixed + case audioChapters + } + + static func mediaComposition(_ kind: MediaCompositionKind = .onDemand) -> MediaComposition { + let data = NSDataAsset(name: "MediaComposition_\(kind.rawValue)", bundle: .module)!.data + return try! DataProvider.decoder().decode(MediaComposition.self, from: data) + } +} diff --git a/Tests/CoreBusinessTests/PublishersTests.swift b/Tests/CoreBusinessTests/PublishersTests.swift new file mode 100644 index 00000000..610e95c8 --- /dev/null +++ b/Tests/CoreBusinessTests/PublishersTests.swift @@ -0,0 +1,20 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCoreBusiness + +import PillarboxCircumspect +import XCTest + +final class PublishersTests: XCTestCase { + func testHttpError() { + expectFailure( + DataError.http(withStatusCode: 404), + from: URLSession(configuration: .default).dataTaskPublisher(for: URL(string: "http://localhost:8123/not_found")!) + .mapHttpErrors() + ) + } +} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/Contents.json b/Tests/CoreBusinessTests/Resources.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Tests/CoreBusinessTests/Resources.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_audioChapters.dataset/Contents.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_audioChapters.dataset/Contents.json new file mode 100644 index 00000000..2ac61c6c --- /dev/null +++ b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_audioChapters.dataset/Contents.json @@ -0,0 +1,12 @@ +{ + "data" : [ + { + "filename" : "urn_rts_audio_13598743.json", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_audioChapters.dataset/urn_rts_audio_13598743.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_audioChapters.dataset/urn_rts_audio_13598743.json new file mode 100644 index 00000000..d6b7b9a1 --- /dev/null +++ b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_audioChapters.dataset/urn_rts_audio_13598743.json @@ -0,0 +1,752 @@ +{ + "chapterUrn" : "urn:rts:audio:13598743", + "episode" : { + "id" : "13598757", + "title" : "Forum du 12.12.2022", + "publishedDate" : "2022-12-12T18:00:00+01:00", + "imageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/16x9", + "imageTitle" : "Logo Forum [RTS]" + }, + "show" : { + "id" : "1784426", + "vendor" : "RTS", + "transmission" : "RADIO", + "urn" : "urn:rts:show:radio:1784426", + "title" : "Forum", + "lead" : "7 jours sur 7, Forum questionne en direct les acteurs de l’actualité, ouvre le débat sur les controverses qui animent la vie politique, culturelle et économique.", + "description" : "7 jours sur 7, Forum questionne en direct les acteurs de l'actualité, ouvre le débat sur les controverses qui animent la vie politique, culturelle et économique. C'est un lieu d'écoute, d'échanges, de remise en question. Forum propose chaque soir un regard attentif et acéré sur l’actualité suisse et internationale.", + "imageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/16x9", + "imageTitle" : "Logo Forum [RTS]", + "bannerImageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/3x1", + "posterImageUrl" : "https://ws.srf.ch/asset/image/audio/e0322b37-5697-474d-93ac-19a4044a6a24/POSTER.jpg", + "posterImageIsFallbackUrl" : true, + "homepageUrl" : "https://details.rts.ch/la-1ere/programmes/forum/", + "podcastSubscriptionUrl" : "https://www.rts.ch/la-1ere/programmes/forum/podcast/", + "primaryChannelId" : "a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", + "primaryChannelUrn" : "urn:rts:channel:radio:a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", + "audioDescriptionAvailable" : false, + "subtitlesAvailable" : false, + "multiAudioLanguagesAvailable" : false, + "allowIndexing" : false + }, + "channel" : { + "id" : "a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", + "vendor" : "RTS", + "urn" : "urn:rts:channel:radio:a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", + "title" : "La 1ère", + "imageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/16x9", + "imageTitle" : "Logo Forum [RTS]", + "transmission" : "RADIO" + }, + "chapterList" : [ { + "id" : "13598743", + "mediaType" : "AUDIO", + "vendor" : "RTS", + "urn" : "urn:rts:audio:13598743", + "title" : "Forum - Présenté par Tania Sazpinar et Esther Coquoz", + "imageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/16x9", + "imageTitle" : "Logo Forum [RTS]", + "type" : "EPISODE", + "date" : "2022-12-12T18:00:00+01:00", + "duration" : 3600000, + "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598743/d96d95c9-23cf-38b8-92af-42e89cd85afa.mp3", + "playableAbroad" : true, + "socialCountList" : [ { + "key" : "srgView", + "value" : 898 + }, { + "key" : "srgLike", + "value" : 0 + }, { + "key" : "fbShare", + "value" : 0 + }, { + "key" : "twitterShare", + "value" : 0 + }, { + "key" : "googleShare", + "value" : 0 + }, { + "key" : "whatsAppShare", + "value" : 0 + } ], + "displayable" : true, + "position" : 0, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Forum - Présenté par Tania Sazpinar et Esther Coquoz", + "media_type" : "Audio", + "media_segment_id" : "13598743", + "media_episode_length" : "3600", + "media_segment_length" : "3600", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "long", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:audio:13598743", + "media_sub_set_id" : "EPISODE" + }, + "eventData" : "$ec997ecbd9874fae$258ca058508c83574cb22919f3635503768c1a1c0add2b04ca590b77d2b9457e657aacd76afc90a50061e17ac60c3e1b67605cb41d8dbc3fcfb4eda6081a00dad23404d36ad5c0847b7094c4dbdf712baff4d617d6908953f61b3fd1052da55e58b1762651dc68ab1edc98f3895efd8b3de1ef01a92d0dee673380413069693faf88e3d89798afe7702404805004026b09179557a9338275a432ee9565179bf6551b3364914984da3c5bc88caa91d9b58e6cd3d2373d8ff4526bfc407467ed82cd9235d6718249b36b1d16bf2711443d330799f7a4daa3a739e3b314cf29569e2f064917e6b7adfb7a7ef08b0a415a0dc51b17bcf404d74b84c0a8eedc6c0d1b8c119db4f09d0a9e5e819aafddb6e721cad6e712beb3499f590d293a523af2d5c1529292ca9c2be172e43c321c199e55604299c18745e27c6e6a8f8ab728d0e7484e4932e10e9e1d5a4698bc146261d54a537619f59cd4677770cbd389f454732c002e3598846ce835295afe13ad03e2", + "fullLengthMarkIn" : 0, + "fullLengthMarkOut" : 0, + "resourceList" : [ { + "url" : "https://rts-aod-dd.akamaized.net/ww/13598743/d96d95c9-23cf-38b8-92af-42e89cd85afa.mp3", + "quality" : "HQ", + "protocol" : "HTTPS", + "encoding" : "MP3", + "mimeType" : "audio/mpeg", + "presentation" : "DEFAULT", + "streaming" : "PROGRESSIVE", + "dvr" : false, + "live" : false, + "mediaContainer" : "NONE", + "audioCodec" : "MP3", + "videoCodec" : "NONE", + "tokenType" : "NONE", + "analyticsMetadata" : { + "media_streaming_quality" : "HQ", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598743/d96d95c9-23cf-38b8-92af-42e89cd85afa.mp3" + } + } ] + }, { + "id" : "13622547", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13622547", + "title" : "Forum (vidéo) - Présenté par Tania Sazpinar et Esther Coquoz", + "imageUrl" : "https://www.rts.ch/2022/12/12/23/08/13622546.image/16x9", + "imageTitle" : "Forum (vidéo) - Présenté par Tania Sazpinar et Esther Coquoz [RTS]", + "type" : "CLIP", + "date" : "2022-12-12T18:00:00+01:00", + "duration" : 3600840, + "validFrom" : "2022-12-12T19:00:00+01:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:audio:13598743", + "position" : 1, + "noEmbed" : false, + "analyticsData" : { + "ns_st_ep" : "Forum (vidéo) - Présenté par Tania Sazpinar et Esther Coquoz", + "ns_st_ty" : "Video", + "ns_st_ci" : "13622547", + "ns_st_el" : "3600840", + "ns_st_cl" : "3600840", + "ns_st_sl" : "3600840", + "srg_mgeobl" : "false", + "ns_st_tp" : "1", + "ns_st_cn" : "1", + "ns_st_ct" : "vc12", + "ns_st_pn" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc" + }, + "analyticsMetadata" : { + "media_segment" : "Forum (vidéo) - Présenté par Tania Sazpinar et Esther Coquoz", + "media_type" : "Video", + "media_segment_id" : "13622547", + "media_episode_length" : "3601", + "media_segment_length" : "3601", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "long", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:13622547", + "media_sub_set_id" : "CLIP" + }, + "eventData" : "$f992085ed4fa2c25$c6998ff87df1d9ea9b7cb76e4b23da9ccb9d0748c5f7c36f018bc86158a004d801a0736b824af35c828a67092a5607683d005f30cb1d4e9a4d6a98640119a7376fe4018ca8b0a34f7d08970687edc20b19931236b201953449b0c2fed80dc7807f436c2e96b8636a941cf7ad31e297d87411254cd1c9c1e876dc1269b9ff899bfed9c36153b83a4d8aaf09953bdc0e89f808894cdd69d0a83089a730d6d73bfa6561ea171738e61b27961494af846a485a55e415fae74b124d3f81c0276fa75c9f37af3d9c0f5711fa062e15e232bdce3eddebc1d02eb77f236dd10c12aac38ebceb53beaba79f683f2295fa3abf3c1a014f075500913f9c14ffd6c48e4c01dbf9ddb044db5d35fa0aebf1ff3bb177b790a979301a88013d8d3aa4fd3b0350f27a9f443dc4a89b67eb55965ad027f8a0958055b4e6e82a9a15ea22192d9273cf0d6adf93159f41e566749d6a485971fd3229d78144f708ff746967c267ac2c65999de99e444684cdeedbe0f4ae8beece", + "resourceList" : [ { + "url" : "https://rts-vod-amd.akamaized.net/ww/13622547/ba5e0176-bace-398f-9d89-ed0a3f18dab4/master.m3u8", + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + } ], + "subtitleInformationList" : [ { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "HLS", + "type" : "SDH" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-vod-amd.akamaized.net/ww/13622547/ba5e0176-bace-398f-9d89-ed0a3f18dab4/master.m3u8" + } + } ], + "aspectRatio" : "16:9", + "spriteSheet" : { + "urn" : "urn:rts:video:13622547", + "rows" : 26, + "columns" : 20, + "thumbnailHeight" : 84, + "thumbnailWidth" : 150, + "interval" : 7000, + "url" : "https://il.srgssr.ch/spritesheet/urn/rts/video/13622547/sprite-13622547.jpeg" + } + }, { + "id" : "13598744", + "mediaType" : "AUDIO", + "vendor" : "RTS", + "urn" : "urn:rts:audio:13598744", + "title" : "Les hautes instances européennes expriment leur inquiétude après les accusations de corruption", + "imageUrl" : "https://www.rts.ch/2022/10/18/17/58/13474798.image/16x9", + "imageTitle" : "Ursula von der Leyen au Parlement européen à Strasbourg, 18.10.2022. [Jean-Francois Badias - AP/Keystone]", + "imageCopyright" : "Jean-Francois Badias - AP/Keystone", + "type" : "CLIP", + "date" : "2022-12-12T18:02:00+01:00", + "duration" : 237000, + "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598744/23c3b7cd-a474-3fa6-9961-9019cf0a94e8.mp3", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:audio:13598743", + "position" : 2, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Les hautes instances européennes expriment leur inquiétude après les accusations de corruption", + "media_type" : "Audio", + "media_segment_id" : "13598744", + "media_episode_length" : "237", + "media_segment_length" : "237", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:audio:13598744", + "media_sub_set_id" : "CLIP" + }, + "eventData" : "$ddaa9deb763583f4$71f264168ee2b6b5831574deab90274752575e6b3e61cb945aca3ecdc7ccbfa7d48f87832df12d11e604d7bceeb871dab04acb165b95ea842780df4c7568b481603d3113ff7cdfe19680137b9c06d932c5a4b231bb81f7084de9e10f2a752289ce267e170ee986c43a5cc3841d88a5e6049e4f3d86e209cc9b6c1bdc262b7bd0a3b1db4abd997a9c7bad3c8ae64bd89b7d6d5683b00c4a5760118d8358d85fbd0a89e6a528ab345690c378fec7e87bd9c48408f407663e3f8d19fa80c948c3b4f93f58dab1a6e03b21505c6bca8599acb6ed1ee77a9a83fce9311edb23d47c4b3805a8bf7442f804e1adc3e115772228a7297f7c50406503f4a23c1f0a27f3c1d9093f6773843e37ffd58210088c9c2888af8f2180a4d54ad4d1ab254b91daee6cda26cfc329ee3b5fbc965df3e7da45ca1702e966b6951897fd0aa4c0618feb71143238bebe84812ecb0926a99ad00a27bf71793ff3d5b6537f53a2aab99ac4f02f64710b645c3306dfe53948ec4539", + "resourceList" : [ { + "url" : "https://rts-aod-dd.akamaized.net/ww/13598744/23c3b7cd-a474-3fa6-9961-9019cf0a94e8.mp3", + "quality" : "HQ", + "protocol" : "HTTPS", + "encoding" : "MP3", + "mimeType" : "audio/mpeg", + "presentation" : "DEFAULT", + "streaming" : "PROGRESSIVE", + "dvr" : false, + "live" : false, + "mediaContainer" : "NONE", + "audioCodec" : "MP3", + "videoCodec" : "NONE", + "tokenType" : "NONE", + "analyticsMetadata" : { + "media_streaming_quality" : "HQ", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598744/23c3b7cd-a474-3fa6-9961-9019cf0a94e8.mp3" + } + } ] + }, { + "id" : "13598745", + "mediaType" : "AUDIO", + "vendor" : "RTS", + "urn" : "urn:rts:audio:13598745", + "title" : "Yves Bertoncini s’exprime sur les cas de corruptions présumés au Parlement européen", + "description" : "Interview de Yves Bertoncini, enseignant en affaires européennes à l'école de commerce de Paris, et consultant en affaires européennes.", + "imageUrl" : "https://www.rts.ch/2020/07/19/16/43/11478416.image/16x9", + "imageTitle" : "Yves Bertoncini, président du Mouvement européen en France. [eesc.europa.eu]", + "imageCopyright" : "eesc.europa.eu", + "type" : "CLIP", + "date" : "2022-12-12T18:03:00+01:00", + "duration" : 336000, + "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598745/2da36319-1f31-36af-9f09-e9d86f157e40.mp3", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:audio:13598743", + "position" : 3, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Yves Bertoncini s’exprime sur les cas de corruptions présumés au Parlement européen", + "media_type" : "Audio", + "media_segment_id" : "13598745", + "media_episode_length" : "336", + "media_segment_length" : "336", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:audio:13598745", + "media_sub_set_id" : "CLIP" + }, + "eventData" : "$6affd6172a1fb2b5$298147a56fd0d5dfd2ba9795c9a5c69c8ba397d86fdde5696924cb66475b623869a91bd789ded60b18d93bf43d5f241bfae6d903d3bd3a547c45fcebc0b1a672c3f6088457ad57ed4b312a965c190f9cabe26f33b0bf4e66e7ce2d775adb39bbe0213138ec69e0a35e374f3295700742dd4a4f394811b916dd8825ed91aaa740058954798c880696c944cc25b81e144a9888e68c8d31191cb739b6949079d7c70ac153f2e81cd36782ad53ec72c94c5f277fa69ae077ad79739e0e3bf43207939bf1b960f0d1402598eaedd36e0542cd4c7d370104b08c0305f21541dda6a18c11d8133f2f64026010d5b1ba681dc9fbc07f0b1b9c173459df97488ebe374dc758b9a4828b12456c892259428fd9d04174d7d2ea40a45fb1fb6d85135347b118a7ccfaa99a2262fdc3c7d828562154bf8ff163ac9e9bc9555123d8f1af656bdd98c3cf4aa0e3f2dba4dafd0dc72a285139502b271fbdfd7ea5cf22abfd3265119858ccdb2473b4f49226f02ac93b31e7", + "resourceList" : [ { + "url" : "https://rts-aod-dd.akamaized.net/ww/13598745/2da36319-1f31-36af-9f09-e9d86f157e40.mp3", + "quality" : "HQ", + "protocol" : "HTTPS", + "encoding" : "MP3", + "mimeType" : "audio/mpeg", + "presentation" : "DEFAULT", + "streaming" : "PROGRESSIVE", + "dvr" : false, + "live" : false, + "mediaContainer" : "NONE", + "audioCodec" : "MP3", + "videoCodec" : "NONE", + "tokenType" : "NONE", + "analyticsMetadata" : { + "media_streaming_quality" : "HQ", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598745/2da36319-1f31-36af-9f09-e9d86f157e40.mp3" + } + } ] + }, { + "id" : "13598746", + "mediaType" : "AUDIO", + "vendor" : "RTS", + "urn" : "urn:rts:audio:13598746", + "title" : "Première journée d’audience à Bellinzone pour le terroriste du kebab de Morges", + "imageUrl" : "https://www.rts.ch/2022/12/12/20/04/13622249.image/16x9", + "imageTitle" : "Première journée d’audience à Bellinzone pour le terroriste de Morges. [Linda Graedel - Keystone]", + "imageCopyright" : "Linda Graedel - Keystone", + "type" : "CLIP", + "date" : "2022-12-12T18:04:00+01:00", + "duration" : 161000, + "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598746/ac7ee7e2-5b20-359f-9655-f9e814bee447.mp3", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:audio:13598743", + "position" : 4, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Première journée d’audience à Bellinzone pour le terroriste du kebab de Morges", + "media_type" : "Audio", + "media_segment_id" : "13598746", + "media_episode_length" : "161", + "media_segment_length" : "161", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:audio:13598746", + "media_sub_set_id" : "CLIP" + }, + "eventData" : "$cf0f417e58a57656$75222e3db44c918c724e36471224ff8a7439cde9f3b249451df03ce7df4cb7a89bf6ac20089a37a66981de5c753a64a8fb34ee58db2062215983622a2184a953e6ea811782b27b8ce2f4c49cd73244812108447dda38bc9185cf35bc06895b04015ced91c13bf1e1d9f284e4878d326251a5d5beec438aadc91d460baf68eb333d892e36ffa49bb1cba06fab9d45590bfb5860467b482832760ed71a65c39175c2c24fb3602bc67afb3c9817aad42532caba64b693f5c445de5c8d712ed2a5b81863d00e48c7a50d12845aac4445c41113813db784ed2a6235c4047ae6a4ffd481917e73b1503892c84b63bb813dce3ef6b234ea20c4bbaafe957bbaf284fac50a148911e79093154475c8cbea03b9e74d848d900dac16ade67042c53e8319b7c8e549969f8bf1e306eb2b7cabb0d1df7568f34eafcb4e027fea412b11f99a4515562691cbaab1b9918f205c4a5a92f0265cd5fa6826af40ac88efe4a8b8b56372d9f2f35fe2c20c1c2f4ae844c85b69", + "resourceList" : [ { + "url" : "https://rts-aod-dd.akamaized.net/ww/13598746/ac7ee7e2-5b20-359f-9655-f9e814bee447.mp3", + "quality" : "HQ", + "protocol" : "HTTPS", + "encoding" : "MP3", + "mimeType" : "audio/mpeg", + "presentation" : "DEFAULT", + "streaming" : "PROGRESSIVE", + "dvr" : false, + "live" : false, + "mediaContainer" : "NONE", + "audioCodec" : "MP3", + "videoCodec" : "NONE", + "tokenType" : "NONE", + "analyticsMetadata" : { + "media_streaming_quality" : "HQ", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598746/ac7ee7e2-5b20-359f-9655-f9e814bee447.mp3" + } + } ] + }, { + "id" : "13598747", + "mediaType" : "AUDIO", + "vendor" : "RTS", + "urn" : "urn:rts:audio:13598747", + "title" : "Le nombre de jeunes femmes hospitalisées pour des troubles psychiques a largement augmenté: interview de Anne Edan", + "description" : "Interview de Anne Edan, responsable de Malatavie, Unité de crise-Partenariat Public privé HUG-Children Action", + "imageUrl" : "https://www.rts.ch/2022/12/12/18/43/13622215.image/16x9", + "imageTitle" : "La doctoresse Anne Edan est médecin responsable de Malatavie (HUG). [RTS]", + "imageCopyright" : "RTS", + "type" : "CLIP", + "date" : "2022-12-12T18:05:00+01:00", + "duration" : 306000, + "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598747/603ff318-7020-3b44-8ea8-efc1f500758d.mp3", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:audio:13598743", + "position" : 5, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Le nombre de jeunes femmes hospitalisées pour des troubles psychiques a largement augmenté: interview de Anne Edan", + "media_type" : "Audio", + "media_segment_id" : "13598747", + "media_episode_length" : "306", + "media_segment_length" : "306", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:audio:13598747", + "media_sub_set_id" : "CLIP" + }, + "eventData" : "$ebf7057806b66ff5$20cff69bc7509ca8a40119107dd01414fcac55cf2a2d4aa9bb7e13a8b50745fe7f1c7b6690ff41722a2b227ce4fea003cb98af73d0cda6a5216a2857e56b3df8542ca270e551ccbab0439e0142230090cc385cf6e4eb28bcf25cac1bba91696a9d85c9bceac0919559b07cacc6b584fcb7bc4f1b2b66e64a621d1c43b996a41ce5f89d125d42d251bd808d5f4ee2556c2743944e7caaa5629971a24aa99c6961c15756f77043c5bad622c87083b65decf3a97a80695a13a107fdef6d23d58c2ffa39e53344352734a8c627cad9f72796eb3207d06880bad20c616938c6ae37a9d274c0354776c078e7fc3cd01ee6af765282fbc2d02fafe6b6e6f96ed73f921465dbe73297e63cf496b40e1599ed37f1dab991f1aa9b39a64e68942523eb60f83aed372115739566f13e426d959f9414110ef3a632e4d333bbb3c667c2d7a9661a682d6099dbb53743633b47581d8ec96077ced60f6792d520da15cf1ce0d1c3129dcbafd0980faa1449bfbfa48050e9", + "resourceList" : [ { + "url" : "https://rts-aod-dd.akamaized.net/ww/13598747/603ff318-7020-3b44-8ea8-efc1f500758d.mp3", + "quality" : "HQ", + "protocol" : "HTTPS", + "encoding" : "MP3", + "mimeType" : "audio/mpeg", + "presentation" : "DEFAULT", + "streaming" : "PROGRESSIVE", + "dvr" : false, + "live" : false, + "mediaContainer" : "NONE", + "audioCodec" : "MP3", + "videoCodec" : "NONE", + "tokenType" : "NONE", + "analyticsMetadata" : { + "media_streaming_quality" : "HQ", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598747/603ff318-7020-3b44-8ea8-efc1f500758d.mp3" + } + } ] + }, { + "id" : "13598748", + "mediaType" : "AUDIO", + "vendor" : "RTS", + "urn" : "urn:rts:audio:13598748", + "title" : "Réforme du 2e pilier: la question des compensations financières pour la génération transitoire divise les Chambres fédérales", + "imageUrl" : "https://www.rts.ch/2022/12/05/17/48/13601356.image/16x9", + "imageTitle" : "La transformation numérique de l'administration divise les Chambres fédérales. [RTS]", + "imageCopyright" : "RTS", + "type" : "CLIP", + "date" : "2022-12-12T18:06:00+01:00", + "duration" : 151000, + "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598748/c8364401-7593-3c61-aefb-cd206fcb4101.mp3", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:audio:13598743", + "position" : 6, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Réforme du 2e pilier: la question des compensations financières pour la génération transitoire divise les Chambres fédérales", + "media_type" : "Audio", + "media_segment_id" : "13598748", + "media_episode_length" : "151", + "media_segment_length" : "151", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:audio:13598748", + "media_sub_set_id" : "CLIP" + }, + "eventData" : "$a4182629afa7c2d7$16cd8d9d1c92ece2ffd7db8649edfadfea3904afc9ba1d4acf8ef3bd38f50d80ea9b8b13038e6521e9f2d6949d97e226e9d9beae7c7e2289ccf429b551bc38ebaf7aab6821edc5396ee0547a188641d561cdf82e9c3700ec70be619740ca1493281ead1465c0c49996ff707f3d38bc97a88f14c19aa8e5200153b5e4e898261cd18e1a7e3e61094a134241756a6f8558463477fb51e09598a50bcb8c8b3fbd841170bff51c516ad6ace9d97989a2b78c3705a60ff211606781ecc296d949ace022ddac0e8736f2f499f827cb58469f723a0b708242f33cd5d9bef8ed4d0ae8ddf6f446de47eaf128949ca72970cb7a036996df5d2f43f952f44325c507ae2b9e8e56d468c4361d16e317cc338dd713dcdbae946f9288ec141bdd44f9eb30b325fd71229e850330754a92d0f88309117990bc9d33e4183c82ea103a1cd478b9111aef3db2fad3c7ef50cc4c4cad18a5395d8e547a36dbb70997d1588eac3bfed8c64fce49128575fd2331060377af7625", + "resourceList" : [ { + "url" : "https://rts-aod-dd.akamaized.net/ww/13598748/c8364401-7593-3c61-aefb-cd206fcb4101.mp3", + "quality" : "HQ", + "protocol" : "HTTPS", + "encoding" : "MP3", + "mimeType" : "audio/mpeg", + "presentation" : "DEFAULT", + "streaming" : "PROGRESSIVE", + "dvr" : false, + "live" : false, + "mediaContainer" : "NONE", + "audioCodec" : "MP3", + "videoCodec" : "NONE", + "tokenType" : "NONE", + "analyticsMetadata" : { + "media_streaming_quality" : "HQ", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598748/c8364401-7593-3c61-aefb-cd206fcb4101.mp3" + } + } ] + }, { + "id" : "13598749", + "mediaType" : "AUDIO", + "vendor" : "RTS", + "urn" : "urn:rts:audio:13598749", + "title" : "Retour sur l’élection d’Éric Ciotti à la tête du parti français Les Républicains", + "imageUrl" : "https://www.rts.ch/2022/12/12/17/37/13622160.image/16x9", + "imageTitle" : "Éric Ciotti est élu à la tête du parti français Les Républicains le 11 décembre 2022. [Christophe Petit Tesson - EPA/Keystone]", + "imageCopyright" : "Christophe Petit Tesson - EPA/Keystone", + "type" : "CLIP", + "date" : "2022-12-12T18:07:00+01:00", + "duration" : 166000, + "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598749/8c057540-b74b-35f8-9790-64821c110eb9.mp3", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:audio:13598743", + "position" : 7, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Retour sur l’élection d’Éric Ciotti à la tête du parti français Les Républicains", + "media_type" : "Audio", + "media_segment_id" : "13598749", + "media_episode_length" : "166", + "media_segment_length" : "166", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:audio:13598749", + "media_sub_set_id" : "CLIP" + }, + "eventData" : "$d8edc280c72c858e$645dcf60f54f9a71c47bb237ec2674bcc3713eabb9a5f1f3008fed53e99ccdadeded7697f0ba048e8dccc1367486f718c29c59448c392eeedc4267eb44eb799b5845d710f92771a5e91d59cc2a5dce42b5a91fa430eaaaeecd08e332b8ce5cbfe036afdc99aab2c06c002af485d354fa1a4636126a069cd11cab33e12baad3d762462eaa03a543dda98c7aa52e8d4545378e49dddde527d463f4b3a8ff44ba1c9a399aaa97101f8c341dbda37164c9d43df821681c19abd73e2bc83fcf404dd436d2f8b64503f548ceb42672e58566696da69314cc56462b0818740b47f34079bcb604e12d46dd27e7b4c13b9b1519e9fa228b61973d1604ee3787fe2660fd51ba1c30f79be516edf7355751f1b096e38e425079f998aaad023de2dd8a9a754e4f445752c79dac4545400958de48acd769a2eb64e5579a0cc5bba86752c05f1abcab04d254fe1bbcf1488b66bff49634784ed1ae381c1604e2d612917f5fe58213221fd67378b5dd92d38da9b02c8ac7", + "resourceList" : [ { + "url" : "https://rts-aod-dd.akamaized.net/ww/13598749/8c057540-b74b-35f8-9790-64821c110eb9.mp3", + "quality" : "HQ", + "protocol" : "HTTPS", + "encoding" : "MP3", + "mimeType" : "audio/mpeg", + "presentation" : "DEFAULT", + "streaming" : "PROGRESSIVE", + "dvr" : false, + "live" : false, + "mediaContainer" : "NONE", + "audioCodec" : "MP3", + "videoCodec" : "NONE", + "tokenType" : "NONE", + "analyticsMetadata" : { + "media_streaming_quality" : "HQ", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598749/8c057540-b74b-35f8-9790-64821c110eb9.mp3" + } + } ] + }, { + "id" : "13598750", + "mediaType" : "AUDIO", + "vendor" : "RTS", + "urn" : "urn:rts:audio:13598750", + "title" : "Vincent Baudriller s’exprime sur la nomination de la Française Séverine Chavrier à la tête de la Comédie à Genève", + "description" : "Interview de Vincent Baudriller, directeur du Théâtre de Vidy.", + "imageUrl" : "https://www.rts.ch/2020/04/14/16/05/11246790.image/16x9", + "imageTitle" : "Vincent Baudriller. [Jean-Christophe Bott - Keystone]", + "imageCopyright" : "Jean-Christophe Bott - Keystone", + "type" : "CLIP", + "date" : "2022-12-12T18:08:00+01:00", + "duration" : 402000, + "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598750/4f917b97-c0c7-3775-9564-5fc8cb11a5b8.mp3", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:audio:13598743", + "position" : 8, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Vincent Baudriller s’exprime sur la nomination de la Française Séverine Chavrier à la tête de la Comédie à Genève", + "media_type" : "Audio", + "media_segment_id" : "13598750", + "media_episode_length" : "402", + "media_segment_length" : "402", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:audio:13598750", + "media_sub_set_id" : "CLIP" + }, + "eventData" : "$eae27a0140800d45$b6ba151f5298efd03d42142e78f964c7e451e7511b1f3ff5fff3dd6b4cda7136fc5a1ad7ac52128003d431a94c222d30740781e2bcdd2ced46f84dc828c967ec5fbc3eeb32df9678c036ecdffd97b0ccf581a492e0819c575dedb053c44c45ae824e6035416f975a7ade48492d7c98c469d646f859e27de83c80020de052f5b136539c97b0772adf35f120ff09c3feccb5ac20892ac5002d3c10eb28e6dbfa7f9f598089fa93c004a4939b3d654c71459542a4c8bdf11354c4a762013ad47d3f3bba9dbfece5677d500a2031206c93df103bfb66663499a1f73f38ae449f0871beb73ef585612d016bd881fdc8bb35e0429499b2049dd1ad0880a62239b46f197ade98d6848e8aa2f17797f28b533f60554fa7b062d5b7b47d069bba7fee70687327f27e81d2aed628c09c925e2c5c6e86ccd7cc30158ca20a7a94d12cb0c98e9722d7f350bd8bd18a3f095842bb4d7733be89733d8710608a9bf0f87cdae636f5cfbb33f0d1386a4994938dd5f7d526", + "resourceList" : [ { + "url" : "https://rts-aod-dd.akamaized.net/ww/13598750/4f917b97-c0c7-3775-9564-5fc8cb11a5b8.mp3", + "quality" : "HQ", + "protocol" : "HTTPS", + "encoding" : "MP3", + "mimeType" : "audio/mpeg", + "presentation" : "DEFAULT", + "streaming" : "PROGRESSIVE", + "dvr" : false, + "live" : false, + "mediaContainer" : "NONE", + "audioCodec" : "MP3", + "videoCodec" : "NONE", + "tokenType" : "NONE", + "analyticsMetadata" : { + "media_streaming_quality" : "HQ", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598750/4f917b97-c0c7-3775-9564-5fc8cb11a5b8.mp3" + } + } ] + }, { + "id" : "13598751", + "mediaType" : "AUDIO", + "vendor" : "RTS", + "urn" : "urn:rts:audio:13598751", + "title" : "Forum des idées - Le Campus pour la démocratie veut rendre la démocratie plus accessible aux citoyens suisses", + "description" : "Interview de Catherine Carron, représentante romande du Campus pour la démocratie, plate-forme nationale de l'éducation à la citoyenneté et à la participation politique.", + "imageUrl" : "https://www.rts.ch/2023/05/31/13/06/13531758.image/16x9", + "imageTitle" : "Le Palais fédéral à Berne. [Peter Klaunzer - Keystone]", + "imageCopyright" : "Peter Klaunzer - Keystone", + "type" : "CLIP", + "date" : "2022-12-12T18:09:00+01:00", + "duration" : 379000, + "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598751/c2311297-1727-36ec-87d8-dcf686e443a0.mp3", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:audio:13598743", + "position" : 9, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Forum des idées - Le Campus pour la démocratie veut rendre la démocratie plus accessible aux citoyens suisses", + "media_type" : "Audio", + "media_segment_id" : "13598751", + "media_episode_length" : "379", + "media_segment_length" : "379", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:audio:13598751", + "media_sub_set_id" : "CLIP" + }, + "eventData" : "$cfff2b2de5e31883$8435e642492008527864da938d268db4ff9ad63541b83d0192ac5da8fba432287d015b7f7ee0a9cd15e07c53a739c75e1a75edb0ca8b960f08c0f28c67fce3decbb41279f1966a042f1e211d31087178834bcff57323e5f167b2d401f85116c07881ce0946db23d45d0210ee3fa2ce47d758f1ffd33ace7764de97c0026530c94629151151a21478d9811a1fc40a1480624caafb03b4ffc7f5d60cebae02f1bfde44250293515a95466c093c684e8cae9e17abf7e23d1689dab081d2f2789dc457208dda279b0a0c91455a9f49ffdef96f6a13bd34c41181177737de10ac66672eb546990ec2e5e0a5f59471838e544628660638665621802751beb522788dc0abe15fc010ea722c8671f1bf0fa97ad13fc1bc01a5bbf6b355cce414a04c65f5cf03cd78757e7f43cdda95046b734dab8f169a4e3338ef1e3634064cd20c065a3037ce8a27c54f9cafe95a44608f0d3e014df1a0b5be43ccdea3e5817a85b15759f7c12eece51ade1619eac3c840f62b", + "resourceList" : [ { + "url" : "https://rts-aod-dd.akamaized.net/ww/13598751/c2311297-1727-36ec-87d8-dcf686e443a0.mp3", + "quality" : "HQ", + "protocol" : "HTTPS", + "encoding" : "MP3", + "mimeType" : "audio/mpeg", + "presentation" : "DEFAULT", + "streaming" : "PROGRESSIVE", + "dvr" : false, + "live" : false, + "mediaContainer" : "NONE", + "audioCodec" : "MP3", + "videoCodec" : "NONE", + "tokenType" : "NONE", + "analyticsMetadata" : { + "media_streaming_quality" : "HQ", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598751/c2311297-1727-36ec-87d8-dcf686e443a0.mp3" + } + } ] + }, { + "id" : "13598756", + "mediaType" : "AUDIO", + "vendor" : "RTS", + "urn" : "urn:rts:audio:13598756", + "title" : "Le grand débat - Faut-il se méfier de TikTok?", + "description" : "Débat entre Laurence Allard, maîtresse de conférences en Sciences de la Communication à la Sorbonne-Nouvelle et sociologue des usages numériques, Charles Thibout, politiste, chercheur à l'IRIS et à l'Université Paris 1, spécialiste de la géopolitique des entreprises de nouvelles technologies, et Yaniv Benhamou, professeur en droit du numérique à la Faculté de droit et au Digital Law Center de l’Université de Genève et avocat en droit des nouvelles technologies.", + "imageUrl" : "https://www.rts.ch/2022/12/12/19/02/13622244.image/16x9", + "imageTitle" : "Débat entre Laurence Allard, maîtresse de conférences en Sciences de la Communication à la Sorbonne-Nouvelle et sociologue des usages numériques, Charles Thibout, politiste, chercheur à l'IRIS et à l'Université Paris 1, spécialiste de la géopolitique des entreprises de nouvelles technologies, et Yaniv Benhamou, professeur en droit du numérique à la Faculté de droit et au Digital Law Center de l’Université de Genève et avocat en droit des nouvelles technologies. [RTS - RTS]", + "imageCopyright" : "RTS - RTS", + "type" : "CLIP", + "date" : "2022-12-12T18:14:00+01:00", + "duration" : 1286000, + "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/13598756/e6723f95-82a7-3a9f-97fd-c2c6a0421d28.mp3", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:audio:13598743", + "position" : 10, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Le grand débat - Faut-il se méfier de TikTok?", + "media_type" : "Audio", + "media_segment_id" : "13598756", + "media_episode_length" : "1286", + "media_segment_length" : "1286", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "long", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:audio:13598756", + "media_sub_set_id" : "CLIP" + }, + "eventData" : "$05d56fc176b428a7$c4d4c80e459350910101e4a51f2103e4d1decd01ac68bf302012719250e82ebba3730b1e526100865258028134005197e2ca53e885fc5c1596af2b03bfa87ab00717fd1e776a2117ff6de14c74135d21a7e75cfd5f78f34fc4bc05c5f6d6e9d4131f83701dba8d5bb403b1f70c0d6844c9ba1143024dfd8dfb51632ca593c515a42885e33e7d5c5a52530d8e728d7af5950bdf2b53be9513a0219ae62ddfb028dd10b9c99a8852d60fad788d02105300a1db890c61f57fa2756de8b1b7d66e20214996f035c7d2777e10f3a565e4bd9baa26431e49f2cf78c359dd46cb20d166fb60ad10a8e865065e4dc6881028931fb21ddc2aa708cfd2c1addf35e1fd54e733da0813c27b1be77aaeed5b08932c3c08a9feddd92cc294f05734b0ea2cc96f6b69d2a4608f90ec41045c560580782548de98c42c39cd7dfb61d1878b927371afc194d99a266b39ff8b1ada6a2e0120076f53bf8ddc4ed33a530c234c63c8c005c76a06888964520e2cef23ef655e8e", + "resourceList" : [ { + "url" : "https://rts-aod-dd.akamaized.net/ww/13598756/e6723f95-82a7-3a9f-97fd-c2c6a0421d28.mp3", + "quality" : "HQ", + "protocol" : "HTTPS", + "encoding" : "MP3", + "mimeType" : "audio/mpeg", + "presentation" : "DEFAULT", + "streaming" : "PROGRESSIVE", + "dvr" : false, + "live" : false, + "mediaContainer" : "NONE", + "audioCodec" : "MP3", + "videoCodec" : "NONE", + "tokenType" : "NONE", + "analyticsMetadata" : { + "media_streaming_quality" : "HQ", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-aod-dd.akamaized.net/ww/13598756/e6723f95-82a7-3a9f-97fd-c2c6a0421d28.mp3" + } + } ] + } ], + "analyticsData" : { + "srg_pr_id" : "13598757", + "srg_plid" : "1784426", + "ns_st_pl" : "Forum", + "ns_st_pr" : "Forum du 12.12.2022", + "ns_st_dt" : "2022-12-12", + "ns_st_ddt" : "2022-12-12", + "ns_st_tdt" : "2022-12-12", + "ns_st_tm" : "18:00", + "ns_st_tep" : "*null", + "ns_st_li" : "0", + "ns_st_stc" : "0867", + "ns_st_st" : "RTS Online", + "ns_st_tpr" : "1784426", + "ns_st_en" : "*null", + "ns_st_ge" : "*null", + "ns_st_ia" : "*null", + "ns_st_ce" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc", + "srg_unit" : "RTS", + "srg_c1" : "full", + "srg_c2" : "la-1ere_programmes_forum", + "srg_c3" : "LA 1ÈRE", + "srg_aod_prid" : "13598757" + }, + "analyticsMetadata" : { + "media_episode_id" : "13598757", + "media_show_id" : "1784426", + "media_show" : "Forum", + "media_episode" : "Forum du 12.12.2022", + "media_is_livestream" : "false", + "media_full_length" : "full", + "media_enterprise_units" : "RTS", + "media_joker1" : "full", + "media_joker2" : "la-1ere_programmes_forum", + "media_joker3" : "LA 1ÈRE", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_thumbnail" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/16x9/scale/width/344", + "media_publication_date" : "2022-12-12", + "media_publication_time" : "18:00:00", + "media_publication_datetime" : "2022-12-12T18:00:00+01:00", + "media_tv_date" : "2022-12-12", + "media_tv_time" : "18:00:00", + "media_tv_datetime" : "2022-12-12T18:00:00+01:00", + "media_content_group" : "Forum,Programmes,La 1ère", + "media_channel_id" : "a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", + "media_channel_cs" : "0867", + "media_channel_name" : "La 1ère", + "media_since_publication_d" : "496", + "media_since_publication_h" : "11919" + } +} \ No newline at end of file diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_drm.dataset/Contents.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_drm.dataset/Contents.json new file mode 100644 index 00000000..d5ec4402 --- /dev/null +++ b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_drm.dataset/Contents.json @@ -0,0 +1,12 @@ +{ + "data" : [ + { + "filename" : "MediaComposition_drm.json", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_drm.dataset/MediaComposition_drm.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_drm.dataset/MediaComposition_drm.json new file mode 100644 index 00000000..b261eecd --- /dev/null +++ b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_drm.dataset/MediaComposition_drm.json @@ -0,0 +1,315 @@ + +{ + "chapterUrn" : "urn:rts:video:13548828", + "episode" : { + "id" : "13435837", + "title" : "Top Models", + "lead" : "8846", + "publishedDate" : "2022-11-18T11:44:09+01:00", + "imageUrl" : "https://www.rts.ch/2022/11/18/08/05/13548823.image/16x9", + "imageTitle" : "Top Models [RTS]" + }, + "show" : { + "id" : "532539", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:show:tv:532539", + "title" : "Top Models", + "lead" : "Drames, paillettes et glamour au coeur de Los Angeles.\n\nDu lundi au vendredi à 11h45 sur RTS Un. L'épisode du jour est disponible en \"preview\" sur Play RTS 24h avant la diffusion antenne, puis à revoir durant 30 jours.", + "description" : "Drames, paillettes et glamour au coeur de Los Angeles. Du lundi au vendredi à 11h45 sur RTS Un. L'épisode du jour est disponible en \"preview\" sur Play RTS 24h avant la diffusion antenne, puis à revoir durant 30 jours.", + "imageUrl" : "https://www.rts.ch/2022/04/26/11/09/11507387.image/16x9", + "imageTitle" : "Top Models. [RTS/Monty Brinton/CBS/Courtesy of Sony Pictures of Televisions]", + "bannerImageUrl" : "https://www.rts.ch/2022/04/26/11/09/11507387.image/3x1", + "posterImageUrl" : "https://www.rts.ch/2022/04/26/10/03/12155676.image/2x3", + "posterImageIsFallbackUrl" : false, + "homepageUrl" : "https://details.rts.ch/emissions/series", + "primaryChannelId" : "143932a79bb5a123a646b68b1d1188d7ae493e5b", + "primaryChannelUrn" : "urn:rts:channel:tv:143932a79bb5a123a646b68b1d1188d7ae493e5b", + "availableAudioLanguageList" : [ { + "locale" : "fr", + "language" : "Français" + }, { + "locale" : "en", + "language" : "English" + } ], + "availableVideoQualityList" : [ "SD" ], + "audioDescriptionAvailable" : false, + "subtitlesAvailable" : true, + "multiAudioLanguagesAvailable" : true, + "topicList" : [ { + "id" : "2386", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:2386", + "title" : "Top Models" + }, { + "id" : "2383", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:2383", + "title" : "Séries" + }, { + "id" : "2026", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:2026", + "title" : "Émissions" + } ], + "allowIndexing" : false + }, + "channel" : { + "id" : "143932a79bb5a123a646b68b1d1188d7ae493e5b", + "vendor" : "RTS", + "urn" : "urn:rts:channel:tv:143932a79bb5a123a646b68b1d1188d7ae493e5b", + "title" : "RTS 1", + "imageUrl" : "https://www.rts.ch/2022/04/26/11/09/11507387.image/16x9", + "imageUrlRaw" : "https://il.srgssr.ch/image-service/dynamic/8eebe5.svg", + "imageTitle" : "Top Models. [RTS/Monty Brinton/CBS/Courtesy of Sony Pictures of Televisions]", + "transmission" : "TV" + }, + "chapterList" : [ { + "id" : "13548828", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13548828", + "title" : "8846", + "imageUrl" : "https://www.rts.ch/2022/11/18/08/05/13548823.image/16x9", + "imageTitle" : "8846 [RTS]", + "type" : "EPISODE", + "date" : "2022-11-18T11:44:09+01:00", + "duration" : 1259200, + "validFrom" : "2022-11-17T12:11:44+01:00", + "validTo" : "2022-12-18T12:11:44+01:00", + "playableAbroad" : false, + "socialCountList" : [ { + "key" : "srgView", + "value" : 4779 + }, { + "key" : "srgLike", + "value" : 0 + }, { + "key" : "fbShare", + "value" : 1 + }, { + "key" : "twitterShare", + "value" : 1 + }, { + "key" : "googleShare", + "value" : 0 + }, { + "key" : "whatsAppShare", + "value" : 3 + } ], + "displayable" : true, + "position" : 0, + "noEmbed" : true, + "analyticsData" : { + "ns_st_ep" : "8846", + "ns_st_ty" : "Video", + "ns_st_ci" : "13548828", + "ns_st_el" : "1259200", + "ns_st_cl" : "1259200", + "ns_st_sl" : "1259200", + "srg_mgeobl" : "true", + "ns_st_tp" : "1", + "ns_st_cn" : "1", + "ns_st_ct" : "vc12", + "ns_st_pn" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc" + }, + "analyticsMetadata" : { + "media_segment" : "8846", + "media_type" : "Video", + "media_segment_id" : "13548828", + "media_episode_length" : "1259", + "media_segment_length" : "1259", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "long", + "media_is_geoblocked" : "true", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:13548828" + }, + "eventData" : "$fbb22ba2ca84dbae$9405a51d787a879d706b2354bbe054bb494dcba5fd4e5769985cb35df800bd3325fe61c69bb52ac217a0e5d5b5f9b679087b77d331f294e663042184283ec77ab2e349cef8cf00412fb057769142e4cfd03afa06e50c39a5d077821cb998a3bd728622d3c9f41f836b9268736db5f9ce11bc5091de7fe319ff4e8e4d1a801c0c9a1ab2f9e435038338d3be4cb1f956ec177901145ae0bc284d967a4c6240d1a70de139c4b06b7bc85b4cafe61ac36f659bca7e579e4be756e89bbab9fb583908afa5f327d5d77e0a6b931ff245fd91c703ff97a02224b62cc09d30d4ec3ef276f994212f5a009521c58b3f9b08b95197d71b3abb0df932802466511d069318acc4205cdc1e73a144ce24caa1becb10acd9afe26c7243e8c8f4ae5bccc28aa813c99a0a8759aa7078f62e4f3c4b8c1722690e48fa1bdc1dab50aaf79d39d9a9b3f035b6d6e2fbc746600fbdbf65a41896dabf07a929fb3182dded8644fa14683a998e9e1b3ce808431d8bceda1bbbd2bf", + "resourceList" : [ { + "url" : "https://rtsvod-euwe.akamaized.net:443/2a438722-f0c3-4653-8528-6d4dc11543e7/RTSVOD-11c75e3a-3091.ism/manifest(format=mpd-time-csf,encryption=cenc)", + "drmList" : [ { + "type" : "PLAYREADY", + "licenseUrl" : "https://srg.live.ott.irdeto.com/licenseServer/playready/v1/SRG/license?contentId=RTSVOD" + }, { + "type" : "WIDEVINE", + "licenseUrl" : "https://srg.live.ott.irdeto.com/licenseServer/widevine/v1/SRG/license?contentId=RTSVOD" + } ], + "quality" : "HD", + "protocol" : "DASH", + "encoding" : "H264", + "mimeType" : "application/dash+xml", + "presentation" : "DEFAULT", + "streaming" : "DASH", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "DASH" + }, { + "locale" : "en", + "language" : "English", + "source" : "DASH" + } ], + "subtitleInformationList" : [ { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "DASH", + "type" : "SDH" + }, { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "DASH", + "type" : "SDH" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rtsvod-euwe.akamaized.net:443/2a438722-f0c3-4653-8528-6d4dc11543e7/RTSVOD-11c75e3a-3091.ism/manifest(format=mpd-time-csf,encryption=cenc)" + } + }, { + "url" : "https://rtsvod-euwe.akamaized.net:443/2a438722-f0c3-4653-8528-6d4dc11543e7/RTSVOD-11c75e3a-3091.ism/manifest(format=m3u8-aapl,encryption=cbcs-aapl)", + "drmList" : [ { + "type" : "FAIRPLAY", + "licenseUrl" : "https://srg.live.ott.irdeto.com/licenseServer/streaming/v1/SRG/getckc?contentId=RTSVOD&keyId=6470ddd4-63ab-4d1c-972a-f91b10278eba", + "certificateUrl" : "https://srg.live.ott.irdeto.com/licenseServer/streaming/v1/SRG/getcertificate?applicationId=live" + } ], + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "MPEG2_TS", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + }, { + "locale" : "en", + "language" : "English", + "source" : "HLS" + } ], + "subtitleInformationList" : [ { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "HLS", + "type" : "SDH" + }, { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "HLS", + "type" : "SDH" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rtsvod-euwe.akamaized.net:443/2a438722-f0c3-4653-8528-6d4dc11543e7/RTSVOD-11c75e3a-3091.ism/manifest(format=m3u8-aapl,encryption=cbcs-aapl)" + } + } ], + "aspectRatio" : "16:9", + "timeIntervalList" : [ { + "type" : "CLOSING_CREDITS", + "markIn" : 1233720, + "markOut" : 1259200 + } ] + } ], + "topicList" : [ { + "id" : "2386", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:2386", + "title" : "Top Models" + }, { + "id" : "2383", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:2383", + "title" : "Séries" + }, { + "id" : "2026", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:2026", + "title" : "Émissions" + } ], + "analyticsData" : { + "srg_pr_id" : "13435837", + "srg_plid" : "532539", + "ns_st_pl" : "Top Models", + "ns_st_pr" : "Top Models du 18.11.2022", + "ns_st_dt" : "2022-11-18", + "ns_st_ddt" : "2022-11-17", + "ns_st_tdt" : "2022-11-18", + "ns_st_tm" : "11:44:09", + "ns_st_tep" : "500386540", + "ns_st_li" : "0", + "ns_st_stc" : "0867", + "ns_st_st" : "RTS Online", + "ns_st_tpr" : "532539", + "ns_st_en" : "*null", + "ns_st_ge" : "*null", + "ns_st_ia" : "*null", + "ns_st_ce" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc", + "srg_unit" : "RTS", + "srg_c1" : "full", + "srg_c2" : "video_plus7_series_top-models", + "srg_c3" : "RTS 1", + "srg_tv_id" : "500386540" + }, + "analyticsMetadata" : { + "media_episode_id" : "13435837", + "media_show_id" : "532539", + "media_show" : "Top Models", + "media_episode" : "Top Models du 18.11.2022", + "media_is_livestream" : "false", + "media_full_length" : "full", + "media_enterprise_units" : "RTS", + "media_joker1" : "full", + "media_joker2" : "video_plus7_series_top-models", + "media_joker3" : "RTS 1", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_tv_id" : "500386540", + "media_thumbnail" : "https://www.rts.ch/2022/11/18/08/05/13548823.image/16x9/scale/width/344", + "media_publication_date" : "2022-11-17", + "media_publication_time" : "12:11:44", + "media_publication_datetime" : "2022-11-17T12:11:44+01:00", + "media_tv_date" : "2022-11-18", + "media_tv_time" : "11:44:09", + "media_tv_datetime" : "2022-11-18T11:44:09+01:00", + "media_content_group" : "Top Models,Séries,Émissions", + "media_channel_id" : "143932a79bb5a123a646b68b1d1188d7ae493e5b", + "media_channel_cs" : "0867", + "media_channel_name" : "RTS 1", + "media_since_publication_d" : "0", + "media_since_publication_h" : "-3" + } +} \ No newline at end of file diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_live.dataset/Contents.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_live.dataset/Contents.json new file mode 100644 index 00000000..1f3ee82c --- /dev/null +++ b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_live.dataset/Contents.json @@ -0,0 +1,12 @@ +{ + "data" : [ + { + "filename" : "urn_rts_audio_3262320.json", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_live.dataset/urn_rts_audio_3262320.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_live.dataset/urn_rts_audio_3262320.json new file mode 100644 index 00000000..8971ba65 --- /dev/null +++ b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_live.dataset/urn_rts_audio_3262320.json @@ -0,0 +1,143 @@ +{ + "chapterUrn" : "urn:rts:audio:3262320", + "episode" : { + "id" : "3262332", + "title" : "La 1ère en direct", + "publishedDate" : "2011-07-11T14:18:47+02:00", + "imageUrl" : "https://www.rts.ch/2020/05/18/14/24/11333295.image/16x9", + "imageTitle" : "Chaîne La 1ère" + }, + "show" : { + "id" : "3262333", + "vendor" : "RTS", + "transmission" : "RADIO", + "urn" : "urn:rts:show:radio:3262333", + "title" : "La 1ère en direct", + "imageUrl" : "https://www.rts.ch/2020/05/18/14/24/11333295.image/16x9", + "imageTitle" : "Chaîne La 1ère", + "bannerImageUrl" : "https://www.rts.ch/2020/05/18/14/24/11333295.image/3x1", + "posterImageUrl" : "https://ws.srf.ch/asset/image/audio/e0322b37-5697-474d-93ac-19a4044a6a24/POSTER.jpg", + "posterImageIsFallbackUrl" : true, + "primaryChannelId" : "a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", + "primaryChannelUrn" : "urn:rts:channel:radio:a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", + "audioDescriptionAvailable" : false, + "subtitlesAvailable" : false, + "multiAudioLanguagesAvailable" : false, + "allowIndexing" : false + }, + "channel" : { + "id" : "a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", + "vendor" : "RTS", + "urn" : "urn:rts:channel:radio:a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", + "title" : "La 1ere", + "imageUrl" : "https://www.rts.ch/2020/05/18/14/24/11333295.image/16x9", + "imageTitle" : "Chaîne La 1ère", + "transmission" : "RADIO" + }, + "chapterList" : [ { + "id" : "3262320", + "mediaType" : "AUDIO", + "vendor" : "RTS", + "urn" : "urn:rts:audio:3262320", + "title" : "La 1ère en direct", + "imageUrl" : "https://www.rts.ch/2020/05/18/14/24/11333295.image/16x9", + "imageTitle" : "Chaîne La 1ère", + "type" : "LIVESTREAM", + "date" : "2011-07-11T14:18:47+02:00", + "duration" : 0, + "playableAbroad" : true, + "displayable" : true, + "position" : 0, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Livestream", + "media_type" : "Audio", + "media_segment_id" : "3262320", + "media_episode_length" : "0", + "media_segment_length" : "0", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "infinit.livestream", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:audio:3262320" + }, + "fullLengthMarkIn" : 0, + "fullLengthMarkOut" : 0, + "resourceList" : [ { + "url" : "https://lsaplus.swisstxt.ch/audio/la-1ere_96.stream/playlist.m3u8?", + "quality" : "HD", + "protocol" : "HLS-DVR", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : true, + "live" : true, + "mediaContainer" : "MPEG2_TS", + "audioCodec" : "AAC", + "videoCodec" : "NONE", + "tokenType" : "NONE", + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://lsaplus.swisstxt.ch/audio/la-1ere_96.stream/playlist.m3u8?" + }, + "streamOffset" : 55000 + } ] + } ], + "analyticsData" : { + "srg_pr_id" : "3262332", + "srg_plid" : "3262333", + "ns_st_pl" : "Livestream", + "ns_st_pr" : "La 1ère en direct", + "ns_st_dt" : "2011-07-11", + "ns_st_ddt" : "2011-07-11", + "ns_st_tdt" : "2011-07-11", + "ns_st_tm" : "14:18:47", + "ns_st_tep" : "*null", + "ns_st_li" : "1", + "ns_st_stc" : "0867", + "ns_st_st" : "La 1ere", + "ns_st_tpr" : "1423878", + "ns_st_en" : "*null", + "ns_st_ge" : "*null", + "ns_st_ia" : "*null", + "ns_st_ce" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc", + "srg_unit" : "RTS", + "srg_c1" : "live", + "srg_c2" : "rts.ch_audio_la-1ere", + "srg_c3" : "LA 1ÈRE", + "srg_aod_prid" : "3262332" + }, + "analyticsMetadata" : { + "media_episode_id" : "3262332", + "media_show_id" : "1423878", + "media_show" : "On en parle", + "media_episode" : "La 1ère en direct", + "media_is_livestream" : "true", + "media_full_length" : "full", + "media_enterprise_units" : "RTS", + "media_joker1" : "live", + "media_joker2" : "rts.ch_audio_la-1ere", + "media_joker3" : "LA 1ÈRE", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_thumbnail" : "https://www.rts.ch/2020/05/18/14/24/11333295.image/16x9/scale/width/344", + "media_publication_date" : "2011-07-11", + "media_publication_time" : "14:18:47", + "media_publication_datetime" : "2011-07-11T14:18:47+02:00", + "media_tv_date" : "2011-07-11", + "media_tv_time" : "14:18:47", + "media_tv_datetime" : "2011-07-11T14:18:47+02:00", + "media_content_group" : "La 1ère", + "media_channel_id" : "a9e7621504c6959e35c3ecbe7f6bed0446cdf8da", + "media_channel_cs" : "0867", + "media_channel_name" : "La 1ere", + "media_since_publication_d" : "4074", + "media_since_publication_h" : "97795" + } +} \ No newline at end of file diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_missingAnalytics.dataset/Contents.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_missingAnalytics.dataset/Contents.json new file mode 100644 index 00000000..56c7e33d --- /dev/null +++ b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_missingAnalytics.dataset/Contents.json @@ -0,0 +1,12 @@ +{ + "data" : [ + { + "filename" : "urn_rts_video_13360574.json", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_missingAnalytics.dataset/urn_rts_video_13360574.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_missingAnalytics.dataset/urn_rts_video_13360574.json new file mode 100644 index 00000000..51f9103a --- /dev/null +++ b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_missingAnalytics.dataset/urn_rts_video_13360574.json @@ -0,0 +1,179 @@ +{ + "chapterUrn" : "urn:rts:video:13360574", + "episode" : { + "id" : "13360565", + "title" : "Yadebat", + "publishedDate" : "2022-09-05T16:30:00+02:00", + "imageUrl" : "https://www.rts.ch/2022/09/05/18/20/13360573.image/16x9", + "imageTitle" : "On réunit des ex après leur rupture [RTS]" + }, + "show" : { + "id" : "10174267", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:show:tv:10174267", + "title" : "Yadebat", + "lead" : "Une série qui te donne la parole, pour laisser entendre ton avis sur les débats de société animée par Melissa.", + "description" : "Une série qui te donne la parole, pour laisser entendre ton avis sur les débats de société animée par Melissa.", + "imageUrl" : "https://www.rts.ch/2020/01/10/11/14/10520588.image/16x9", + "imageTitle" : "Yadebat - Tataki [RTS]", + "posterImageUrl" : "https://ws.srf.ch/asset/image/audio/e0322b37-5697-474d-93ac-19a4044a6a24/POSTER.jpg", + "posterImageIsFallbackUrl" : true, + "audioDescriptionAvailable" : false, + "subtitlesAvailable" : false, + "multiAudioLanguagesAvailable" : false, + "topicList" : [ { + "id" : "59952", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:59952", + "title" : "Yadebat" + }, { + "id" : "54537", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:54537", + "title" : "Tataki" + } ], + "allowIndexing" : false + }, + "chapterList" : [ { + "id" : "13360574", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13360574", + "title" : "On réunit des ex après leur rupture", + "description" : "Dans ce nouvel épisode de YADEBAT, Mélissa réunit 3 couples qui se sont séparés récemment. Elles les a questionné en face à face pour connaître leurs différents ressentis et réactions.", + "imageUrl" : "https://www.rts.ch/2022/09/05/18/20/13360573.image/16x9", + "imageTitle" : "On réunit des ex après leur rupture [RTS]", + "type" : "EPISODE", + "date" : "2022-09-05T16:30:00+02:00", + "duration" : 902360, + "validFrom" : "2022-09-05T16:30:00+02:00", + "validTo" : "2100-01-01T23:59:59+01:00", + "playableAbroad" : true, + "socialCountList" : [ { + "key" : "srgView", + "value" : 17 + }, { + "key" : "srgLike", + "value" : 0 + }, { + "key" : "fbShare", + "value" : 0 + }, { + "key" : "twitterShare", + "value" : 0 + }, { + "key" : "googleShare", + "value" : 0 + }, { + "key" : "whatsAppShare", + "value" : 0 + } ], + "displayable" : true, + "position" : 0, + "noEmbed" : false, + "eventData" : "$27549332a83ca6ac$64b181b51953d6ed48de11986513e2f93922eb3d4315e6d5ad8189e1fe38d933c011ba7ded29e3d757ba1e566e76d65d97c8f0cd0735cc47b1cb3e5cf091c89c8d6c18ff31e19e3d7509cbf826c0c156fd10b8908ebe481aaf7282de102e92342ffb36b52df58453b40d64883f720fb3eddd38b595ddf6961acc4bc33abb3f2c49b7d90b52a35239f0209caa3ebc532e6a95315bd382bc08f2b78af2ec23c3f7e7917de924cb7f85b8aedac2fdafd027fe3880e07f3a0ba05f43d0ce601a1d2c7b756012c8820e12eef32fb9c0e1f532cce31cf1be738a9d6c05555857700fc5e1f0e1bd9886f06c55f5e731a66daa09be035e5ef53a4da159a7d3943a67ebaa1ac1302ad3ff046739eb185d78737e1543e7788d4edd9858af0e6846460106e954e8f1176cf60876aad36646c11a3b3a824ab54433f99c4576accea86e2b853c", + "resourceList" : [ { + "url" : "https://rts-vod-amd.akamaized.net/ww/13360574/447e0958-42a8-3bdd-8365-95d54031e605/master.m3u8", + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-vod-amd.akamaized.net/ww/13360574/447e0958-42a8-3bdd-8365-95d54031e605/master.m3u8" + } + } ], + "aspectRatio" : "16:9", + "spriteSheet" : { + "urn" : "urn:rts:video:13360574", + "rows" : 23, + "columns" : 20, + "thumbnailHeight" : 84, + "thumbnailWidth" : 150, + "interval" : 2000, + "url" : "https://il.srgssr.ch/spritesheet/urn/rts/video/13360574/sprite-13360574.jpeg" + } + } ], + "topicList" : [ { + "id" : "59952", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:59952", + "title" : "Yadebat" + }, { + "id" : "54537", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:54537", + "title" : "Tataki" + } ], + "analyticsData" : { + "srg_pr_id" : "13360565", + "srg_plid" : "10174267", + "ns_st_pl" : "Yadebat", + "ns_st_pr" : "Yadebat du 05.09.2022", + "ns_st_dt" : "2022-09-05", + "ns_st_ddt" : "2022-09-05", + "ns_st_tdt" : "*null", + "ns_st_tm" : "*null", + "ns_st_tep" : "500418168", + "ns_st_li" : "0", + "ns_st_stc" : "0867", + "ns_st_st" : "RTS Online", + "ns_st_tpr" : "10174267", + "ns_st_en" : "*null", + "ns_st_ge" : "*null", + "ns_st_ia" : "*null", + "ns_st_ce" : "1", + "ns_st_cdm" : "eo", + "ns_st_cmt" : "ec", + "srg_unit" : "RTS", + "srg_c1" : "full", + "srg_c2" : "video_tataki_yadebat", + "srg_c3" : "RTS.ch", + "srg_tv_id" : "500418168" + }, + "analyticsMetadata" : { + "media_episode_id" : "13360565", + "media_show_id" : "10174267", + "media_show" : "Yadebat", + "media_episode" : "Yadebat du 05.09.2022", + "media_is_livestream" : "false", + "media_full_length" : "full", + "media_enterprise_units" : "RTS", + "media_joker1" : "full", + "media_joker2" : "video_tataki_yadebat", + "media_joker3" : "RTS.ch", + "media_is_web_only" : "true", + "media_production_source" : "produced.for.web", + "media_tv_id" : "500418168", + "media_thumbnail" : "https://www.rts.ch/2022/09/05/18/20/13360573.image/16x9/scale/width/344", + "media_publication_date" : "2022-09-05", + "media_publication_time" : "16:30:00", + "media_publication_datetime" : "2022-09-05T16:30:00+02:00", + "media_content_group" : "Yadebat,Tataki", + "media_since_publication_d" : "0", + "media_since_publication_h" : "19" + } +} \ No newline at end of file diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_mixed.dataset/Contents.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_mixed.dataset/Contents.json new file mode 100644 index 00000000..be7832a3 --- /dev/null +++ b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_mixed.dataset/Contents.json @@ -0,0 +1,12 @@ +{ + "data" : [ + { + "filename" : "urn_rts_video_14827796.json", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_mixed.dataset/urn_rts_video_14827796.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_mixed.dataset/urn_rts_video_14827796.json new file mode 100644 index 00000000..be1d38a6 --- /dev/null +++ b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_mixed.dataset/urn_rts_video_14827796.json @@ -0,0 +1,1258 @@ +{ + "chapterUrn" : "urn:rts:video:14827796", + "episode" : { + "id" : "14718074", + "title" : "Forum", + "lead" : "Forum du 10.04.2024", + "publishedDate" : "2024-04-10T18:00:00+02:00", + "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827795.image/16x9", + "imageTitle" : "Forum [RTS]" + }, + "show" : { + "id" : "9933104", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:show:tv:9933104", + "title" : "Forum", + "lead" : "7 jours sur 7, Forum questionne en direct les acteurs de l’actualité, ouvre le débat sur les controverses qui animent la vie politique, culturelle et économique.", + "description" : "7 jours sur 7, Forum questionne en direct les acteurs de l'actualité, ouvre le débat sur les controverses qui animent la vie politique, culturelle et économique. C'est un lieu d'écoute, d'échanges, de remise en question. Forum propose chaque soir un regard attentif et acéré sur l’actualité suisse et internationale.", + "imageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/16x9", + "imageTitle" : "Logo Forum [RTS]", + "bannerImageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/3x1", + "posterImageUrl" : "https://www.rts.ch/2024/02/22/13/58/12399002.image/2x3", + "posterImageIsFallbackUrl" : false, + "homepageUrl" : "https://details.rts.ch/la-1ere/programmes/forum/", + "primaryChannelId" : "d7dfff28deee44e1d3c49a3d37d36d492b29671b", + "primaryChannelUrn" : "urn:rts:channel:tv:d7dfff28deee44e1d3c49a3d37d36d492b29671b", + "availableAudioLanguageList" : [ { + "locale" : "fr", + "language" : "Français" + } ], + "availableVideoQualityList" : [ "SD", "HD" ], + "audioDescriptionAvailable" : false, + "subtitlesAvailable" : true, + "multiAudioLanguagesAvailable" : false, + "topicList" : [ { + "id" : "49683", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:49683", + "title" : "Forum" + }, { + "id" : "16202", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:16202", + "title" : "La 1ère" + } ], + "allowIndexing" : false + }, + "channel" : { + "id" : "d7dfff28deee44e1d3c49a3d37d36d492b29671b", + "vendor" : "RTS", + "urn" : "urn:rts:channel:tv:d7dfff28deee44e1d3c49a3d37d36d492b29671b", + "title" : "RTS 2", + "imageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/16x9", + "imageUrlRaw" : "https://il.srgssr.ch/image-service/dynamic/c915e35.svg", + "imageTitle" : "Logo Forum [RTS]", + "transmission" : "TV" + }, + "chapterList" : [ { + "id" : "14827796", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:14827796", + "title" : "Forum (vidéo) - Présenté par Thibaut Schaller et Renaud Malik", + "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827795.image/16x9", + "imageTitle" : "Forum (vidéo) - Présenté par Thibaut Schaller et Renaud Malik [RTS]", + "type" : "EPISODE", + "date" : "2024-04-10T18:00:00+02:00", + "duration" : 3599720, + "validFrom" : "2024-04-10T19:00:00+02:00", + "playableAbroad" : true, + "socialCountList" : [ { + "key" : "srgView", + "value" : 1212 + }, { + "key" : "srgLike", + "value" : 0 + }, { + "key" : "fbShare", + "value" : 0 + }, { + "key" : "twitterShare", + "value" : 0 + }, { + "key" : "googleShare", + "value" : 0 + }, { + "key" : "whatsAppShare", + "value" : 2 + } ], + "displayable" : true, + "position" : 0, + "noEmbed" : false, + "analyticsData" : { + "ns_st_ep" : "Forum (vidéo) - Présenté par Thibaut Schaller et Renaud Malik", + "ns_st_ty" : "Video", + "ns_st_ci" : "14827796", + "ns_st_el" : "3599720", + "ns_st_cl" : "3599720", + "ns_st_sl" : "3599720", + "srg_mgeobl" : "false", + "ns_st_tp" : "1", + "ns_st_cn" : "1", + "ns_st_ct" : "vc12", + "ns_st_pn" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc" + }, + "analyticsMetadata" : { + "media_segment" : "Forum (vidéo) - Présenté par Thibaut Schaller et Renaud Malik", + "media_type" : "Video", + "media_segment_id" : "14827796", + "media_episode_length" : "3600", + "media_segment_length" : "3600", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "long", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:14827796", + "media_sub_set_id" : "EPISODE", + "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" + }, + "eventData" : "$639edb61591433fa$39dd7d5589c57c9d9e934d9b277655679f3ccc1188abc26332eb015bf8eb84749d1d4eade6218a91e7fa548fb8de0f5304154a3212400eb6e288c54acb84175d5176a476c815cabd7786be1e6f1fb06a9f6cd699995fb9e0338af76422d6b8cfd56f32634f999dff81476e0eb174d284ea765538b8cd7e9eea02572602725f07a08b873078b5ee9e76231fe34cb6bc2ee20f961289e9f59fb8b87255d04d2938f23e51aa62a340545f28850c9b272644b0de206dd2664284733ce12297efd04c0ce6da56f3cfa50bb82380510ea739d0ccb7a83dcfd5ec198dae564d0ed6c58315c9317342395edade408b0abba2c4935c924663a4ad37e8c606b77bcb9a54ecfa5136f2f86e6e9a0074b18e3cdb0c211a26e8a48fdab82a1fb9d375219f986bbfd51fe1ae40b412b206027e47c0f5c50cf5b1b3bd0607b047b15cc5c75aeb4dc9ba90f24b2e3ae4105d524fbff21c2b4128adeb3d51d27d334a629f75148b8a286714d05e57b742e4eb09b82e126777", + "resourceList" : [ { + "url" : "https://rts-vod-amd.akamaized.net/ww/14827796/a07f41f3-987b-3d1e-af10-b9e7220429db/master.m3u8", + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + } ], + "subtitleInformationList" : [ { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "HLS", + "type" : "SDH" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827796/a07f41f3-987b-3d1e-af10-b9e7220429db/master.m3u8" + } + } ], + "aspectRatio" : "16:9", + "spriteSheet" : { + "urn" : "urn:rts:video:14827796", + "rows" : 30, + "columns" : 20, + "thumbnailHeight" : 84, + "thumbnailWidth" : 150, + "interval" : 6000, + "url" : "https://il.srgssr.ch/spritesheet/urn/rts/video/14827796/sprite-14827796.jpeg" + } + }, { + "id" : "14827774", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:14827774", + "title" : "La Suisse organise une conférence de haut niveau sur la paix en Ukraine qui se déroulera mi-juin", + "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827765.image/16x9", + "imageTitle" : "La Suisse organise une conférence de haut niveau sur la paix en Ukraine qui se déroulera mi-juin [RTS]", + "type" : "CLIP", + "date" : "2024-04-10T18:00:00+02:00", + "duration" : 208800, + "validFrom" : "2024-04-10T19:00:00+02:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:14827796", + "position" : 1, + "noEmbed" : false, + "analyticsData" : { + "ns_st_ep" : "La Suisse organise une conférence de haut niveau sur la paix en Ukraine qui se déroulera mi-juin", + "ns_st_ty" : "Video", + "ns_st_ci" : "14827774", + "ns_st_el" : "208800", + "ns_st_cl" : "208800", + "ns_st_sl" : "208800", + "srg_mgeobl" : "false", + "ns_st_tp" : "1", + "ns_st_cn" : "1", + "ns_st_ct" : "vc11", + "ns_st_pn" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc" + }, + "analyticsMetadata" : { + "media_segment" : "La Suisse organise une conférence de haut niveau sur la paix en Ukraine qui se déroulera mi-juin", + "media_type" : "Video", + "media_segment_id" : "14827774", + "media_episode_length" : "209", + "media_segment_length" : "209", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:14827774", + "media_sub_set_id" : "CLIP", + "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" + }, + "eventData" : "$3d2421a5fd090279$f1b0b1930c0e0152328c4bede0613736bad4bff09afc6b35fbe2824b8dda4b36ceeebde74eb0f673f6bf0c63bb5d8e943014b8f08fb9678e82c47c205aa66ff887c808e5160e93c610e2da6677d4e01daa331a8bac1d47103ab130151385d47c7519b994f643035e58aee846dad0492b95795ebec6a3ddc4b17be7a03b4e894e43662db6784e803aeec556fd5fb329735f6ca1a4a3a645bf5c647854807d1d655a1ab2e49b8461e42df64afe3aa11649074b7eff32f8d9dcd3c24a4b82d67775987485392f460281d789acff0663d3506dc172775ac74b15ca678a3468a93b52a432c287b4c588ab850149210b224a29c55fbfeeda57c5dd258ce3e33cce59770e506ca75b22e57831deb364554977e6ab9d00c7cccd49d6ef7f0e54cd6bfed5c53d3ea030e6b78fd65d65ea88a5dff5a6c34a8f3eed897bd3363cb2f5fda76661f56b311831c4fd3c039f3d6872c02b1c97e61ba312585965f2ff7f11a0a80c3bad513b8a3dc0b2f723700d1adabe86", + "fullLengthMarkIn" : 99000, + "fullLengthMarkOut" : 307800, + "resourceList" : [ { + "url" : "https://rts-vod-amd.akamaized.net/ww/14827774/d39bbda9-5f74-3c41-a670-d55be1a1f0e3/master.m3u8", + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + } ], + "subtitleInformationList" : [ { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "HLS", + "type" : "SDH" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827774/d39bbda9-5f74-3c41-a670-d55be1a1f0e3/master.m3u8" + } + } ], + "aspectRatio" : "16:9" + }, { + "id" : "14827776", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:14827776", + "title" : "Conférence de haut niveau sur la paix en Ukraine: les réactions de Cédric Wermuth et Pascal Broulis", + "description" : "Réactions de Cédric Wermuth, co-président du Parti socialiste suisse, et Pascal Broulis, conseiller aux Etats PLR vaudois.", + "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827775.image/16x9", + "imageTitle" : "Conférence de haut niveau sur la paix en Ukraine: les réactions de Cédric Wermuth et Pascal Broulis [RTS]", + "type" : "CLIP", + "date" : "2024-04-10T18:00:00+02:00", + "duration" : 135200, + "validFrom" : "2024-04-10T19:00:00+02:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:14827796", + "position" : 2, + "noEmbed" : false, + "analyticsData" : { + "ns_st_ep" : "Conférence de haut niveau sur la paix en Ukraine: les réactions de Cédric Wermuth et Pascal Broulis", + "ns_st_ty" : "Video", + "ns_st_ci" : "14827776", + "ns_st_el" : "135200", + "ns_st_cl" : "135200", + "ns_st_sl" : "135200", + "srg_mgeobl" : "false", + "ns_st_tp" : "1", + "ns_st_cn" : "1", + "ns_st_ct" : "vc11", + "ns_st_pn" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc" + }, + "analyticsMetadata" : { + "media_segment" : "Conférence de haut niveau sur la paix en Ukraine: les réactions de Cédric Wermuth et Pascal Broulis", + "media_type" : "Video", + "media_segment_id" : "14827776", + "media_episode_length" : "135", + "media_segment_length" : "135", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:14827776", + "media_sub_set_id" : "CLIP", + "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" + }, + "eventData" : "$210802670c7f1a3d$710d33293247a3c70783e81fd407a86ee614cda7351b3039aa31ed5f0b1384ec1484092aa0c186828ce2022949336c51b4ccc9434526d02e41b6bdc7882a27206b879dbd73c0d18b21ef3ea018f8a9df9613e1980e14d1f079a8379fd4163c7533831c8b4988e078fa50eb54520ac9181ca02acda9be74dcf2d03862bb547aba11389a8025fadc6771a6e13628dcb9a23809d65d8265f33e692724c0af89def4cfb12f67bdc3fb2c48f38c021c26e105d480162e8f7300dfe0d5b8e1945f1a0a8891c1d828b3c9d606763b4137a7536282870c2f297f5b1b12db616226fe42501c1604aef292fa78f7b9856398307174082e84fd5351635a0a824352c73340b53cbc041cd4fda9dfe2da63c0182f4b5e3398ed9debec5f3d369bda7bb58b5123625358ff825d940d823691454ac208ed7fa04a62272d06de690a2a8ba19103ea8a9747a972cc01a17f120c99a6e3a993854ebd513e5e9907255317c242528da31621298ab8d2e725ff49b48be7969324", + "fullLengthMarkIn" : 307800, + "fullLengthMarkOut" : 443000, + "resourceList" : [ { + "url" : "https://rts-vod-amd.akamaized.net/ww/14827776/3ecdae9b-b4f9-3492-9b16-4b2c7fa09706/master.m3u8", + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + } ], + "subtitleInformationList" : [ { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "HLS", + "type" : "SDH" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827776/3ecdae9b-b4f9-3492-9b16-4b2c7fa09706/master.m3u8" + } + } ], + "aspectRatio" : "16:9" + }, { + "id" : "14827778", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:14827778", + "title" : "Le Conseil fédéral propose des mesures pour mieux encadrer les grandes banques", + "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827777.image/16x9", + "imageTitle" : "Le Conseil fédéral propose des mesures pour mieux encadrer les grandes banques [RTS]", + "type" : "CLIP", + "date" : "2024-04-10T18:00:00+02:00", + "duration" : 215680, + "validFrom" : "2024-04-10T19:00:00+02:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:14827796", + "position" : 3, + "noEmbed" : false, + "analyticsData" : { + "ns_st_ep" : "Le Conseil fédéral propose des mesures pour mieux encadrer les grandes banques", + "ns_st_ty" : "Video", + "ns_st_ci" : "14827778", + "ns_st_el" : "215680", + "ns_st_cl" : "215680", + "ns_st_sl" : "215680", + "srg_mgeobl" : "false", + "ns_st_tp" : "1", + "ns_st_cn" : "1", + "ns_st_ct" : "vc11", + "ns_st_pn" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc" + }, + "analyticsMetadata" : { + "media_segment" : "Le Conseil fédéral propose des mesures pour mieux encadrer les grandes banques", + "media_type" : "Video", + "media_segment_id" : "14827778", + "media_episode_length" : "216", + "media_segment_length" : "216", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:14827778", + "media_sub_set_id" : "CLIP", + "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" + }, + "eventData" : "$068c9a1aad2071b1$e23321f435dd224b565339c3e79a5d70bd795bbedbe5d325618e06d353e522e945f9a6e5ae4fe4c9329aed070a53cbf7ef2232c4357149b7049384c2b66f75c9b58605cc32d00fbbbb1505d14cae448a35655e772f00780b67538ac74c37306328a2cbb668663b24f71aff30803bafc8c6aea3028bee42335598ce904bdd084aed55eb69054d933ad063195a40121f833c54248ebeedf027957fb049d72f8817274cb395da9a58c4a74a9cf5ddef3090496ae6afcff92a9a9e0555dd3aba8d4dee4443b34599ca286d5dc788da5ecec07062afac5959bb997833d899b46bdbfd69c7aa1f5010354f223b2cd5afb77c930cafe2d3d9c0e421d2aeb5ed12b96c61e5a7937b40de5b44a0a85e67ceaad94e356ae3847fa4c24e98b915dd43507adc5e50f8885066bb68142b642641ac8e0648aae561a2e2e99d211551063be17003b7a779606668acc98beff509fc82bf1f8356b44c08e480ece9f1613651de6ea2e2e9b93a79b7953df9fd89d0a9f4162b", + "fullLengthMarkIn" : 443000, + "fullLengthMarkOut" : 658680, + "resourceList" : [ { + "url" : "https://rts-vod-amd.akamaized.net/ww/14827778/7b0ecad4-504d-3c45-9639-f5a8d1d6e3b0/master.m3u8", + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + } ], + "subtitleInformationList" : [ { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "HLS", + "type" : "SDH" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827778/7b0ecad4-504d-3c45-9639-f5a8d1d6e3b0/master.m3u8" + } + } ], + "aspectRatio" : "16:9" + }, { + "id" : "14827780", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:14827780", + "title" : "Mesures pour encadrer les grandes banques: réactions de de Cédric Wermuth et Pascal Broulis", + "description" : "Réactions de Cédric Wermuth, co-président du Parti socialiste suisse, et Pascal Broulis, conseiller aux Etats PLR vaudois.", + "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827779.image/16x9", + "imageTitle" : "Mesures pour encadrer les grandes banques: réactions de de Cédric Wermuth et Pascal Broulis [RTS]", + "type" : "CLIP", + "date" : "2024-04-10T18:00:00+02:00", + "duration" : 431720, + "validFrom" : "2024-04-10T19:00:00+02:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:14827796", + "position" : 4, + "noEmbed" : false, + "analyticsData" : { + "ns_st_ep" : "Mesures pour encadrer les grandes banques: réactions de de Cédric Wermuth et Pascal Broulis", + "ns_st_ty" : "Video", + "ns_st_ci" : "14827780", + "ns_st_el" : "431720", + "ns_st_cl" : "431720", + "ns_st_sl" : "431720", + "srg_mgeobl" : "false", + "ns_st_tp" : "1", + "ns_st_cn" : "1", + "ns_st_ct" : "vc11", + "ns_st_pn" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc" + }, + "analyticsMetadata" : { + "media_segment" : "Mesures pour encadrer les grandes banques: réactions de de Cédric Wermuth et Pascal Broulis", + "media_type" : "Video", + "media_segment_id" : "14827780", + "media_episode_length" : "432", + "media_segment_length" : "432", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:14827780", + "media_sub_set_id" : "CLIP", + "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" + }, + "eventData" : "$6e3534588d6725f4$4703f9023b457157ed443aa7b95fa82dd3f7a70d60dabecced85efebda6f83a468e3375da9dae4a8327eedf7f62305e318b50cddb9d92d7d28119086670b06108021e18ce8d37e8e000630c47f9a94e4b601b2917d8693b7d997d15e9948468f97c491375e6cb40dc8409fcff1c60206d03c0397b889260eb9b0b159fdc8baa32e7d654cca711037d7ad651dbd52f22b84faf5d8ea350d53fd9f87490bb41b3aae02cf8f2c84b96bfc1ccbb5efd44fee7940c554539c19aeb08f4a6435b53a217412807d8c78f03c4b322b8586edb5c04f2fd25d7f54eccf063dc1ff02c07c3aef43ca0026718943d25f537b87eb7ff129eb63f1dd1ed271559817e92ef8f6d30074b4dc41dcb86b9699934b371400e70d95f1a144ff80155c73281d238dc95fa2ab7c18c030d3579c7b42b0c635194cc22ee249eea8323c710dcb3c2f12827c3b7d34cfeee716b29f6f603e6f7bc189dc40e620832d7891d7d35f489ea820626cb7ab263c0ca8ceaf0bd94b55910784", + "fullLengthMarkIn" : 658680, + "fullLengthMarkOut" : 1090400, + "resourceList" : [ { + "url" : "https://rts-vod-amd.akamaized.net/ww/14827780/1c34db09-0de5-3c67-acb0-017db627e3a0/master.m3u8", + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + } ], + "subtitleInformationList" : [ { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "HLS", + "type" : "SDH" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827780/1c34db09-0de5-3c67-acb0-017db627e3a0/master.m3u8" + } + } ], + "aspectRatio" : "16:9" + }, { + "id" : "14827782", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:14827782", + "title" : "Le recours à l'intelligence artificielle par l'armée israélienne: interview de Jean-Marc Rickli", + "description" : "Interview de Jean-Marc Rickli, responsable du département des risques mondiaux et émergents au centre genevois de politique de sécurité (GCSP).", + "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827781.image/16x9", + "imageTitle" : "Le recours à l'intelligence artificielle par l'armée israélienne: interview de Jean-Marc Rickli [RTS]", + "type" : "CLIP", + "date" : "2024-04-10T18:00:00+02:00", + "duration" : 389200, + "validFrom" : "2024-04-10T19:00:00+02:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:14827796", + "position" : 5, + "noEmbed" : false, + "analyticsData" : { + "ns_st_ep" : "Le recours à l'intelligence artificielle par l'armée israélienne: interview de Jean-Marc Rickli", + "ns_st_ty" : "Video", + "ns_st_ci" : "14827782", + "ns_st_el" : "389200", + "ns_st_cl" : "389200", + "ns_st_sl" : "389200", + "srg_mgeobl" : "false", + "ns_st_tp" : "1", + "ns_st_cn" : "1", + "ns_st_ct" : "vc11", + "ns_st_pn" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc" + }, + "analyticsMetadata" : { + "media_segment" : "Le recours à l'intelligence artificielle par l'armée israélienne: interview de Jean-Marc Rickli", + "media_type" : "Video", + "media_segment_id" : "14827782", + "media_episode_length" : "389", + "media_segment_length" : "389", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:14827782", + "media_sub_set_id" : "CLIP", + "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" + }, + "eventData" : "$5ddcf9d04706d658$e5ae57d14ede33563e7155b90bdf29a6c962a6722fc91671678a47d0866d80da9a1b8b6d95d78bfaa81e5db63f24f9571b685ba492542696157005d4441dbd494e13bee4d890cccf7ca0758d0420fd9c72dc9570d3c76c68ab73177b640886b68d7ae30ba4718bc74fe08d53fb150b2d4604657cc8b5b5c0f8de1f5d565c6a8ac93c302d01f654a63e9ff2f38b9dde31e253bfa2a8cd32a540236ca0b4ff2dbb26cf4528666e711441b1dd80378263cbf03a096b0b38873988ea33d92fe86ae8b8a096f9e1fbf38d9f92d3b56664787d8d7ece67b8250d8597e111e3640a157e8d983bec6a8e9899ebc8e3e22ba950c0506d36f07fc5dc4e8dcee0a3141f8cbcfdf0c1a87d1e494d2b5942eac62dbd7ef09225761db130b1ded7de4df6bed9ae6f51f90ae2841462c7a15615bce14836d670d01b76e4ab134aacb619f38ad2f5765c49af7cec668978cca36e8dc99ec270921496cfdd879b27696fe348f7874c34887cfe881715a32724d9c9958b372e", + "fullLengthMarkIn" : 1090400, + "fullLengthMarkOut" : 1479600, + "resourceList" : [ { + "url" : "https://rts-vod-amd.akamaized.net/ww/14827782/11ab086f-9d6c-3d0f-9d2d-ab838ad3f19d/master.m3u8", + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + } ], + "subtitleInformationList" : [ { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "HLS", + "type" : "SDH" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827782/11ab086f-9d6c-3d0f-9d2d-ab838ad3f19d/master.m3u8" + } + } ], + "aspectRatio" : "16:9" + }, { + "id" : "14827784", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:14827784", + "title" : "Pacte migratoire européen: les Vingt-Sept durcissent les contrôles des arrivées aux frontières", + "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827783.image/16x9", + "imageTitle" : "Pacte migratoire européen: les Vingt-Sept durcissent les contrôles des arrivées aux frontières [RTS]", + "type" : "CLIP", + "date" : "2024-04-10T18:00:00+02:00", + "duration" : 146640, + "validFrom" : "2024-04-10T19:00:00+02:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:14827796", + "position" : 6, + "noEmbed" : false, + "analyticsData" : { + "ns_st_ep" : "Pacte migratoire européen: les Vingt-Sept durcissent les contrôles des arrivées aux frontières", + "ns_st_ty" : "Video", + "ns_st_ci" : "14827784", + "ns_st_el" : "146640", + "ns_st_cl" : "146640", + "ns_st_sl" : "146640", + "srg_mgeobl" : "false", + "ns_st_tp" : "1", + "ns_st_cn" : "1", + "ns_st_ct" : "vc11", + "ns_st_pn" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc" + }, + "analyticsMetadata" : { + "media_segment" : "Pacte migratoire européen: les Vingt-Sept durcissent les contrôles des arrivées aux frontières", + "media_type" : "Video", + "media_segment_id" : "14827784", + "media_episode_length" : "147", + "media_segment_length" : "147", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:14827784", + "media_sub_set_id" : "CLIP", + "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" + }, + "eventData" : "$7a293f98d6d07f8b$c9d11b497a448ec03e333750802081d0b53616f8e53f7cdd51c6486ac2cba23360936e017cb4347b8bb4d72161855972c603454ddc5b432e8606034ef571d2314eb287246663590cd2c426d8f73732cac751f17d9fea844cb1501b6c503046bb7fef09fee75ba43919313bddd04a3724f21ee832e99193ef90b62fce096fa9906888f02cce712964ec13bf4be2cdb5b31d1dacb665d9cd007e1a3c74be7fd61377bf5fb65459fd3e0ddefd308e669e4f38a50c5bba6c3271d8aa8c618b9fa915fc57d8c8d83333b51866c5feab4344d50b447cfdef60655d506c59f2b496f9415eae94eda6c300f83ef4bdb76257aaac89d8dc50bb63fe06bc6f162bb872e22c97c9b733ce94cb82bfc94d5334e713f9f84cce3a332a8d6a95fec4c7c82cc7581b40c7476d6ea3e0debaf5578c0b460d340caf8f14d57e71c6fbe0e24d14370a8ebd9aa0c14b906a054ffbf70fc81163837f285654e44c708a6cfeb37308fa000db09ee396bfb3e7e1248ee2a97f98af", + "fullLengthMarkIn" : 1479600, + "fullLengthMarkOut" : 1626240, + "resourceList" : [ { + "url" : "https://rts-vod-amd.akamaized.net/ww/14827784/bdcfc85d-6017-3f28-8334-19fd74b1c4e3/master.m3u8", + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + } ], + "subtitleInformationList" : [ { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "HLS", + "type" : "SDH" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827784/bdcfc85d-6017-3f28-8334-19fd74b1c4e3/master.m3u8" + } + } ], + "aspectRatio" : "16:9" + }, { + "id" : "14827786", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:14827786", + "title" : "Jura: un ressortissant français s'est vu refuser la naturalisation suisse car il tondait sa pelouse le dimanche", + "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827785.image/16x9", + "imageTitle" : "Jura: un ressortissant français s'est vu refuser la naturalisation suisse car il tondait sa pelouse le dimanche [RTS]", + "type" : "CLIP", + "date" : "2024-04-10T18:00:00+02:00", + "duration" : 174080, + "validFrom" : "2024-04-10T19:00:00+02:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:14827796", + "position" : 7, + "noEmbed" : false, + "analyticsData" : { + "ns_st_ep" : "Jura: un ressortissant français s'est vu refuser la naturalisation suisse car il tondait sa pelouse le dimanche", + "ns_st_ty" : "Video", + "ns_st_ci" : "14827786", + "ns_st_el" : "174080", + "ns_st_cl" : "174080", + "ns_st_sl" : "174080", + "srg_mgeobl" : "false", + "ns_st_tp" : "1", + "ns_st_cn" : "1", + "ns_st_ct" : "vc11", + "ns_st_pn" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc" + }, + "analyticsMetadata" : { + "media_segment" : "Jura: un ressortissant français s'est vu refuser la naturalisation suisse car il tondait sa pelouse le dimanche", + "media_type" : "Video", + "media_segment_id" : "14827786", + "media_episode_length" : "174", + "media_segment_length" : "174", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:14827786", + "media_sub_set_id" : "CLIP", + "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" + }, + "eventData" : "$b41d27c3cc43eac9$7b12fe99ba916fd8c5531b966fbc6f00cd1bf8601a8e86e4565b07dcfa760a57fed7fb1aba47c5bf171cb3f190976ed1556afdc765e79b6256dcb853289980d4f6b7ab672d940543579492b30ab510141e0060858cad4d9ef72dbc4ec91698013c0ceb9bfa1b57f7a8ce5a1f6102542868a0aaac711b75f8b08aa7760df8cd86dc880d13e4daecd814160711622fe496d2ab8dd0bcda0ed8f3fb1c3b7650ffc6e07e14199e06f3906a1624a81b0f8d607dc6903d2008825e549f086b003a9e25cdb20dd3f509dd2b29de1c1a43210e89fbab98d42a015106f9f45c088dffaf3aaf3b513c2a613ecb4231df79ecceeef35983e3d009bc253b50ab0e91fa46cd24532756471ea1a961db173b171f0b5d3ff59be4a62152804ed3aa866a7586ed6f40d9f02b051d7851e2e7a76677ef642dd29af0207775255393bc9383a5253832ae42afc022533b7eb4c9780d59235a9ed9b04239bdf011102d2735621e81865d0b86d7d700b01ba4fa5291e82e08717e", + "fullLengthMarkIn" : 1626240, + "fullLengthMarkOut" : 1800320, + "resourceList" : [ { + "url" : "https://rts-vod-amd.akamaized.net/ww/14827786/3324a962-78c5-32a3-a8be-6df0ff97db17/master.m3u8", + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + } ], + "subtitleInformationList" : [ { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "HLS", + "type" : "SDH" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827786/3324a962-78c5-32a3-a8be-6df0ff97db17/master.m3u8" + } + } ], + "aspectRatio" : "16:9" + }, { + "id" : "14827788", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:14827788", + "title" : "Copinage, népotisme: quelles règles s'appliquent dans l'administration?", + "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827787.image/16x9", + "imageTitle" : "Copinage, népotisme: quelles règles s'appliquent dans l'administration? [RTS]", + "type" : "CLIP", + "date" : "2024-04-10T18:00:00+02:00", + "duration" : 189960, + "validFrom" : "2024-04-10T19:00:00+02:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:14827796", + "position" : 8, + "noEmbed" : false, + "analyticsData" : { + "ns_st_ep" : "Copinage, népotisme: quelles règles s'appliquent dans l'administration?", + "ns_st_ty" : "Video", + "ns_st_ci" : "14827788", + "ns_st_el" : "189960", + "ns_st_cl" : "189960", + "ns_st_sl" : "189960", + "srg_mgeobl" : "false", + "ns_st_tp" : "1", + "ns_st_cn" : "1", + "ns_st_ct" : "vc11", + "ns_st_pn" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc" + }, + "analyticsMetadata" : { + "media_segment" : "Copinage, népotisme: quelles règles s'appliquent dans l'administration?", + "media_type" : "Video", + "media_segment_id" : "14827788", + "media_episode_length" : "190", + "media_segment_length" : "190", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:14827788", + "media_sub_set_id" : "CLIP", + "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" + }, + "eventData" : "$cebd9d8e209f9f72$8f7e44e8842283b05aecaac5de32c1b31c6b54ccc538ebc1b493a1d30b484f78e50d1bced6c31655a4fbc1b54e9aa65d3171ad3243af84516d5d87405a7f1f7ccf2744bad835d7b15386bffda048b2e9cc2d63b6e091c4e33d71a2e47917403201bfe0d209409bb2bbb6a3536a7f756e95a64b53d2261df72dd2d38bec51691112d9f2b2838a810d6d033e60709f8cb19447f450e21cac720289d700d4ada620469120134802b2421c8c4f48ea06adfbeeff6af6291166f2c21ab253aab3f0195c545d2a456044adf15784b42c990e5ad43b2cfa47007f6e439859ab27ee2c769a92ce1ec02c40f003dc76fa6e16cd1e1a80f220cfbc821ffb8bcee90243b635a8c7c0af1eaf67bf1d34293a3c2247a37c34570e4c671fd80733b9c6e46964fa353117c642ae975db7b31dbaf05dc7f9842f4039e31e7d3a6fc2e3094cf0e0635ec714a84b3636e0bbf99e23493cd86e6696894239f3d32e9b18581d7d84a51d3d9fb8484a369156bc7881a3e14aeb94", + "fullLengthMarkIn" : 1800320, + "fullLengthMarkOut" : 1990280, + "resourceList" : [ { + "url" : "https://rts-vod-amd.akamaized.net/ww/14827788/a7f8c778-9de2-38a5-8b1a-92c668746054/master.m3u8", + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + } ], + "subtitleInformationList" : [ { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "HLS", + "type" : "SDH" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827788/a7f8c778-9de2-38a5-8b1a-92c668746054/master.m3u8" + } + } ], + "aspectRatio" : "16:9" + }, { + "id" : "14827790", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:14827790", + "title" : "Hockey sur glace: fin de saison pour Fribourg-Gottéron ce soir face à Lausanne?", + "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827789.image/16x9", + "imageTitle" : "Hockey sur glace: fin de saison pour Fribourg-Gottéron ce soir face à Lausanne? [RTS]", + "type" : "CLIP", + "date" : "2024-04-10T18:00:00+02:00", + "duration" : 131840, + "validFrom" : "2024-04-10T19:00:00+02:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:14827796", + "position" : 9, + "noEmbed" : false, + "analyticsData" : { + "ns_st_ep" : "Hockey sur glace: fin de saison pour Fribourg-Gottéron ce soir face à Lausanne?", + "ns_st_ty" : "Video", + "ns_st_ci" : "14827790", + "ns_st_el" : "131840", + "ns_st_cl" : "131840", + "ns_st_sl" : "131840", + "srg_mgeobl" : "false", + "ns_st_tp" : "1", + "ns_st_cn" : "1", + "ns_st_ct" : "vc11", + "ns_st_pn" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc" + }, + "analyticsMetadata" : { + "media_segment" : "Hockey sur glace: fin de saison pour Fribourg-Gottéron ce soir face à Lausanne?", + "media_type" : "Video", + "media_segment_id" : "14827790", + "media_episode_length" : "132", + "media_segment_length" : "132", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:14827790", + "media_sub_set_id" : "CLIP", + "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" + }, + "eventData" : "$2d3cf00ff55412b5$2f4336501473190b1fe37356cd47edb03dc54a41f59a9a9459d4046610427b68b36280b927492d45acb239c29e076010beead9d7429ed417b9dc5a003f4b8e6865a8dfe7df6e8419774e038639aa069e0037122289c33e85cd5682eb199f2049d49b7166d6aef9130c8d7bd0bb08d24ddd0d63fe6766468ad28c479c95cc57d66e1a7aa2f58bf2298461f77688345d40d7438779381639a115e1f7ea672ff445c7efeec82f4fd16f4bb161d4f3021d277eb8a788ba7273d7e1a0e5738a87c364a10115395b40236f502e8acb4cf2e2b9817b23b79a15bc7a7ada2934fc23ed7aa7f5e560ad76c751eb409c7606d3467e6a9ff1dea3790170c3d6668330bee3e09f970ebf424697f73200c6f7c2c03ab49aee60d2c7510fb3cfb6070387324d49dceddb4c6b0bb03f8e7fbfde18c987fa2816f0846da671aeafae890e7681ec379afff649cd0b9a9395209b93373edb270b01c38e1f3e9fb180b6a31366042297c4dc1d5ffae5cc8c7a2c48152c8e1e03", + "fullLengthMarkIn" : 2004520, + "fullLengthMarkOut" : 2136360, + "resourceList" : [ { + "url" : "https://rts-vod-amd.akamaized.net/ww/14827790/f3ee717e-1234-339d-b370-2b91e09e33c4/master.m3u8", + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + } ], + "subtitleInformationList" : [ { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "HLS", + "type" : "SDH" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827790/f3ee717e-1234-339d-b370-2b91e09e33c4/master.m3u8" + } + } ], + "aspectRatio" : "16:9" + }, { + "id" : "14827792", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:14827792", + "title" : "Le grand débat - Faut-il sauver les jobs d'été?", + "description" : "Débat entre les députés genevois Caroline Renold (PS) et Jean-Marc Guinchard (Centre), et Sylvain Weber, professeur à la Haute école de gestion de Genève.", + "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827791.image/16x9", + "imageTitle" : "Le grand débat - Faut-il sauver les jobs d'été? [RTS]", + "type" : "CLIP", + "date" : "2024-04-10T18:00:00+02:00", + "duration" : 1071000, + "validFrom" : "2024-04-10T19:00:00+02:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:14827796", + "position" : 10, + "noEmbed" : false, + "analyticsData" : { + "ns_st_ep" : "Le grand débat - Faut-il sauver les jobs d'été?", + "ns_st_ty" : "Video", + "ns_st_ci" : "14827792", + "ns_st_el" : "1071000", + "ns_st_cl" : "1071000", + "ns_st_sl" : "1071000", + "srg_mgeobl" : "false", + "ns_st_tp" : "1", + "ns_st_cn" : "1", + "ns_st_ct" : "vc12", + "ns_st_pn" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc" + }, + "analyticsMetadata" : { + "media_segment" : "Le grand débat - Faut-il sauver les jobs d'été?", + "media_type" : "Video", + "media_segment_id" : "14827792", + "media_episode_length" : "1071", + "media_segment_length" : "1071", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "long", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:14827792", + "media_sub_set_id" : "CLIP", + "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" + }, + "eventData" : "$45a21bdfa3fde5e1$7ad9a53938ab0577fbcca7109ec84f74b0c6990bb249ddab37baf116d840a4b2373efbe794b027213f30a4df44105572d370af89d17416c5693289ce3238e8f81058612a823918ea4bf8c93f364f2f308d841cdfbdc5d7b7e71dc54c84d5e1f115d01a82e9c250623c9cc209b68f9f6aa9afe19a2b35f8641368fc375fb3ba874243da940b0be273d334f290f22d9545d60c9193c00bc5d73e76250f8fe988839d38e5ca596c6c3f4c45d9a6fe1a5a0cd0323d9f1cf19ec14941971abd9346ecc0183ebe4905debd36b9e592383e16209b0fed0f664fc3babdff3d5d3c9d618b2249990e1a3fe5e6b635bf6967c2186e696a44f7b2bf7ea1aa23230cc6c7411e1d477edd85d80a4d6660dabdf9e621960e833c8914694c1b0cc808ec214b7e96b8fd62bd1785c1bb2012635b3bb37185e537400cf4923ec62b41e2507629ef9c35434efc4492be56d7a23325167f83f7895482185662547452c90f20db79b720161a89008effa74ef43b66484797e95e", + "fullLengthMarkIn" : 2142080, + "fullLengthMarkOut" : 3213080, + "resourceList" : [ { + "url" : "https://rts-vod-amd.akamaized.net/ww/14827792/4def5e57-2634-3f0f-97a8-cd19b96bd2ef/master.m3u8", + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + } ], + "subtitleInformationList" : [ { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "HLS", + "type" : "SDH" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827792/4def5e57-2634-3f0f-97a8-cd19b96bd2ef/master.m3u8" + } + } ], + "aspectRatio" : "16:9" + }, { + "id" : "14827794", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:14827794", + "title" : "Forum des idées - L'Université de Lausanne veut rendre la science plus utile aux citoyens", + "description" : "Interview de Cléolia Sabot, coordinatrice d'Interface, le Fonds de soutien à la recherche partenariale de l'Unil.", + "imageUrl" : "https://www.rts.ch/2024/04/10/19/51/14827793.image/16x9", + "imageTitle" : "Forum des idées - L'Université de Lausanne veut rendre la science plus utile aux citoyens [RTS]", + "type" : "CLIP", + "date" : "2024-04-10T18:00:00+02:00", + "duration" : 335160, + "validFrom" : "2024-04-10T19:00:00+02:00", + "playableAbroad" : true, + "displayable" : true, + "position" : 11, + "noEmbed" : false, + "analyticsData" : { + "ns_st_ep" : "Forum des idées - L'Université de Lausanne veut rendre la science plus utile aux citoyens", + "ns_st_ty" : "Video", + "ns_st_ci" : "14827794", + "ns_st_el" : "335160", + "ns_st_cl" : "335160", + "ns_st_sl" : "335160", + "srg_mgeobl" : "false", + "ns_st_tp" : "1", + "ns_st_cn" : "1", + "ns_st_ct" : "vc11", + "ns_st_pn" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc" + }, + "analyticsMetadata" : { + "media_segment" : "Forum des idées - L'Université de Lausanne veut rendre la science plus utile aux citoyens", + "media_type" : "Video", + "media_segment_id" : "14827794", + "media_episode_length" : "335", + "media_segment_length" : "335", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:14827794", + "media_sub_set_id" : "CLIP", + "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" + }, + "eventData" : "$1be907c9fe4c81d9$1327cda5d0b97592ab053853b4aebb4e163333dffe62d17cbc2dbe50c87fc6c252dca6a57b91d22c9fe05891ebd8e9ac8cfe8f6d906826259f49ef35c7cec418e30e88b451677989f78a15ab3c40c8533a2b9f0abe21a102c653ebc5ac32e1aa7b65b553895923f1c9f6d601a6a9ca50a6ad5b19ff867063b6d1b10746d82d81c215146081c26458572f242ded2833b64dddc959bd9947f0adf80cef893a4fb9046077db73f4d265d713d956d73bfde3cfb5251e41bc38f0f1e61260f4265a3609b52c418ab9c1aaf7e35db1e952b5b33a21ba49d937955056823c2dea32a493cf2df63b348d303d97afde95e7b55a24546102b6341bd4b81bf8183c842c01e6b30532a296a1e8076aadbea22635b29dc37272b3690df4b4253fc70db8015310e8d07a9c03fe6354734fad839bb164ad7d0862b67f048df4244f5084862e5e3ea81949b669e0db43e50b479c7ee8ec10163ed05c88b07d468df463171ce5bc8b24caa0c159587aa47e90e85868b7d092", + "fullLengthMarkIn" : 3218240, + "fullLengthMarkOut" : 3553400, + "resourceList" : [ { + "url" : "https://rts-vod-amd.akamaized.net/ww/14827794/8e8efcc7-37f6-3078-bf7e-5894324b29e6/master.m3u8", + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + } ], + "subtitleInformationList" : [ { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "HLS", + "type" : "SDH" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-vod-amd.akamaized.net/ww/14827794/8e8efcc7-37f6-3078-bf7e-5894324b29e6/master.m3u8" + } + } ], + "aspectRatio" : "16:9" + }, { + "id" : "14812522", + "mediaType" : "AUDIO", + "vendor" : "RTS", + "urn" : "urn:rts:audio:14812522", + "title" : "Forum - Présenté par Thibaut Schaller et Renaud Malik", + "imageUrl" : "https://www.rts.ch/2023/09/28/17/49/1784333.image/16x9", + "imageTitle" : "Logo Forum [RTS]", + "type" : "CLIP", + "date" : "2024-04-10T18:00:00+02:00", + "duration" : 3600000, + "podcastHdUrl" : "https://rts-aod-dd.akamaized.net/ww/14812522/b3fde219-dd00-3bfa-a085-a90ee4c9969a.mp3", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:14827796", + "position" : 12, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Forum - Présenté par Thibaut Schaller et Renaud Malik", + "media_type" : "Audio", + "media_segment_id" : "14812522", + "media_episode_length" : "3600", + "media_segment_length" : "3600", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "long", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:audio:14812522", + "media_sub_set_id" : "CLIP", + "media_topic_list" : "urn:rts:topic:tv:49683,urn:rts:topic:tv:16202" + }, + "eventData" : "$94d0a22d6ce4f018$cce0e3446e92b4d9024e6a2e2e390087e63c86fd4586474c28ce7ece2806db8ddc68336257833a0c54c98ed43e6e283e8f321b56b0ee6bd47f56acfe2f12ffc438ca2c71ba9406d02c1a0be9c0f4f929f54107d9065f94b78b464aa90711ef056b97eebad09da193c1f16040d7df5d7a0983166d7139e57dbfed5951dd29c5761969b9412266cd2baa7f8fd0efbedb05ed2489d61dc08ed1c51768e997c989e69d055a645d3ed277c2a2837d7317141bb6e1bb2a2c204d4574a0b99be034c7af81e7331c459f48b17725aca6ad1fe179035ce3d61853623a06306b30b89f48f69932de9a0e61829d9b5b678ce0e662a9378a9a2fa10914586793b6efbabb9bc91863306cc39b1534ce39fc17632f59c2461d1ccfded9504fa7810e6c2b4f2cc3c3932fa54d8c4e262ac6fa82b358542e55b07302655a89e6eb5485a5b6dcf2e9febc6167095269892f133b74bea6596c173fdc8cfef634e6e8d0fb1f4982e4c81aded9d224122ded855676dbe6d59206", + "fullLengthMarkIn" : 0, + "fullLengthMarkOut" : 0, + "resourceList" : [ { + "url" : "https://rts-aod-dd.akamaized.net/ww/14812522/b3fde219-dd00-3bfa-a085-a90ee4c9969a.mp3", + "quality" : "HQ", + "protocol" : "HTTPS", + "encoding" : "MP3", + "mimeType" : "audio/mpeg", + "presentation" : "DEFAULT", + "streaming" : "PROGRESSIVE", + "dvr" : false, + "live" : false, + "mediaContainer" : "NONE", + "audioCodec" : "MP3", + "videoCodec" : "NONE", + "tokenType" : "NONE", + "analyticsMetadata" : { + "media_streaming_quality" : "HQ", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-aod-dd.akamaized.net/ww/14812522/b3fde219-dd00-3bfa-a085-a90ee4c9969a.mp3" + } + } ] + } ], + "topicList" : [ { + "id" : "49683", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:49683", + "title" : "Forum" + }, { + "id" : "16202", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:16202", + "title" : "La 1ère" + } ], + "analyticsData" : { + "srg_pr_id" : "14718074", + "srg_plid" : "9933104", + "ns_st_pl" : "Forum", + "ns_st_pr" : "Forum du 10.04.2024", + "ns_st_dt" : "2024-04-10", + "ns_st_ddt" : "2024-04-10", + "ns_st_tdt" : "2024-04-10", + "ns_st_tm" : "18:00", + "ns_st_tep" : "500531747", + "ns_st_li" : "0", + "ns_st_stc" : "0867", + "ns_st_st" : "RTS Online", + "ns_st_tpr" : "9933104", + "ns_st_en" : "*null", + "ns_st_ge" : "*null", + "ns_st_ia" : "*null", + "ns_st_ce" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc", + "srg_unit" : "RTS", + "srg_c1" : "full", + "srg_c2" : "video_la-1ere_forum", + "srg_c3" : "RTS 2", + "srg_tv_id" : "500531747", + "srg_aod_prid" : "14718074" + }, + "analyticsMetadata" : { + "media_episode_id" : "14718074", + "media_show_id" : "9933104", + "media_show" : "Forum", + "media_episode" : "Forum du 10.04.2024", + "media_is_livestream" : "false", + "media_full_length" : "full", + "media_enterprise_units" : "RTS", + "media_joker1" : "full", + "media_joker2" : "video_la-1ere_forum", + "media_joker3" : "RTS 2", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_tv_id" : "500531747", + "media_thumbnail" : "https://www.rts.ch/2024/04/10/19/51/14827795.image/16x9/scale/width/344", + "media_publication_date" : "2024-04-10", + "media_publication_time" : "19:00:00", + "media_publication_datetime" : "2024-04-10T19:00:00+02:00", + "media_tv_date" : "2024-04-10", + "media_tv_time" : "18:00:00", + "media_tv_datetime" : "2024-04-10T18:00:00+02:00", + "media_content_group" : "Forum,La 1ère", + "media_channel_id" : "d7dfff28deee44e1d3c49a3d37d36d492b29671b", + "media_channel_cs" : "0867", + "media_channel_name" : "RTS 2", + "media_since_publication_d" : "11", + "media_since_publication_h" : "278" + } +} \ No newline at end of file diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_onDemand.dataset/Contents.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_onDemand.dataset/Contents.json new file mode 100644 index 00000000..56c7e33d --- /dev/null +++ b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_onDemand.dataset/Contents.json @@ -0,0 +1,12 @@ +{ + "data" : [ + { + "filename" : "urn_rts_video_13360574.json", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_onDemand.dataset/urn_rts_video_13360574.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_onDemand.dataset/urn_rts_video_13360574.json new file mode 100644 index 00000000..7fe752b9 --- /dev/null +++ b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_onDemand.dataset/urn_rts_video_13360574.json @@ -0,0 +1,210 @@ +{ + "chapterUrn" : "urn:rts:video:13360574", + "episode" : { + "number": 12, + "seasonNumber": 2, + "id" : "13360565", + "title" : "Yadebat", + "publishedDate" : "2022-09-05T16:30:00+02:00", + "imageUrl" : "https://www.rts.ch/2022/09/05/18/20/13360573.image/16x9", + "imageTitle" : "On réunit des ex après leur rupture [RTS]" + }, + "show" : { + "id" : "10174267", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:show:tv:10174267", + "title" : "Yadebat", + "lead" : "Une série qui te donne la parole, pour laisser entendre ton avis sur les débats de société animée par Melissa.", + "description" : "Une série qui te donne la parole, pour laisser entendre ton avis sur les débats de société animée par Melissa.", + "imageUrl" : "https://www.rts.ch/2020/01/10/11/14/10520588.image/16x9", + "imageTitle" : "Yadebat - Tataki [RTS]", + "posterImageUrl" : "https://ws.srf.ch/asset/image/audio/e0322b37-5697-474d-93ac-19a4044a6a24/POSTER.jpg", + "posterImageIsFallbackUrl" : true, + "audioDescriptionAvailable" : false, + "subtitlesAvailable" : false, + "multiAudioLanguagesAvailable" : false, + "topicList" : [ { + "id" : "59952", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:59952", + "title" : "Yadebat" + }, { + "id" : "54537", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:54537", + "title" : "Tataki" + } ], + "allowIndexing" : false + }, + "chapterList" : [ { + "id" : "13360574", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13360574", + "title" : "On réunit des ex après leur rupture", + "description" : "Dans ce nouvel épisode de YADEBAT, Mélissa réunit 3 couples qui se sont séparés récemment. Elles les a questionné en face à face pour connaître leurs différents ressentis et réactions.", + "imageUrl" : "https://www.rts.ch/2022/09/05/18/20/13360573.image/16x9", + "imageTitle" : "On réunit des ex après leur rupture [RTS]", + "type" : "EPISODE", + "date" : "2022-09-05T16:30:00+02:00", + "duration" : 902360, + "validFrom" : "2022-09-05T16:30:00+02:00", + "validTo" : "2100-01-01T23:59:59+01:00", + "playableAbroad" : true, + "socialCountList" : [ { + "key" : "srgView", + "value" : 17 + }, { + "key" : "srgLike", + "value" : 0 + }, { + "key" : "fbShare", + "value" : 0 + }, { + "key" : "twitterShare", + "value" : 0 + }, { + "key" : "googleShare", + "value" : 0 + }, { + "key" : "whatsAppShare", + "value" : 0 + } ], + "displayable" : true, + "position" : 0, + "noEmbed" : false, + "analyticsData" : { + "ns_st_ep" : "On réunit des ex après leur rupture", + "ns_st_ty" : "Video", + "ns_st_ci" : "13360574", + "ns_st_el" : "902360", + "ns_st_cl" : "902360", + "ns_st_sl" : "902360", + "srg_mgeobl" : "false", + "ns_st_tp" : "1", + "ns_st_cn" : "1", + "ns_st_ct" : "vc12", + "ns_st_pn" : "1", + "ns_st_cdm" : "eo", + "ns_st_cmt" : "ec" + }, + "analyticsMetadata" : { + "media_segment" : "On réunit des ex après leur rupture", + "media_type" : "Video", + "media_segment_id" : "13360574", + "media_episode_length" : "902", + "media_segment_length" : "902", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "1", + "media_duration_category" : "long", + "media_is_geoblocked" : "false", + "media_is_web_only" : "true", + "media_production_source" : "produced.for.web", + "media_urn" : "urn:rts:video:13360574" + }, + "eventData" : "$27549332a83ca6ac$64b181b51953d6ed48de11986513e2f93922eb3d4315e6d5ad8189e1fe38d933c011ba7ded29e3d757ba1e566e76d65d97c8f0cd0735cc47b1cb3e5cf091c89c8d6c18ff31e19e3d7509cbf826c0c156fd10b8908ebe481aaf7282de102e92342ffb36b52df58453b40d64883f720fb3eddd38b595ddf6961acc4bc33abb3f2c49b7d90b52a35239f0209caa3ebc532e6a95315bd382bc08f2b78af2ec23c3f7e7917de924cb7f85b8aedac2fdafd027fe3880e07f3a0ba05f43d0ce601a1d2c7b756012c8820e12eef32fb9c0e1f532cce31cf1be738a9d6c05555857700fc5e1f0e1bd9886f06c55f5e731a66daa09be035e5ef53a4da159a7d3943a67ebaa1ac1302ad3ff046739eb185d78737e1543e7788d4edd9858af0e6846460106e954e8f1176cf60876aad36646c11a3b3a824ab54433f99c4576accea86e2b853c", + "resourceList" : [ { + "url" : "https://rts-vod-amd.akamaized.net/ww/13360574/447e0958-42a8-3bdd-8365-95d54031e605/master.m3u8", + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-vod-amd.akamaized.net/ww/13360574/447e0958-42a8-3bdd-8365-95d54031e605/master.m3u8" + } + } ], + "aspectRatio" : "16:9", + "spriteSheet" : { + "urn" : "urn:rts:video:13360574", + "rows" : 23, + "columns" : 20, + "thumbnailHeight" : 84, + "thumbnailWidth" : 150, + "interval" : 2000, + "url" : "https://il.srgssr.ch/spritesheet/urn/rts/video/13360574/sprite-13360574.jpeg" + } + } ], + "topicList" : [ { + "id" : "59952", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:59952", + "title" : "Yadebat" + }, { + "id" : "54537", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:54537", + "title" : "Tataki" + } ], + "analyticsData" : { + "srg_pr_id" : "13360565", + "srg_plid" : "10174267", + "ns_st_pl" : "Yadebat", + "ns_st_pr" : "Yadebat du 05.09.2022", + "ns_st_dt" : "2022-09-05", + "ns_st_ddt" : "2022-09-05", + "ns_st_tdt" : "*null", + "ns_st_tm" : "*null", + "ns_st_tep" : "500418168", + "ns_st_li" : "0", + "ns_st_stc" : "0867", + "ns_st_st" : "RTS Online", + "ns_st_tpr" : "10174267", + "ns_st_en" : "*null", + "ns_st_ge" : "*null", + "ns_st_ia" : "*null", + "ns_st_ce" : "1", + "ns_st_cdm" : "eo", + "ns_st_cmt" : "ec", + "srg_unit" : "RTS", + "srg_c1" : "full", + "srg_c2" : "video_tataki_yadebat", + "srg_c3" : "RTS.ch", + "srg_tv_id" : "500418168" + }, + "analyticsMetadata" : { + "media_episode_id" : "13360565", + "media_show_id" : "10174267", + "media_show" : "Yadebat", + "media_episode" : "Yadebat du 05.09.2022", + "media_is_livestream" : "false", + "media_full_length" : "full", + "media_enterprise_units" : "RTS", + "media_joker1" : "full", + "media_joker2" : "video_tataki_yadebat", + "media_joker3" : "RTS.ch", + "media_is_web_only" : "true", + "media_production_source" : "produced.for.web", + "media_tv_id" : "500418168", + "media_thumbnail" : "https://www.rts.ch/2022/09/05/18/20/13360573.image/16x9/scale/width/344", + "media_publication_date" : "2022-09-05", + "media_publication_time" : "16:30:00", + "media_publication_datetime" : "2022-09-05T16:30:00+02:00", + "media_content_group" : "Yadebat,Tataki", + "media_since_publication_d" : "0", + "media_since_publication_h" : "19" + } +} \ No newline at end of file diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_redundant.dataset/Contents.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_redundant.dataset/Contents.json new file mode 100644 index 00000000..414c11c3 --- /dev/null +++ b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_redundant.dataset/Contents.json @@ -0,0 +1,13 @@ +{ + "data" : [ + { + "filename" : "urn_rts_video_13763072.json", + "idiom" : "universal", + "universal-type-identifier" : "public.json" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_redundant.dataset/urn_rts_video_13763072.json b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_redundant.dataset/urn_rts_video_13763072.json new file mode 100644 index 00000000..c0f2de89 --- /dev/null +++ b/Tests/CoreBusinessTests/Resources.xcassets/MediaComposition_redundant.dataset/urn_rts_video_13763072.json @@ -0,0 +1,690 @@ +{ + "chapterUrn" : "urn:rts:video:13763072", + "episode" : { + "id" : "13646015", + "title" : "19h30", + "publishedDate" : "2023-02-06T19:30:00+01:00", + "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763071.image/16x9", + "imageTitle" : "19h30 [RTS]" + }, + "show" : { + "id" : "105932", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:show:tv:105932", + "title" : "19h30", + "lead" : "L'édition du soir du téléjournal.", + "imageUrl" : "https://www.rts.ch/2019/08/28/11/33/10667272.image/16x9", + "imageTitle" : "RTS Info - Le 19h30, avec nouveau logo RTS Info (la mise en ligne le lundi 26 août 2019) [RTS]", + "bannerImageUrl" : "https://www.rts.ch/2019/08/28/11/33/10667272.image/3x1", + "posterImageUrl" : "https://www.rts.ch/2021/08/05/18/12/12396566.image/2x3", + "posterImageIsFallbackUrl" : false, + "primaryChannelId" : "143932a79bb5a123a646b68b1d1188d7ae493e5b", + "primaryChannelUrn" : "urn:rts:channel:tv:143932a79bb5a123a646b68b1d1188d7ae493e5b", + "availableAudioLanguageList" : [ { + "locale" : "fr", + "language" : "Français" + } ], + "availableVideoQualityList" : [ "SD", "HD" ], + "audioDescriptionAvailable" : false, + "subtitlesAvailable" : true, + "multiAudioLanguagesAvailable" : false, + "topicList" : [ { + "id" : "908", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:908", + "title" : "19h30" + }, { + "id" : "904", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:904", + "title" : "Vidéos" + }, { + "id" : "665", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:665", + "title" : "Info" + } ], + "allowIndexing" : true + }, + "channel" : { + "id" : "143932a79bb5a123a646b68b1d1188d7ae493e5b", + "vendor" : "RTS", + "urn" : "urn:rts:channel:tv:143932a79bb5a123a646b68b1d1188d7ae493e5b", + "title" : "RTS 1", + "imageUrl" : "https://www.rts.ch/2019/08/28/11/33/10667272.image/16x9", + "imageUrlRaw" : "https://il.srgssr.ch/image-service/dynamic/8eebe5.svg", + "imageTitle" : "RTS Info - Le 19h30, avec nouveau logo RTS Info (la mise en ligne le lundi 26 août 2019) [RTS]", + "transmission" : "TV" + }, + "chapterList" : [ { + "id" : "13763072", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13763072", + "title" : "19h30", + "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763071.image/16x9", + "imageTitle" : "19h30 [RTS]", + "type" : "EPISODE", + "date" : "2023-02-06T19:30:00+01:00", + "duration" : 1857560, + "validFrom" : "2023-02-06T20:05:19+01:00", + "playableAbroad" : true, + "socialCountList" : [ { + "key" : "srgView", + "value" : 13340 + }, { + "key" : "srgLike", + "value" : 0 + }, { + "key" : "fbShare", + "value" : 1 + }, { + "key" : "twitterShare", + "value" : 0 + }, { + "key" : "googleShare", + "value" : 0 + }, { + "key" : "whatsAppShare", + "value" : 34 + } ], + "displayable" : true, + "position" : 0, + "noEmbed" : false, + "analyticsData" : { + "ns_st_ep" : "19h30", + "ns_st_ty" : "Video", + "ns_st_ci" : "13763072", + "ns_st_el" : "1857560", + "ns_st_cl" : "1857560", + "ns_st_sl" : "1857560", + "srg_mgeobl" : "false", + "ns_st_tp" : "13", + "ns_st_cn" : "1", + "ns_st_ct" : "vc12", + "ns_st_pn" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc" + }, + "analyticsMetadata" : { + "media_segment" : "19h30", + "media_type" : "Video", + "media_segment_id" : "13763072", + "media_episode_length" : "1858", + "media_segment_length" : "1858", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "13", + "media_duration_category" : "long", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:13763072" + }, + "eventData" : "$9a6505bcccb854bf$a0ec7b2518b7ce6260492f932bbe32902090d850691731a263a076b740edfd09021e43de81ef4cbec8fd112788116eec867927a2eaea7aed9f5df592d48fed9209a004578d1192ac68df0f063ca108cee4c7e783890e1c1af04fdc95de08a3515919f0910f4804d6d0f6d90182f46894a40c6f254b132655c1d4e7c2a532312a9999a945b0d5edb4f21bbe1f7e7f12cc7a484b000984d395b8ac3f3222433004536c0a7233874ef4ae80cbc4d6f5dc3e9952a8ad986666021bb3b9849ae83b86b163cc7e0ef8617f7cfabac9d12e649ad4dc395a4f8c5e12ec9b865d7d1ae28802977ff0d268032cf7ef7209711b75459705353edf342f05f01a5dcf3853dfb2e46bb7adb5852fc6d9ca115877b3d08a22b3a822c751ee88b0c279dcdadf16604b3c7c73cb2f8e58156cd2de4b78cd1d6fe0f57b400088a5a892d365086f75e3ce0dcf35fe7af7bf6221a679b639ec8141ff5d019cbf4cb520663f95fd1d93c52c4edab3440fdfcb1d4a27b4d442f8cf", + "resourceList" : [ { + "url" : "https://rts-vod-amd.akamaized.net/ww/13763072/11def5d2-733d-3f82-bb0a-90492ff637d2/master.m3u8", + "quality" : "HD", + "protocol" : "HLS", + "encoding" : "H264", + "mimeType" : "application/x-mpegURL", + "presentation" : "DEFAULT", + "streaming" : "HLS", + "dvr" : false, + "live" : false, + "mediaContainer" : "FMP4", + "audioCodec" : "AAC", + "videoCodec" : "H264", + "tokenType" : "NONE", + "audioTrackList" : [ { + "locale" : "fr", + "language" : "Français", + "source" : "HLS" + } ], + "subtitleInformationList" : [ { + "locale" : "fr", + "language" : "Français (SDH)", + "source" : "HLS", + "type" : "SDH" + } ], + "analyticsData" : { + "srg_mqual" : "HD", + "srg_mpres" : "DEFAULT" + }, + "analyticsMetadata" : { + "media_streaming_quality" : "HD", + "media_special_format" : "DEFAULT", + "media_url" : "https://rts-vod-amd.akamaized.net/ww/13763072/11def5d2-733d-3f82-bb0a-90492ff637d2/master.m3u8" + } + } ], + "segmentList" : [ { + "id" : "13763046", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13763046", + "title" : "L'est de la Turquie dévasté par un séisme de 7,8 Le bilan pourrait atteindre plusieurs milliers de morts.", + "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763036.image/16x9", + "imageTitle" : "L'est de la Turquie dévasté par un séisme de 7,8 Le bilan pourrait atteindre plusieurs milliers de morts. [RTS]", + "type" : "CLIP", + "date" : "2023-02-06T19:30:00+01:00", + "duration" : 131840, + "validFrom" : "2023-02-06T20:05:19+01:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:13763072", + "position" : 1, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "L'est de la Turquie dévasté par un séisme de 7,8 Le bilan pourrait atteindre plusieurs milliers de morts.", + "media_type" : "Video", + "media_segment_id" : "13763046", + "media_episode_length" : "1858", + "media_segment_length" : "132", + "media_number_of_segment_selected" : "1", + "media_number_of_segments_total" : "13", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:13763046" + }, + "eventData" : "$b6e9570e7c7c008b$b4b2b89162a5acc3d5e1a659fcdc259315f6fd1b5a19421d31a9a21dc075474b61de92c4cfca39722266b88e2925cb5eb10604e19c9d21dd67ff275340d75dd0a18c55c053ab20150494704581ca4b4b6368f51a8abd08dd432b1a088564873db897ffc9309bec2b48278d935942394d1ac3cc7b08a8a447ec96de6ad790dfaf6473b176e75df4b2f7fd5c32fbc9a5edb0422be37476f62c8d842980a63b7555a1414fdec97a2a1f3617b4ac1fe37b071e01fc1f5056f6b2e8744155f50e46fa0cfe5c0e14a3d121e2da0f152ef9d69eb575d3785858bc207d9082e6dec5e7dfa0ac4c11602a275b6f9e4b5a74a813f2e1796915e45b75e37c1a88c5037e4f5a3b40781b5daac89cfe065b4b167f43068d5bcc1116cc39ec76beaa314ac488d3818e4d4589bbb13796cd7e981e240bcfd7db0cfe9cbb3272e0a3a0190f06356546f1e695f64ede1f7daac3ca0128895bf02bd1cfc576b71db7dcbc403b5cdee266fdc579a35ee688770323980a976e9a", + "markIn" : 101360, + "markOut" : 233200 + }, { + "id" : "13763048", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13763048", + "title" : "Tremblement de terre en Turquie et en Syrie : les explications de Mayalen de Castelbajac, correspondante en Turquie", + "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763047.image/16x9", + "imageTitle" : "Tremblement de terre en Turquie et en Syrie : les explications de Mayalen de Castelbajac, correspondante en Turquie [RTS]", + "type" : "CLIP", + "date" : "2023-02-06T19:30:00+01:00", + "duration" : 156840, + "validFrom" : "2023-02-06T20:05:19+01:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:13763072", + "position" : 2, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Tremblement de terre en Turquie et en Syrie : les explications de Mayalen de Castelbajac, correspondante en Turquie", + "media_type" : "Video", + "media_segment_id" : "13763048", + "media_episode_length" : "1858", + "media_segment_length" : "157", + "media_number_of_segment_selected" : "2", + "media_number_of_segments_total" : "13", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:13763048" + }, + "eventData" : "$16a882befb748245$5aee998923fd67269f5f08d0d1fadd76d80802de2d0a37a81b9a6717c04d4c0f3abe45a31eec7fd840e415b4f7754c8fa9cc6c9d2b151c54d4339fa875f8d9825111ae41e913a219f1d52a27e817949c4bef16015726f7e14940f1756909338ac60c9fe606a8fc03ea972d111a45c3c7ef29474d1d9a9d1804be0d79ca4aedbd50b7dfcadbca48e82eed99a5d8d7059d46c49f6fda33bb8aa075d11aaf1e76c8cfff482715b0804b6dafaa97e871a5ab8bbc1e9a00c0aa0b6048a739544ab53710afc9bad895836701d9cb63ee0f7c38b23a0a74ed545cfd91d1e925296c4e93ac6d9d5ef4f266491e660e036efd5e5da1b25e4bbd1d0f7e2e7cbdaae0dae09999c5df2cc2c5afa232ced5c3cf924a909e0703e5ae8fe1737cdb5e1b2a119c772b433e00a328b5f3896f67a127a2a16167b0ced53377fdeaca1e9d7738ee7d2aeaaf5a1d63e4960efa45823452ffbe44d8aa77d5bb9369b45ae3e4a584b30be93a13139d069bfe017e0ddf93463b0c4b", + "markIn" : 233200, + "markOut" : 390040 + }, { + "id" : "13763050", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13763050", + "title" : "En Syrie, le tremblement de terre qui a frappé le pays lundi a touché une région déjà ravagée par la guerre civile.", + "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763049.image/16x9", + "imageTitle" : "En Syrie, le tremblement de terre qui a frappé le pays lundi a touché une région déjà ravagée par la guerre civile. [RTS]", + "type" : "CLIP", + "date" : "2023-02-06T19:30:00+01:00", + "duration" : 119160, + "validFrom" : "2023-02-06T20:05:19+01:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:13763072", + "position" : 3, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "En Syrie, le tremblement de terre qui a frappé le pays lundi a touché une région déjà ravagée par la guerre civile.", + "media_type" : "Video", + "media_segment_id" : "13763050", + "media_episode_length" : "1858", + "media_segment_length" : "119", + "media_number_of_segment_selected" : "3", + "media_number_of_segments_total" : "13", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:13763050" + }, + "eventData" : "$00cf3e37b9871701$24ad456ebd36455798644ce1372a18e8d12d9d9b5accfa63ace5251bee2c67447b0aad1a07d0c881363adaf5ada12655ed9d8f36824338a57262908f068dfcbc58825092f9cf528fe435a7bc531700aab0215d3f59609e1217682de7fa93830afa0b82c561cec4ee006793772e56c18dbf64aa3009059dd1fcb0ae390a298d9b48ddb0995bf36aa7fa5a540eab7a81d814a9d6c35e4e2eaff212e6b32a16d2787d6d0a7ef8fb3c1b24d7f7039426b336e85ace49b973d37df9db3e4aa995f04ee5988b0dcae50fafe0cd567d0a5acbb37bd7697141013597f1edbb296ee9902faccb91d820b2c42411beae9d31158ade8999e129a3211951de6787ee88f123c9d4fc6ca6479700a60c53720f36d20234f69321813935acf74137b5fde0838a8552b46b03c5e7e6926f268c71f278a8b70725cdc491948de9200b0de5893df4da7a964ab8465b68847ab01b80fc06e120cd8d87250d62ebc9f71e20b880fe3fe4365029cd0677ee7aec2281b3cfeee538", + "markIn" : 390040, + "markOut" : 509200 + }, { + "id" : "13763052", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13763052", + "title" : "Tremblement de terre en Turquie et en Syrie : La Suisse a annoncé qu’elle allait envoyer des équipes et des chiens de sauvetage. Ils doivent s’envoler lundi soir pour la Turquie.", + "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763051.image/16x9", + "imageTitle" : "Tremblement de terre en Turquie et en Syrie : La Suisse a annoncé qu’elle allait envoyer des équipes et des chiens de sauvetage. Ils doivent s’envoler lundi soir pour la Turquie. [RTS]", + "type" : "CLIP", + "date" : "2023-02-06T19:30:00+01:00", + "duration" : 105080, + "validFrom" : "2023-02-06T20:05:19+01:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:13763072", + "position" : 4, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Tremblement de terre en Turquie et en Syrie : La Suisse a annoncé qu’elle allait envoyer des équipes et des chiens de sauvetage. Ils doivent s’envoler lundi soir pour la Turquie.", + "media_type" : "Video", + "media_segment_id" : "13763052", + "media_episode_length" : "1858", + "media_segment_length" : "105", + "media_number_of_segment_selected" : "4", + "media_number_of_segments_total" : "13", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:13763052" + }, + "eventData" : "$459879157b1c695b$dcdf6794f1f24da7e77c37e9debba2aa96d38384400cbf1db6a9b821d960c3b40b183a3392ccfd2860649c70e37dfc0feb55db43644a75386ffca3a115815e924028dcccbabfefa4722c3e20ac42f57dd7b6dfab5f6e630cd97c268d682a4fc10a2cd6ff00eba8e3da8488ca7fac957dd2f90e1c2ca93ec7fcbaa9e083026b50a1c3fbcf1751f0968ddc5f831c6ec638f8bf18fb8c33b325cf8953d2df6f2176cfa86323c79687955c6bc8ea1fd082a81f883d574f8a37d5d6442a754821bf1a31f97e8494c93ed8a4e03c21a1019988e282e64ad98eca4e1d2a49993817df6c99027de09f26eaa49faf8995c7358bbc9307a1a021ca2646f4748f442d33da0c5020fd19cc7f17defeef18becadbd8aafcffefc94747cd11fa9fce0fc96f0f7f251fea090c54b581a6547cb9d0c1509a2cc2b5ab402946d5f5cce429e2d0399c8e8f91b5f658914cf1e994549a3b1cc0856c165e2eb93050cdda86cafa42a31c9a12437f528fe6cfc81b9dc396b214a9", + "markIn" : 509200, + "markOut" : 614280 + }, { + "id" : "13763054", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13763054", + "title" : "Séisme en Turquie et en Syrie : Olivier Hagon, chef du groupe \"Santé\" du corps suisse d'aide humanitaire, revient sur l'importance d'une aide internationale immédiate", + "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763053.image/16x9", + "imageTitle" : "Séisme en Turquie et en Syrie : Olivier Hagon, chef du groupe \"Santé\" du corps suisse d'aide humanitaire, revient sur l'importance d'une aide internationale immédiate [RTS]", + "type" : "CLIP", + "date" : "2023-02-06T19:30:00+01:00", + "duration" : 149480, + "validFrom" : "2023-02-06T20:05:19+01:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:13763072", + "position" : 5, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Séisme en Turquie et en Syrie : Olivier Hagon, chef du groupe \"Santé\" du corps suisse d'aide humanitaire, revient sur l'importance d'une aide internationale immédiate", + "media_type" : "Video", + "media_segment_id" : "13763054", + "media_episode_length" : "1858", + "media_segment_length" : "149", + "media_number_of_segment_selected" : "5", + "media_number_of_segments_total" : "13", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:13763054" + }, + "eventData" : "$cb47ec31639040f6$4e51360d4cbc0a2dd71b960ed6935727f221e27fda235c10f508a6be94a473a66fa15f3e037a5ba3a5e5e99bad571dc30773c88fce53a5e8da1bb794e127230881e5beb9e4d012b4ee423f9722013f8d58574deaaf30c82e9549f8bfc757a9b613b1f573ab2b0984b37e08d975032ed117af8765a317a491e6df21efe2b3a120cc1f7759130afbea7aae2fc3a0bb1b273068fb1d33ab1271c03441518129b8f7861aad4ee68b19719c1553a01dc3d83ab8dbd18eea5578280b5833324f196e49f9897c8512071493e5c01b7c67a42079cb40a13a8c3d87ad6935840388391444b059a1891f9c0bee6565aebac761000a05fc4a6063a1aff5b3072e64744a547203fb859ada45fc65579657fba1336155ecb295d7a38a137756bbba1f4451eb4b4499d7806741e8ebcd15e4681f65f760b07b05c85490293d3063fa4aaa9a5ace0c879509669996155fe2bcd8544b05ebde74a2bc1a3cd9bf3de38d7ef073afe35feeab42c8c7eaf53c7cc447bce133e8", + "markIn" : 614280, + "markOut" : 763760 + }, { + "id" : "13763056", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13763056", + "title" : "Une délégation de parlementaires suisses a rendu visite à la Présidente taiwanaise.", + "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763055.image/16x9", + "imageTitle" : "Une délégation de parlementaires suisses a rendu visite à la Présidente taiwanaise. [RTS]", + "type" : "CLIP", + "date" : "2023-02-06T19:30:00+01:00", + "duration" : 110360, + "validFrom" : "2023-02-06T20:05:19+01:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:13763072", + "position" : 6, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Une délégation de parlementaires suisses a rendu visite à la Présidente taiwanaise.", + "media_type" : "Video", + "media_segment_id" : "13763056", + "media_episode_length" : "1858", + "media_segment_length" : "110", + "media_number_of_segment_selected" : "6", + "media_number_of_segments_total" : "13", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:13763056" + }, + "eventData" : "$57ef956cdc01fb68$1e76173b238e6d873218d416f667a7d312b45a5dea058751206f20fc6db32f6d02ac1a5fecfb6ddeaa7bac331fbb48e37a1b59dca16ddf947ded0e9fe0b7c77a50e5235f61a15e149457f2765284302a87b1379ee40e386e3a52aba94dc80ef80f7e421187c414eeb2a2a87cc4a880e5370e95098b822083d6d0371bc7d1cf27171490d0fb55fec43807fb0dc5b65bea0ff42f862f5475824d5df8443b1604e9e115bc6b3c6972733498579f641461c5f6c70d45d0cac604989623e105c981a359e509832bd307f21cc7bab262922fdb487160aa9a372138fdcada9c2bfcd95ecb4e30a0f10c9fb7c41da2767f54a0f0909804e76696739587371b77bc7c63d32bb820d47414adbc194441d38fc65e8072e90ec9b5d3500d6a7eb5767de11ba18554008cf9fe8c31710e9d03b6848e5ff4b7e92619433bea9a65f000ac3067b97e0554d975a863e0241c21fa730c721dbcaa9397f36f3e88e9eba30607bcf53bc6c1c0a8cefd6908382113552fd4b21c", + "markIn" : 776200, + "markOut" : 886560 + }, { + "id" : "13763058", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13763058", + "title" : "À Hong-Kong, alors que la plupart des opposants à Pékin ont été réduits au silence, quelques-uns résistent encore", + "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763057.image/16x9", + "imageTitle" : "À Hong-Kong, alors que la plupart des opposants à Pékin ont été réduits au silence, quelques-uns résistent encore [RTS]", + "type" : "CLIP", + "date" : "2023-02-06T19:30:00+01:00", + "duration" : 177720, + "validFrom" : "2023-02-06T20:05:19+01:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:13763072", + "position" : 7, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "À Hong-Kong, alors que la plupart des opposants à Pékin ont été réduits au silence, quelques-uns résistent encore", + "media_type" : "Video", + "media_segment_id" : "13763058", + "media_episode_length" : "1858", + "media_segment_length" : "178", + "media_number_of_segment_selected" : "7", + "media_number_of_segments_total" : "13", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:13763058" + }, + "eventData" : "$1a93330bbc15fad9$a604aa17bcf32efc7fb057fd07f1f8c7bac3b6925cd34e46a03f3f895b025dd320d415f97a6392be0d4e95d7f7351310c07f19a343834504ad59d1ec82c85905b0fd0c83185ce812bfe40d582ef9de50da2a2b747a3617052f1121181336eae0b6df069741128683556f64465c7515849ae1a705fe5e37bc912fad9c336fdf7c652c4f6a4d64c9010e259df08b6f605ff475f84c929933bff673dae4b72004c760409b3a5066145a338897c751f12678820b1876376b80c9eb005abb3062d696b1f5d856c8000fb2b7feb6439d047decf0ecedde3d74fcc684cb44de45b067b0196b99917164f8439c4d606b2d7e78a035a08b2c1b3b8d30915054f973e27df03abc3112769fffb0057145033738dc1dacda9fdc0a9fa0c59941e7955f2936b6557ddf05570312b43658a5254ad322401817f2023bc407297cfcf2c1c697fa72f995d005dbc2d71949edab436df50faa3a4c74b7c961ad7322dcdbac76587244f228965f6b4154ca0e821b842b8bc54a", + "markIn" : 916160, + "markOut" : 1093880 + }, { + "id" : "13763060", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13763060", + "title" : "La nouvelle carte d'identité suisse sera disponible dès le 3 mars prochain.", + "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763059.image/16x9", + "imageTitle" : "La nouvelle carte d'identité suisse sera disponible dès le 3 mars prochain. [RTS]", + "type" : "CLIP", + "date" : "2023-02-06T19:30:00+01:00", + "duration" : 79280, + "validFrom" : "2023-02-06T20:05:19+01:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:13763072", + "position" : 8, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "La nouvelle carte d'identité suisse sera disponible dès le 3 mars prochain.", + "media_type" : "Video", + "media_segment_id" : "13763060", + "media_episode_length" : "1858", + "media_segment_length" : "79", + "media_number_of_segment_selected" : "8", + "media_number_of_segments_total" : "13", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:13763060" + }, + "eventData" : "$c95a60ec09271449$74c6da37289f167a2751a1955cbc5d68462366528ebaae5f2d716c8e6809d3a93cff003b8dc4f5e7d931dc6d54b4ce33c65395cefbe99eed9483c6c48f84fb3ea69b83158653bb88bd229d5e816400a949aad113bbe7e0f0b7b4b35a5b50ad29be6f9fec4feaac7278ff990a0b9f234980096272cf7ce165c22ff17444012c6807326b97e9eb257dd962c0b0b6b547b2c50b2412506ace70e230e8dcb29607cab31a6640a8c2ff4493c8cc7e8eaf7332c54dd6a42dcbb825328e630eb1967bd62f36d0f13608f2ffd729feb62f1ca22fd416e32ee12d60e2ff73f3bd3f3dd64f49dd769d04e2d8bb8b0227723552d1d1275a26095ece3acc543f383700ab0806c24447d862ace9dee6eeb9228a5bb70998a486f2581b6b40e6c9f785ff68d9ee504223bae916d24ec87d204cb9e281d76897859a8c1207fc0aa8572adfceca3979abf4834605dada7950598df31045f6cad6b3780e83dafa51f4624f24d6552291351108201a93d38b739c21fa8ce717", + "markIn" : 1093880, + "markOut" : 1173160 + }, { + "id" : "13763062", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13763062", + "title" : "Vive controverse à Neuchâtel. L'Eglise réformée ne veut plus de cérémonies laïques dans ses temples.", + "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763061.image/16x9", + "imageTitle" : "Vive controverse à Neuchâtel. L'Eglise réformée ne veut plus de cérémonies laïques dans ses temples. [RTS]", + "type" : "CLIP", + "date" : "2023-02-06T19:30:00+01:00", + "duration" : 131520, + "validFrom" : "2023-02-06T20:05:19+01:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:13763072", + "position" : 9, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Vive controverse à Neuchâtel. L'Eglise réformée ne veut plus de cérémonies laïques dans ses temples.", + "media_type" : "Video", + "media_segment_id" : "13763062", + "media_episode_length" : "1858", + "media_segment_length" : "132", + "media_number_of_segment_selected" : "9", + "media_number_of_segments_total" : "13", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:13763062" + }, + "eventData" : "$fe5ff11ee266bee8$617478588c4826d351ae27d63c295aa6ecb7098c5aede3501f3eb710ce7338119fdb9fc91b88dd26503c23ee5decdc1752f22c695968dffcec3db8034f7b3a552a9f371800d6d1ec7926a834dae805ff90911adace6c570443209ed1d0baf130c98573c7c7235b1da9786928263a5ba5558ee76d21f5529d296491c4e3bfc9c0488dd9098000dbc492453b95da65d64ac87d589f0960d2810cabe922ed9bda080e5c2b13fbfea894dc0ba460172e4a6b21dd1ca17427b63f0f41156d7ffe1091636ecc1d162fff2888a7212cb424e4aeab7097681a4e7b61cd522f60fad3b740e38e5b52b4450fccca9539eb57d666ffabd5bf0fe1dcf99b0e15032d18f8fe27935a19679e8f5a6426af9fad82ff081943616fd8483e81dd17ae31a797dd6fa9554a52eec6793b222f9220e342651c64df65f4b50b17348233257a94832a33e7a0d502881f2ee1dadfc58ae584ef6c95c0116971aa91588d1992c89ab67caecde0989a4273aa5e1a49ef3329908904bf", + "markIn" : 1173160, + "markOut" : 1304680 + }, { + "id" : "13763064", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13763064", + "title" : "Michel Kocher, journaliste à RTSreligion, commente la décision de l'Église réformée neuchâteloise de ne plus accueillir de cérémonies laïques dans ses temples.", + "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763063.image/16x9", + "imageTitle" : "Michel Kocher, journaliste à RTSreligion, commente la décision de l'Église réformée neuchâteloise de ne plus accueillir de cérémonies laïques dans ses temples. [RTS]", + "type" : "CLIP", + "date" : "2023-02-06T19:30:00+01:00", + "duration" : 142600, + "validFrom" : "2023-02-06T20:05:19+01:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:13763072", + "position" : 10, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Michel Kocher, journaliste à RTSreligion, commente la décision de l'Église réformée neuchâteloise de ne plus accueillir de cérémonies laïques dans ses temples.", + "media_type" : "Video", + "media_segment_id" : "13763064", + "media_episode_length" : "1858", + "media_segment_length" : "143", + "media_number_of_segment_selected" : "10", + "media_number_of_segments_total" : "13", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:13763064" + }, + "eventData" : "$b4e9b17cb149766c$104065454cabe642af9c753e7e05443dfea2ca49820e1dc89e286604535af353ebe1c52da3eb2fef21689581e9727c535cd70b6842488e8f785ce2927465f966d73120a483c5e03e304dc7dcf91dfa23831085d275f49fe0139166e070ce6957c8eebf6f0767eb9449b6b5e6a6ae469f6a2c493dfaf176ef343819d62de8861183bedc43521ddb3497798f2fce203121aeab9d56ded99e8879f03e4de082786ec8b9209d7eb2e4f07123f610e226b380f39a83db742a78ac270ef27534e55adf294f436aee1e4b6de7164a48d12d6addff6f07ed5a714f66f00ea6efd6ca97171ecaf0a6bb0ae78f31dee52480ad8afad009ad1df0b99ee06571656bbb7f4c21469c874d941f8326579d9f9de593f09a490e6a4c57dbd0f9cf3a991533f6b0aa7371203e7263d4a07a1bfe151562ce9a48664548a74fb3d33bd42b08093a3b12ae53f1b0d390ed2ab26027346551ea4e03663069fe23363968350bc309070723f4146d626cfac7399940fea3f22282e1", + "markIn" : 1304680, + "markOut" : 1447280 + }, { + "id" : "13763066", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13763066", + "title" : "2 mois après l'entrée en vigueur de la \"Lex Booking\", notre enquête montre qu'il est avantageux de réserver son hôtel en direct.", + "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763065.image/16x9", + "imageTitle" : "2 mois après l'entrée en vigueur de la \"Lex Booking\", notre enquête montre qu'il est avantageux de réserver son hôtel en direct. [RTS]", + "type" : "CLIP", + "date" : "2023-02-06T19:30:00+01:00", + "duration" : 130560, + "validFrom" : "2023-02-06T20:05:19+01:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:13763072", + "position" : 11, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "2 mois après l'entrée en vigueur de la \"Lex Booking\", notre enquête montre qu'il est avantageux de réserver son hôtel en direct.", + "media_type" : "Video", + "media_segment_id" : "13763066", + "media_episode_length" : "1858", + "media_segment_length" : "131", + "media_number_of_segment_selected" : "11", + "media_number_of_segments_total" : "13", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:13763066" + }, + "eventData" : "$2ece6a538f114427$0e716d1d8e4a1cdd5fc88b1400fd4317d5674af7d51adc5ad25446c6953385c3799d71633e54d4a6b010c46e253fb4af972e56f993e9fced0362f7ea4fc1a32a892624bcb5ede4db30f4e18de68cde5267345a301e324aaa5f6fc606cf18c60f935a29d5d71a21845cb331ba9e434eff25e23830f367f3e5f7a12d55e2c3e9cada84b7689d7b4bb3326d781882f7e21d79a706a4ee07719aa2d3ddd415725dea049f630093af6ce1d68ff536418c36907f4ad57f631ef9ba9b1829f11f6bc8919ec133aedea7187e757adb3a96cdd05726cf705d622613413458e47f21dbcc491b5f82589135ecdd98de7565cc7912d25d1778299b50550f7670ac38740c71f3d69286f30a6d1e241e1183235a134e8a6816b4a9face4c1c72c81694225a708c8a6f7404ca6ceaf8782f97b548cac69dfb5e30075f5774d2e6133f5ae142dad60fe4cf6b2cf10bc5bfb786eba33b02e1a08578f0353ff44d5cbf6e9c71ca9e9807ca5b63cd11ca493eaaefbc3d197ccc", + "markIn" : 1447280, + "markOut" : 1577840 + }, { + "id" : "13763068", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13763068", + "title" : "Première journée au championnats du monde de ski et première médaille suisse avec l’argent de Wendy Holdener en combiné.", + "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763067.image/16x9", + "imageTitle" : "Première journée au championnats du monde de ski et première médaille suisse avec l’argent de Wendy Holdener en combiné. [RTS]", + "type" : "CLIP", + "date" : "2023-02-06T19:30:00+01:00", + "duration" : 119760, + "validFrom" : "2023-02-06T20:05:19+01:00", + "playableAbroad" : false, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:13763072", + "position" : 12, + "noEmbed" : true, + "analyticsMetadata" : { + "media_segment" : "Première journée au championnats du monde de ski et première médaille suisse avec l’argent de Wendy Holdener en combiné.", + "media_type" : "Video", + "media_segment_id" : "13763068", + "media_episode_length" : "1858", + "media_segment_length" : "120", + "media_number_of_segment_selected" : "12", + "media_number_of_segments_total" : "13", + "media_duration_category" : "short", + "media_is_geoblocked" : "true", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:13763068" + }, + "eventData" : "$b0e5010f0d20637f$3e77d084182f46934ad9ee16de6e15e80d37d5b528ed652e9b894d96a68b663e98eff6318fe88f9dbb8f52011572a40379bef3dcec053b35b0595bc91f75375c046fdca30575850a7fbca5d4221bd1ff95547d0ae81a058182d277c3704d03e72a7b6e9a022b9a246433341f3b31dd50d1304515ab0c5afc4e5c68d98566ec1c2522a629253f6294ef89db7eb28ac6c21bc88b19affb75ff5fd35a519fbb0bc5aab8236776203b200e9ea78b657500a6203fefb750e2f307fb0197b6fa40288361d44686aa7543bee9ba3471d9fd9ecd4dd80067108ab7ac943b8be9e4e210100c991334c68837e365bcb3b086cf063cd9db9eec0180c64bc6e834436f9367ae85ccf512cab3df1a61ccd9dffb72d9654b19b7ea6e375eda7fa25af868a9ef9d2d592fea7b04c13f0ee2e721c3756f80d887640505f383436f1c84680430ac3a92d5a68fc9a059eea55f2d130bf94d87e7b5b083e4c5494e5bf64a0fcb48c92ab49e892c01d95cd726cd7e08ffaca8a0", + "markIn" : 1581720, + "markOut" : 1701480 + }, { + "id" : "13763070", + "mediaType" : "VIDEO", + "vendor" : "RTS", + "urn" : "urn:rts:video:13763070", + "title" : "Michel Simonet, le célèbre balayeur fribourgeois, cumule 4 millions de vue sur Instagram avec une vidéo racontant son quotidien.", + "imageUrl" : "https://www.rts.ch/2023/02/06/21/06/13763069.image/16x9", + "imageTitle" : "Michel Simonet, le célèbre balayeur fribourgeois, cumule 4 millions de vue sur Instagram avec une vidéo racontant son quotidien. [RTS]", + "type" : "CLIP", + "date" : "2023-02-06T19:30:00+01:00", + "duration" : 128800, + "validFrom" : "2023-02-06T20:05:19+01:00", + "playableAbroad" : true, + "displayable" : true, + "fullLengthUrn" : "urn:rts:video:13763072", + "position" : 13, + "noEmbed" : false, + "analyticsMetadata" : { + "media_segment" : "Michel Simonet, le célèbre balayeur fribourgeois, cumule 4 millions de vue sur Instagram avec une vidéo racontant son quotidien.", + "media_type" : "Video", + "media_segment_id" : "13763070", + "media_episode_length" : "1858", + "media_segment_length" : "129", + "media_number_of_segment_selected" : "13", + "media_number_of_segments_total" : "13", + "media_duration_category" : "short", + "media_is_geoblocked" : "false", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_urn" : "urn:rts:video:13763070" + }, + "eventData" : "$4c2cb6787916e0cf$7656f4a41f1c6fa32d08561727846b3bb5351df71cbb9ee961ae30c670a6a17cabe57da7e6f183f391290366827ce367208076430e6de3593c8ef9a39588edc4d7faaac91cc9d7433f01a8e3e98366a39b4f312fb237046101672c15d441d82b798a5ea698d2358485def04e8e66e32dd835bfcba7ce04611b1787e29e7bb0f2f0f15ee75b5d6ba756154913c2a50752d89d2bacdfa85398f694031cd39549c74db85fed1562d9b86daf0c8da686a56f89782b4d7786631cbb5993a45aab579fa2b0b5f3fb7de747dcfffefcc544cb3bf3d147861ed8d026262b07810b775603c7040b6521fdc5b33fbc5a9ec5e078e6112272ae73f4bd97c9a7f367bc6ec718239994397aabac1787800819a8adefc349e4b92438fcaf316959f8e8dcdde47cae695cfdc3ae7d124d2de58925eb313894d1acf5ef693b4627e588f4bd494ce7ece42436cce6b5de66dfdb234389d5743a9b843bc25ff8234f80ad7d15646e066c4f26f76a097137d3b0f9fd137365c6", + "markIn" : 1701480, + "markOut" : 1830280 + } ], + "aspectRatio" : "16:9", + "spriteSheet" : { + "urn" : "urn:rts:video:13763072", + "rows" : 24, + "columns" : 20, + "thumbnailHeight" : 84, + "thumbnailWidth" : 150, + "interval" : 4000, + "url" : "https://il.srgssr.ch/spritesheet/urn/rts/video/13763072/sprite-13763072.jpeg" + } + } ], + "topicList" : [ { + "id" : "908", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:908", + "title" : "19h30" + }, { + "id" : "904", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:904", + "title" : "Vidéos" + }, { + "id" : "665", + "vendor" : "RTS", + "transmission" : "TV", + "urn" : "urn:rts:topic:tv:665", + "title" : "Info" + } ], + "analyticsData" : { + "srg_pr_id" : "13646015", + "srg_plid" : "105932", + "ns_st_pl" : "19h30", + "ns_st_pr" : "19h30 du 06.02.2023", + "ns_st_dt" : "2023-02-06", + "ns_st_ddt" : "2023-02-06", + "ns_st_tdt" : "2023-02-06", + "ns_st_tm" : "19:30:00", + "ns_st_tep" : "500434867", + "ns_st_li" : "0", + "ns_st_stc" : "0867", + "ns_st_st" : "RTS Online", + "ns_st_tpr" : "105932", + "ns_st_en" : "*null", + "ns_st_ge" : "*null", + "ns_st_ia" : "*null", + "ns_st_ce" : "1", + "ns_st_cdm" : "to", + "ns_st_cmt" : "fc", + "srg_unit" : "RTS", + "srg_c1" : "full", + "srg_c2" : "video_info_journal-19h30", + "srg_c3" : "RTS 1", + "srg_tv_id" : "500434867" + }, + "analyticsMetadata" : { + "media_episode_id" : "13646015", + "media_show_id" : "105932", + "media_show" : "19h30", + "media_episode" : "19h30 du 06.02.2023", + "media_is_livestream" : "false", + "media_full_length" : "full", + "media_enterprise_units" : "RTS", + "media_joker1" : "full", + "media_joker2" : "video_info_journal-19h30", + "media_joker3" : "RTS 1", + "media_is_web_only" : "false", + "media_production_source" : "produced.for.broadcasting", + "media_tv_id" : "500434867", + "media_thumbnail" : "https://www.rts.ch/2023/02/06/21/06/13763071.image/16x9/scale/width/344", + "media_publication_date" : "2023-02-06", + "media_publication_time" : "20:05:19", + "media_publication_datetime" : "2023-02-06T20:05:19+01:00", + "media_tv_date" : "2023-02-06", + "media_tv_time" : "19:30:00", + "media_tv_datetime" : "2023-02-06T19:30:00+01:00", + "media_content_group" : "19h30,Vidéos,Info", + "media_channel_id" : "143932a79bb5a123a646b68b1d1188d7ae493e5b", + "media_channel_cs" : "0867", + "media_channel_name" : "RTS 1", + "media_since_publication_d" : "6", + "media_since_publication_h" : "163" + } +} \ No newline at end of file diff --git a/Tests/CoreTests/AccumulatePublisherTests.swift b/Tests/CoreTests/AccumulatePublisherTests.swift new file mode 100644 index 00000000..c32a22ec --- /dev/null +++ b/Tests/CoreTests/AccumulatePublisherTests.swift @@ -0,0 +1,186 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Combine +import PillarboxCircumspect +import XCTest + +final class AccumulatePublisherTests: XCTestCase { + private static func publisher(at index: Int) -> AnyPublisher { + precondition(index > 0) + return Just(index) + .delay(for: .seconds(Double(index) * 0.1), scheduler: DispatchQueue.main) + .eraseToAnyPublisher() + } + + private static func prependedPublisher(at index: Int) -> AnyPublisher { + precondition(index > 0) + return Just(index) + .delay(for: .seconds(Double(index) * 0.1), scheduler: DispatchQueue.main) + .prepend(0) + .eraseToAnyPublisher() + } + + func testSyntaxWithoutTypeErasure() { + expectOnlyEqualPublished( + values: [ + [1, 2, 3] + ], + from: Publishers.AccumulateLatestMany( + Just(1), + Just(2), + Just(3) + ) + ) + } + + func testAccumulateOne() { + expectOnlyEqualPublished( + values: [ + [1] + ], + from: Publishers.AccumulateLatestMany( + Self.publisher(at: 1) + ) + ) + } + + func testAccumulateTwo() { + expectOnlyEqualPublished( + values: [ + [1, 2] + ], + from: Publishers.AccumulateLatestMany( + Self.publisher(at: 1), + Self.publisher(at: 2) + ) + ) + } + + func testAccumulateThree() { + expectOnlyEqualPublished( + values: [ + [1, 2, 3] + ], + from: Publishers.AccumulateLatestMany( + Self.publisher(at: 1), + Self.publisher(at: 2), + Self.publisher(at: 3) + ) + ) + } + + func testAccumulateFour() { + expectOnlyEqualPublished( + values: [ + [1, 2, 3, 4] + ], + from: Publishers.AccumulateLatestMany( + Self.publisher(at: 1), + Self.publisher(at: 2), + Self.publisher(at: 3), + Self.publisher(at: 4) + ) + ) + } + + func testAccumulateFive() { + expectOnlyEqualPublished( + values: [ + [1, 2, 3, 4, 5] + ], + from: Publishers.AccumulateLatestMany( + Self.publisher(at: 1), + Self.publisher(at: 2), + Self.publisher(at: 3), + Self.publisher(at: 4), + Self.publisher(at: 5) + ) + ) + } + + func testAccumulateOnePrepended() { + expectOnlyEqualPublished( + values: [ + [0], + [1] + ], + from: Publishers.AccumulateLatestMany( + Self.prependedPublisher(at: 1) + ) + ) + } + + func testAccumulateTwoPrepended() { + expectOnlyEqualPublished( + values: [ + [0, 0], + [1, 0], + [1, 2] + ], + from: Publishers.AccumulateLatestMany( + Self.prependedPublisher(at: 1), + Self.prependedPublisher(at: 2) + ) + ) + } + + func testAccumulateThreePrepended() { + expectOnlyEqualPublished( + values: [ + [0, 0, 0], + [1, 0, 0], + [1, 2, 0], + [1, 2, 3] + ], + from: Publishers.AccumulateLatestMany( + Self.prependedPublisher(at: 1), + Self.prependedPublisher(at: 2), + Self.prependedPublisher(at: 3) + ) + ) + } + + func testAccumulateFourPrepended() { + expectOnlyEqualPublished( + values: [ + [0, 0, 0, 0], + [1, 0, 0, 0], + [1, 2, 0, 0], + [1, 2, 3, 0], + [1, 2, 3, 4] + ], + from: Publishers.AccumulateLatestMany( + Self.prependedPublisher(at: 1), + Self.prependedPublisher(at: 2), + Self.prependedPublisher(at: 3), + Self.prependedPublisher(at: 4) + ) + ) + } + + func testAccumulateFivePrepended() { + expectOnlyEqualPublished( + values: [ + [0, 0, 0, 0, 0], + [1, 0, 0, 0, 0], + [1, 2, 0, 0, 0], + [1, 2, 3, 0, 0], + [1, 2, 3, 4, 0], + [1, 2, 3, 4, 5] + ], + from: Publishers.AccumulateLatestMany( + Self.prependedPublisher(at: 1), + Self.prependedPublisher(at: 2), + Self.prependedPublisher(at: 3), + Self.prependedPublisher(at: 4), + Self.prependedPublisher(at: 5) + ) + ) + } +} diff --git a/Tests/CoreTests/ArrayTests.swift b/Tests/CoreTests/ArrayTests.swift new file mode 100644 index 00000000..d840ea30 --- /dev/null +++ b/Tests/CoreTests/ArrayTests.swift @@ -0,0 +1,24 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Nimble +import PillarboxCircumspect +import XCTest + +final class ArrayTests: XCTestCase { + func testRemoveDuplicates() { + expect([1, 2, 3, 4].removeDuplicates()).to(equalDiff([1, 2, 3, 4])) + expect([1, 2, 1, 4].removeDuplicates()).to(equalDiff([1, 2, 4])) + } + + func testSafeIndex() { + expect([1, 2, 3][safeIndex: 0]).to(equal(1)) + expect([1, 2, 3][safeIndex: -1]).to(beNil()) + expect([1, 2, 3][safeIndex: 3]).to(beNil()) + } +} diff --git a/Tests/CoreTests/CombineLatestTests.swift b/Tests/CoreTests/CombineLatestTests.swift new file mode 100644 index 00000000..b6f83f66 --- /dev/null +++ b/Tests/CoreTests/CombineLatestTests.swift @@ -0,0 +1,64 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Combine +import PillarboxCircumspect +import XCTest + +final class CombineLatestPublisherTests: XCTestCase { + func testOutput5() { + expectOnlyEqualPublished( + values: [ + [1, 2, 3, 4, 5] + ], + from: Publishers.CombineLatest5( + Just(1), + Just(2), + Just(3), + Just(4), + Just(5) + ) + .map { [$0.0, $0.1, $0.2, $0.3, $0.4] } + ) + } + + func testOutput6() { + expectOnlyEqualPublished( + values: [ + [1, 2, 3, 4, 5, 6] + ], + from: Publishers.CombineLatest6( + Just(1), + Just(2), + Just(3), + Just(4), + Just(5), + Just(6) + ) + .map { [$0.0, $0.1, $0.2, $0.3, $0.4, $0.5] } + ) + } + + func testOutput7() { + expectOnlyEqualPublished( + values: [ + [1, 2, 3, 4, 5, 6, 7] + ], + from: Publishers.CombineLatest7( + Just(1), + Just(2), + Just(3), + Just(4), + Just(5), + Just(6), + Just(7) + ) + .map { [$0.0, $0.1, $0.2, $0.3, $0.4, $0.5, $0.6] } + ) + } +} diff --git a/Tests/CoreTests/ComparableTests.swift b/Tests/CoreTests/ComparableTests.swift new file mode 100644 index 00000000..6546fc69 --- /dev/null +++ b/Tests/CoreTests/ComparableTests.swift @@ -0,0 +1,20 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Nimble +import XCTest + +final class ComparableTests: XCTestCase { + func testClamped() { + expect((-1).clamped(to: 0...1)).to(equal(0)) + expect(0.clamped(to: 0...1)).to(equal(0)) + expect(0.5.clamped(to: 0...1)).to(equal(0.5)) + expect(1.clamped(to: 0...1)).to(equal(1)) + expect(2.clamped(to: 0...1)).to(equal(1)) + } +} diff --git a/Tests/CoreTests/DemandBufferTests.swift b/Tests/CoreTests/DemandBufferTests.swift new file mode 100644 index 00000000..7b8386c6 --- /dev/null +++ b/Tests/CoreTests/DemandBufferTests.swift @@ -0,0 +1,81 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Combine +import Nimble +import PillarboxCircumspect +import XCTest + +final class DemandBufferTests: XCTestCase { + func testEmptyBuffer() { + let buffer = DemandBuffer() + expect(buffer.values).to(beEmpty()) + expect(buffer.requested).to(equal(Subscribers.Demand.none)) + } + + func testPrefilledBuffer() { + let buffer: DemandBuffer = [1, 2] + expect(buffer.values).to(equalDiff([1, 2])) + } + + func testLimitedRequestWithEmptyBuffer() { + let buffer = DemandBuffer() + expect(buffer.request(.max(2))).to(beEmpty()) + expect(buffer.requested).to(equal(.max(2))) + } + + func testLimitedRequestWithPartiallyFilledBuffer() { + let buffer: DemandBuffer = [1, 2] + expect(buffer.request(.max(10))).to(equalDiff([1, 2])) + expect(buffer.requested).to(equal(.max(8))) + } + + func testLimitedRequestWithFullyFilledBuffer() { + let buffer: DemandBuffer = [1, 2, 3, 4] + expect(buffer.request(.max(2))).to(equalDiff([1, 2])) + expect(buffer.requested).to(equal(.max(0))) + expect(buffer.append(5)).to(beEmpty()) + } + + func testUnlimitedRequestWithEmptyBuffer() { + let buffer = DemandBuffer() + expect(buffer.request(.unlimited)).to(beEmpty()) + expect(buffer.requested).to(equal(.unlimited)) + } + + func testUnlimitedRequestWithFilledBuffer() { + let buffer: DemandBuffer = [1, 2] + expect(buffer.request(.unlimited)).to(equalDiff([1, 2])) + expect(buffer.requested).to(equal(.unlimited)) + } + + func testAppendWithPendingLimitedRequest() { + let buffer = DemandBuffer() + expect(buffer.request(.max(2))).to(beEmpty()) + expect(buffer.append(1)).to(equalDiff([1])) + expect(buffer.append(2)).to(equalDiff([2])) + expect(buffer.requested).to(equal(.max(0))) + expect(buffer.append(3)).to(beEmpty()) + } + + func testAppendWithPendingUnlimitedRequest() { + let buffer = DemandBuffer() + expect(buffer.request(.unlimited)).to(beEmpty()) + expect(buffer.append(1)).to(equalDiff([1])) + expect(buffer.append(2)).to(equalDiff([2])) + } + + func testThreadSafety() { + let buffer = DemandBuffer([0...1000]) + for _ in 0..<100 { + DispatchQueue.global().async { + _ = buffer.request(.unlimited) + } + } + } +} diff --git a/Tests/CoreTests/DispatchPublisherTests.swift b/Tests/CoreTests/DispatchPublisherTests.swift new file mode 100644 index 00000000..0cfd45a6 --- /dev/null +++ b/Tests/CoreTests/DispatchPublisherTests.swift @@ -0,0 +1,93 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Combine +import Nimble +import PillarboxCircumspect +import XCTest + +final class DispatchPublisherTests: XCTestCase { + private var cancellables = Set() + + func testReceiveOnMainThreadFromMainThread() { + var value = 0 + Just(3) + .receiveOnMainThread() + .sink { i in + expect(Thread.isMainThread).to(beTrue()) + value = i + } + .store(in: &cancellables) + expect(value).to(equal(3)) + } + + func testReceiveOnMainThreadFromBackgroundThread() { + var value = 0 + Just(3) + .receive(on: DispatchQueue(label: "com.srgssr.pillarbox-tests")) + .receiveOnMainThread() + .sink { i in + expect(Thread.isMainThread).to(beTrue()) + value = i + } + .store(in: &cancellables) + expect(value).to(equal(0)) + } + + func testStandardReceiveOnMainThreadFromMainThread() { + var value = 0 + Just(3) + .receive(on: DispatchQueue.main) + .sink { i in + expect(Thread.isMainThread).to(beTrue()) + value = i + } + .store(in: &cancellables) + expect(value).to(equal(0)) + } + + func testStandardReceiveOnMainThreadFromBackgroundThread() { + var value = 0 + Just(3) + .receive(on: DispatchQueue(label: "com.srgssr.pillarbox-tests")) + .receive(on: DispatchQueue.main) + .sink { i in + expect(Thread.isMainThread).to(beTrue()) + value = i + } + .store(in: &cancellables) + expect(value).to(equal(0)) + } + + func testReceiveOnMainThreadReceivesAllOutputFromMainThread() { + let publisher = [1, 2, 3].publisher + .receiveOnMainThread() + expectOnlyEqualPublished(values: [1, 2, 3], from: publisher) + } + + func testReceiveOnMainThreadReceivesAllOutputFromBackgroundThreads() { + let publisher = [1, 2, 3].publisher + .receive(on: DispatchQueue(label: "com.srgssr.pillarbox-tests")) + .receiveOnMainThread() + expectOnlyEqualPublished(values: [1, 2, 3], from: publisher) + } + + func testDelayIfNeededOutputOrderingWithNonZeroDelay() { + let delayedPublisher = [1, 2, 3].publisher + .delayIfNeeded(for: 0.1, scheduler: DispatchQueue.main) + let subject = CurrentValueSubject(0) + expectEqualPublished(values: [0, 1, 2, 3], from: Publishers.Merge(delayedPublisher, subject), during: .milliseconds(100)) + } + + func testDelayIfNeededOutputOrderingWithZeroDelay() { + let delayedPublisher = [1, 2, 3].publisher + .delayIfNeeded(for: 0, scheduler: DispatchQueue.main) + let subject = CurrentValueSubject(0) + expectEqualPublished(values: [1, 2, 3, 0], from: Publishers.Merge(delayedPublisher, subject), during: .milliseconds(100)) + } +} diff --git a/Tests/CoreTests/LimitedBufferTests.swift b/Tests/CoreTests/LimitedBufferTests.swift new file mode 100644 index 00000000..0267be95 --- /dev/null +++ b/Tests/CoreTests/LimitedBufferTests.swift @@ -0,0 +1,31 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Nimble +import PillarboxCircumspect +import XCTest + +final class LimitedBufferTests: XCTestCase { + func testBufferWithZeroSize() { + let buffer = LimitedBuffer(size: 0) + expect(buffer.values).to(beEmpty()) + buffer.append(1) + expect(buffer.values).to(beEmpty()) + } + + func testBufferWithFiniteSize() { + let buffer = LimitedBuffer(size: 2) + expect(buffer.values).to(beEmpty()) + buffer.append(1) + expect(buffer.values).to(equalDiff([1])) + buffer.append(2) + expect(buffer.values).to(equalDiff([1, 2])) + buffer.append(3) + expect(buffer.values).to(equalDiff([2, 3])) + } +} diff --git a/Tests/CoreTests/MeasurePublisherTests.swift b/Tests/CoreTests/MeasurePublisherTests.swift new file mode 100644 index 00000000..505ff21f --- /dev/null +++ b/Tests/CoreTests/MeasurePublisherTests.swift @@ -0,0 +1,36 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Combine +import PillarboxCircumspect +import XCTest + +final class MeasurePublisherTests: XCTestCase { + func testWithSingleEvent() { + let publisher = Just(1) + .delay(for: .milliseconds(500), scheduler: DispatchQueue.main) + .measureDateInterval() + .map(\.duration) + expectPublished(values: [0.5], from: publisher, to: beClose(within: 0.1), during: .seconds(1)) + } + + func testWithMultipleEvents() { + let publisher = [1, 2].publisher + .delay(for: .milliseconds(500), scheduler: DispatchQueue.main) + .measureDateInterval() + .map(\.duration) + expectPublished(values: [0.5, 0], from: publisher, to: beClose(within: 0.1), during: .seconds(1)) + } + + func testWithoutEvents() { + let publisher = Empty() + .delay(for: .milliseconds(500), scheduler: DispatchQueue.main) + .measureDateInterval() + expectNothingPublished(from: publisher, during: .seconds(1)) + } +} diff --git a/Tests/CoreTests/NotificationPublisherDeallocationTests.swift b/Tests/CoreTests/NotificationPublisherDeallocationTests.swift new file mode 100644 index 00000000..9621f133 --- /dev/null +++ b/Tests/CoreTests/NotificationPublisherDeallocationTests.swift @@ -0,0 +1,43 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Nimble +import PillarboxCircumspect +import XCTest + +final class NotificationPublisherDeallocationTests: XCTestCase { + func testReleaseWithObject() throws { + let notificationCenter = NotificationCenter.default + var object: TestObject? = TestObject() + let publisher = notificationCenter.weakPublisher(for: .testNotification, object: object).first() + + weak var weakObject = object + try autoreleasepool { + try waitForOutput(from: publisher) { + notificationCenter.post(name: .testNotification, object: object) + } + object = nil + } + expect(weakObject).to(beNil()) + } + + func testReleaseWithNSObject() throws { + let notificationCenter = NotificationCenter.default + var object: TestNSObject? = TestNSObject() + let publisher = notificationCenter.weakPublisher(for: .testNotification, object: object).first() + + weak var weakObject = object + try autoreleasepool { + try waitForOutput(from: publisher) { + notificationCenter.post(name: .testNotification, object: object) + } + object = nil + } + expect(weakObject).to(beNil()) + } +} diff --git a/Tests/CoreTests/NotificationPublisherTests.swift b/Tests/CoreTests/NotificationPublisherTests.swift new file mode 100644 index 00000000..83236880 --- /dev/null +++ b/Tests/CoreTests/NotificationPublisherTests.swift @@ -0,0 +1,47 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Nimble +import PillarboxCircumspect +import XCTest + +final class NotificationPublisherTests: XCTestCase { + func testWithObject() throws { + let object = TestObject() + let notificationCenter = NotificationCenter.default + try waitForOutput(from: notificationCenter.weakPublisher(for: .testNotification, object: object).first()) { + notificationCenter.post(name: .testNotification, object: object) + } + } + + func testWithNSObject() throws { + let object = TestNSObject() + let notificationCenter = NotificationCenter.default + try waitForOutput(from: notificationCenter.weakPublisher(for: .testNotification, object: object).first()) { + notificationCenter.post(name: .testNotification, object: object) + } + } + + func testAfterObjectRelease() { + let notificationCenter = NotificationCenter.default + var object: TestObject? = TestObject() + let publisher = notificationCenter.weakPublisher(for: .testNotification, object: object).first() + + weak var weakObject = object + autoreleasepool { + object = nil + } + expect(weakObject).to(beNil()) + + // We were interested in notifications from `object` only. After its release we should not receive other + // notifications from any other source anymore. + expectNothingPublished(from: publisher, during: .seconds(1)) { + notificationCenter.post(name: .testNotification, object: nil) + } + } +} diff --git a/Tests/CoreTests/PublishAndRepeatOnOutputFromTests.swift b/Tests/CoreTests/PublishAndRepeatOnOutputFromTests.swift new file mode 100644 index 00000000..1f5a2aae --- /dev/null +++ b/Tests/CoreTests/PublishAndRepeatOnOutputFromTests.swift @@ -0,0 +1,38 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Combine +import PillarboxCircumspect +import XCTest + +final class PublishAndRepeatOnOutputFromTests: XCTestCase { + private let trigger = Trigger() + + func testNoSignal() { + let publisher = Publishers.PublishAndRepeat(onOutputFrom: Optional>.none) { + Just("out") + } + expectAtLeastEqualPublished(values: ["out"], from: publisher) + } + + func testInactiveSignal() { + let publisher = Publishers.PublishAndRepeat(onOutputFrom: trigger.signal(activatedBy: 1)) { + Just("out") + } + expectAtLeastEqualPublished(values: ["out"], from: publisher) + } + + func testActiveSignal() { + let publisher = Publishers.PublishAndRepeat(onOutputFrom: trigger.signal(activatedBy: 1)) { + Just("out") + } + expectAtLeastEqualPublished(values: ["out", "out"], from: publisher) { [trigger] in + trigger.activate(for: 1) + } + } +} diff --git a/Tests/CoreTests/PublishOnOutputFromTests.swift b/Tests/CoreTests/PublishOnOutputFromTests.swift new file mode 100644 index 00000000..68e5963e --- /dev/null +++ b/Tests/CoreTests/PublishOnOutputFromTests.swift @@ -0,0 +1,38 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Combine +import PillarboxCircumspect +import XCTest + +final class PublishOnOutputFromTests: XCTestCase { + private let trigger = Trigger() + + func testNoSignal() { + let publisher = Publishers.Publish(onOutputFrom: Optional>.none) { + Just("out") + } + expectNothingPublished(from: publisher, during: .seconds(1)) + } + + func testInactiveSignal() { + let publisher = Publishers.Publish(onOutputFrom: trigger.signal(activatedBy: 1)) { + Just("out") + } + expectNothingPublished(from: publisher, during: .seconds(1)) + } + + func testActiveSignal() { + let publisher = Publishers.Publish(onOutputFrom: trigger.signal(activatedBy: 1)) { + Just("out") + } + expectAtLeastEqualPublished(values: ["out"], from: publisher) { [trigger] in + trigger.activate(for: 1) + } + } +} diff --git a/Tests/CoreTests/RangeReplaceableCollectionTests.swift b/Tests/CoreTests/RangeReplaceableCollectionTests.swift new file mode 100644 index 00000000..4d23c50f --- /dev/null +++ b/Tests/CoreTests/RangeReplaceableCollectionTests.swift @@ -0,0 +1,51 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Nimble +import PillarboxCircumspect +import XCTest + +final class RangeReplaceableCollectionTests: XCTestCase { + func testMoveForward() { + var array = [1, 2, 3, 4, 5, 6, 7] + array.move(from: 2, to: 5) + expect(array).to(equalDiff([1, 2, 4, 5, 3, 6, 7])) + } + + func testMoveBackward() { + var array = [1, 2, 3, 4, 5, 6, 7] + array.move(from: 5, to: 2) + expect(array).to(equalDiff([1, 2, 6, 3, 4, 5, 7])) + } + + func testMoveToEnd() { + var array = [1, 2, 3, 4, 5, 6, 7] + array.move(from: 2, to: 7) + expect(array).to(equalDiff([1, 2, 4, 5, 6, 7, 3])) + } + + func testMoveSameItem() { + var array = [1, 2, 3, 4, 5, 6, 7] + array.move(from: 2, to: 2) + expect(array).to(equalDiff([1, 2, 3, 4, 5, 6, 7])) + } + + func testMoveFromInvalidIndex() { + guard nimbleThrowAssertionsAvailable() else { return } + var array = [1, 2, 3, 4, 5, 6, 7] + expect(array.move(from: -1, to: 2)).to(throwAssertion()) + expect(array.move(from: 8, to: 2)).to(throwAssertion()) + } + + func testMoveToInvalidIndex() { + guard nimbleThrowAssertionsAvailable() else { return } + var array = [1, 2, 3, 4, 5, 6, 7] + expect(array.move(from: 2, to: -1)).to(throwAssertion()) + expect(array.move(from: 2, to: 8)).to(throwAssertion()) + } +} diff --git a/Tests/CoreTests/ReplaySubjectTests.swift b/Tests/CoreTests/ReplaySubjectTests.swift new file mode 100644 index 00000000..6395e55c --- /dev/null +++ b/Tests/CoreTests/ReplaySubjectTests.swift @@ -0,0 +1,170 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Combine +import Nimble +import PillarboxCircumspect +import XCTest + +final class ReplaySubjectTests: XCTestCase { + func testEmptyBufferOfZero() { + let subject = ReplaySubject(bufferSize: 0) + expectNothingPublished(from: subject, during: .milliseconds(100)) + } + + func testEmptyBufferOfTwo() { + let subject = ReplaySubject(bufferSize: 2) + expectNothingPublished(from: subject, during: .milliseconds(100)) + } + + func testFilledBufferOfZero() { + let subject = ReplaySubject(bufferSize: 0) + subject.send(1) + expectNothingPublished(from: subject, during: .milliseconds(100)) + } + + func testFilledBufferOfTwo() { + let subject = ReplaySubject(bufferSize: 2) + subject.send(1) + subject.send(2) + subject.send(3) + expectEqualPublished(values: [2, 3], from: subject, during: .milliseconds(100)) + } + + func testNewValuesWithBufferOfZero() { + let subject = ReplaySubject(bufferSize: 0) + subject.send(1) + expectEqualPublished(values: [2, 3], from: subject, during: .milliseconds(100)) { + subject.send(2) + subject.send(3) + } + } + + func testNewValuesWithBufferOfTwo() { + let subject = ReplaySubject(bufferSize: 2) + subject.send(1) + subject.send(2) + subject.send(3) + expectEqualPublished(values: [2, 3, 4, 5], from: subject, during: .milliseconds(100)) { + subject.send(4) + subject.send(5) + } + } + + func testMultipleSubscribers() { + let subject = ReplaySubject(bufferSize: 2) + subject.send(1) + subject.send(2) + subject.send(3) + expectEqualPublished(values: [2, 3], from: subject, during: .milliseconds(100)) + expectEqualPublished(values: [2, 3], from: subject, during: .milliseconds(100)) + } + + func testSubscriptionRelease() { + let subject = ReplaySubject(bufferSize: 1) + subject.send(1) + + _ = subject.sink { _ in } + + expect(subject.subscriptions).to(beEmpty()) + } + + func testNewValuesWithMultipleSubscribers() { + let subject = ReplaySubject(bufferSize: 2) + subject.send(1) + subject.send(2) + subject.send(3) + expectEqualPublished(values: [2, 3, 4], from: subject, during: .milliseconds(100)) { + subject.send(4) + } + expectEqualPublished(values: [3, 4], from: subject, during: .milliseconds(100)) + } + + func testCompletion() { + let subject = ReplaySubject(bufferSize: 2) + expectOnlyEqualPublished(values: [1], from: subject) { + subject.send(1) + subject.send(completion: .finished) + } + } + + func testNoValueAfterCompletion() { + let subject = ReplaySubject(bufferSize: 2) + subject.send(1) + subject.send(completion: .finished) + subject.send(2) + expectEqualPublished(values: [1], from: subject, during: .milliseconds(100)) + } + + func testCompletionWithMultipleSubscribers() { + let subject = ReplaySubject(bufferSize: 2) + expectOnlyEqualPublished(values: [1], from: subject) { + subject.send(1) + subject.send(completion: .finished) + } + expectOnlyEqualPublished(values: [1], from: subject) + } + + func testRequestLessValuesThanAvailable() { + let subject = ReplaySubject(bufferSize: 3) + subject.send(1) + subject.send(2) + subject.send(3) + + var results = [Int]() + subject + .subscribe(AnySubscriber( + receiveSubscription: { subscription in + subscription.request(.max(2)) + }, + receiveValue: { value in + results.append(value) + return .none + }, + receiveCompletion: { _ in } + )) + expect(results).to(equalDiff([1, 2])) + } + + func testThreadSafety() { + let replaySubject = ReplaySubject(bufferSize: 3) + for i in 0..<100 { + DispatchQueue.global().async { + replaySubject.send(i) + } + } + } + + func testDeliveryOrderInRecursiveScenario() { + let subject = ReplaySubject(bufferSize: 1) + var cancellables = Set() + + var values: [String] = [] + + subject.sink { i in + values.append("A\(i)") + } + .store(in: &cancellables) + + subject.sink { i in + values.append("B\(i)") + if i == 1 { + subject.send(2) + } + } + .store(in: &cancellables) + + subject.sink { i in + values.append("C\(i)") + } + .store(in: &cancellables) + + subject.send(1) + expect(values).to(equalDiff(["A1", "B1", "A2", "B2", "C1", "C2"])) + } +} diff --git a/Tests/CoreTests/SlicePublisherTests.swift b/Tests/CoreTests/SlicePublisherTests.swift new file mode 100644 index 00000000..30caeca8 --- /dev/null +++ b/Tests/CoreTests/SlicePublisherTests.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Combine +import PillarboxCircumspect +import XCTest + +private struct Person: Equatable { + let firstName: String + let lastName: String +} + +final class SlicePublisherTests: XCTestCase { + func testDelivery() { + let publisher = [ + Person(firstName: "Jane", lastName: "Doe"), + Person(firstName: "Jane", lastName: "Smith"), + Person(firstName: "John", lastName: "Bridges") + ].publisher.slice(at: \.firstName) + expectEqualPublished(values: ["Jane", "John"], from: publisher) + } +} diff --git a/Tests/CoreTests/StopwatchTests.swift b/Tests/CoreTests/StopwatchTests.swift new file mode 100644 index 00000000..4da359cf --- /dev/null +++ b/Tests/CoreTests/StopwatchTests.swift @@ -0,0 +1,70 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Nimble +import XCTest + +final class StopwatchTests: XCTestCase { + func testCreation() { + let stopwatch = Stopwatch() + wait(for: .milliseconds(500)) + expect(stopwatch.time()).to(equal(0)) + } + + func testStart() { + let stopwatch = Stopwatch() + stopwatch.start() + wait(for: .milliseconds(500)) + expect(stopwatch.time() * 1000).to(beCloseTo(500, within: 100)) + } + + func testStartAndStop() { + let stopwatch = Stopwatch() + stopwatch.start() + wait(for: .milliseconds(200)) + stopwatch.stop() + wait(for: .milliseconds(200)) + expect(stopwatch.time() * 1000).to(beCloseTo(200, within: 100)) + } + + func testStopWithoutStart() { + let stopwatch = Stopwatch() + stopwatch.stop() + wait(for: .milliseconds(200)) + expect(stopwatch.time() * 1000).to(beCloseTo(0, within: 100)) + } + + func testReset() { + let stopwatch = Stopwatch() + stopwatch.start() + wait(for: .milliseconds(200)) + stopwatch.reset() + wait(for: .milliseconds(100)) + expect(stopwatch.time()).to(equal(0)) + } + + func testMultipleStarts() { + let stopwatch = Stopwatch() + stopwatch.start() + wait(for: .milliseconds(200)) + stopwatch.start() + wait(for: .milliseconds(200)) + expect(stopwatch.time() * 1000).to(beCloseTo(400, within: 100)) + } + + func testAccumulation() { + let stopwatch = Stopwatch() + stopwatch.start() + wait(for: .milliseconds(200)) + stopwatch.stop() + wait(for: .milliseconds(200)) + stopwatch.start() + wait(for: .milliseconds(200)) + expect(stopwatch.time() * 1000).to(beCloseTo(400, within: 100)) + } +} diff --git a/Tests/CoreTests/TimeTests.swift b/Tests/CoreTests/TimeTests.swift new file mode 100644 index 00000000..f1c1ca18 --- /dev/null +++ b/Tests/CoreTests/TimeTests.swift @@ -0,0 +1,78 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import CoreMedia +import Nimble +import XCTest + +final class TimeTests: XCTestCase { + func testCloseWithFiniteTimes() { + expect(CMTime.close(within: 0)(CMTime.zero, .zero)).to(beTrue()) + expect(CMTime.close(within: 0.5)(CMTime.zero, .zero)).to(beTrue()) + + expect(CMTime.close(within: 0.5)(CMTime(value: 2, timescale: 1), CMTime(value: 2, timescale: 1))).to(beTrue()) + expect(CMTime.close(within: 0.5)(CMTime(value: 2, timescale: 1), CMTime(value: 200, timescale: 100))).to(beTrue()) + expect(CMTime.close(within: 0.5)(CMTime.zero, CMTime(value: 1, timescale: 2))).to(beTrue()) + expect(CMTime.close(within: 0.5)(CMTime.zero, CMTime(value: 2, timescale: 1))).to(beFalse()) + + expect(CMTime.close(within: 0)(CMTime.zero, CMTime(value: 1, timescale: 10000))).to(beFalse()) + } + + func testCloseWithPositiveInfiniteValues() { + expect(CMTime.close(within: 0)(CMTime.positiveInfinity, .positiveInfinity)).to(beTrue()) + expect(CMTime.close(within: 0.5)(CMTime.positiveInfinity, .positiveInfinity)).to(beTrue()) + + expect(CMTime.close(within: 10000)(CMTime.positiveInfinity, .zero)).to(beFalse()) + expect(CMTime.close(within: 10000)(CMTime.positiveInfinity, .negativeInfinity)).to(beFalse()) + expect(CMTime.close(within: 10000)(CMTime.positiveInfinity, .indefinite)).to(beFalse()) + expect(CMTime.close(within: 10000)(CMTime.positiveInfinity, .invalid)).to(beFalse()) + } + + func testCloseWithMinusInfiniteValues() { + expect(CMTime.close(within: 0)(CMTime.negativeInfinity, .negativeInfinity)).to(beTrue()) + expect(CMTime.close(within: 0.5)(CMTime.negativeInfinity, .negativeInfinity)).to(beTrue()) + + expect(CMTime.close(within: 10000)(CMTime.negativeInfinity, .zero)).to(beFalse()) + expect(CMTime.close(within: 10000)(CMTime.negativeInfinity, .positiveInfinity)).to(beFalse()) + expect(CMTime.close(within: 10000)(CMTime.negativeInfinity, .indefinite)).to(beFalse()) + expect(CMTime.close(within: 10000)(CMTime.negativeInfinity, .invalid)).to(beFalse()) + } + + func testCloseWithIndefiniteValues() { + expect(CMTime.close(within: 0)(CMTime.indefinite, .indefinite)).to(beTrue()) + expect(CMTime.close(within: 0.5)(CMTime.indefinite, .indefinite)).to(beTrue()) + + expect(CMTime.close(within: 10000)(CMTime.indefinite, .zero)).to(beFalse()) + expect(CMTime.close(within: 10000)(CMTime.indefinite, .positiveInfinity)).to(beFalse()) + expect(CMTime.close(within: 10000)(CMTime.indefinite, .negativeInfinity)).to(beFalse()) + expect(CMTime.close(within: 10000)(CMTime.indefinite, .invalid)).to(beFalse()) + } + + func testCloseWithInvalidValues() { + expect(CMTime.close(within: 0)(CMTime.invalid, .invalid)).to(beTrue()) + expect(CMTime.close(within: 0.5)(CMTime.invalid, .invalid)).to(beTrue()) + + expect(CMTime.close(within: 10000)(CMTime.invalid, .zero)).to(beFalse()) + expect(CMTime.close(within: 10000)(CMTime.invalid, .positiveInfinity)).to(beFalse()) + expect(CMTime.close(within: 10000)(CMTime.invalid, .negativeInfinity)).to(beFalse()) + expect(CMTime.close(within: 10000)(CMTime.invalid, .indefinite)).to(beFalse()) + } + + func testTimeRangeIsValidAndNotEmpty() { + expect(CMTimeRange.invalid.isValidAndNotEmpty).to(beFalse()) + expect(CMTimeRange.zero.isValidAndNotEmpty).to(beFalse()) + expect(CMTimeRange( + start: CMTime(value: 1, timescale: 1), + end: CMTime(value: 1, timescale: 1) + ).isValidAndNotEmpty).to(beFalse()) + expect(CMTimeRange( + start: CMTime(value: 0, timescale: 1), + end: CMTime(value: 1, timescale: 1) + ).isValidAndNotEmpty).to(beTrue()) + } +} diff --git a/Tests/CoreTests/Tools.swift b/Tests/CoreTests/Tools.swift new file mode 100644 index 00000000..53b84ec1 --- /dev/null +++ b/Tests/CoreTests/Tools.swift @@ -0,0 +1,21 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import Foundation + +final class TestNSObject: NSObject {} + +final class TestObject { + let identifier: String + + init(identifier: String = UUID().uuidString) { + self.identifier = identifier + } +} + +extension Notification.Name { + static let testNotification = Notification.Name("TestNotification") +} diff --git a/Tests/CoreTests/TriggerTests.swift b/Tests/CoreTests/TriggerTests.swift new file mode 100644 index 00000000..493f225d --- /dev/null +++ b/Tests/CoreTests/TriggerTests.swift @@ -0,0 +1,47 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Combine +import PillarboxCircumspect +import XCTest + +final class TriggerTests: XCTestCase { + func testInactive() { + let trigger = Trigger() + expectNothingPublished(from: trigger.signal(activatedBy: 1), during: .seconds(1)) + } + + func testActiveWithSignal() { + let trigger = Trigger() + expectAtLeastEqualPublished(values: ["out"], from: trigger.signal(activatedBy: 1).map { _ in "out" }) { + trigger.activate(for: 1) + } + } + + func testMultipleActivations() { + let trigger = Trigger() + expectAtLeastEqualPublished(values: ["out", "out"], from: trigger.signal(activatedBy: 1).map { _ in "out" }) { + trigger.activate(for: 1) + trigger.activate(for: 1) + } + } + + func testDifferentActivationIndex() { + let trigger = Trigger() + expectNothingPublished(from: trigger.signal(activatedBy: 1), during: .seconds(1)) { + trigger.activate(for: 2) + } + } + + func testHashableActivationIndex() { + let trigger = Trigger() + expectEqualPublished(values: ["out"], from: trigger.signal(activatedBy: "index").map { _ in "out" }, during: .seconds(1)) { + trigger.activate(for: "index") + } + } +} diff --git a/Tests/CoreTests/WaitPublisherTests.swift b/Tests/CoreTests/WaitPublisherTests.swift new file mode 100644 index 00000000..d5fe9b8b --- /dev/null +++ b/Tests/CoreTests/WaitPublisherTests.swift @@ -0,0 +1,25 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Combine +import PillarboxCircumspect +import XCTest + +final class WaitPublisherTests: XCTestCase { + func testWait() { + let signal = PassthroughSubject() + + let publisher = Just("Received") + .wait(untilOutputFrom: signal) + expectNothingPublished(from: publisher, during: .milliseconds(100)) + + expectEqualPublished(values: ["Received"], from: publisher, during: .milliseconds(100)) { + signal.send(()) + } + } +} diff --git a/Tests/CoreTests/WeakCapturePublisherTests.swift b/Tests/CoreTests/WeakCapturePublisherTests.swift new file mode 100644 index 00000000..49b55c97 --- /dev/null +++ b/Tests/CoreTests/WeakCapturePublisherTests.swift @@ -0,0 +1,39 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Combine +import Nimble +import XCTest + +final class WeakCapturePublisherTests: XCTestCase { + func testDeallocation() { + var object: TestObject? = TestObject() + let publisher = Just("output") + .weakCapture(object) + + weak var weakObject = object + autoreleasepool { + object = nil + } + expect(weakObject).to(beNil()) + + expectNothingPublished(from: publisher, during: .seconds(1)) + } + + func testDelivery() { + let object = TestObject(identifier: "weak_capture") + let publisher = Just("output") + .weakCapture(object, at: \.identifier) + expectAtLeastPublished( + values: [("output", "weak_capture")], + from: publisher + ) { output1, output2 in + output1.0 == output2.0 && output1.1 == output2.1 + } + } +} diff --git a/Tests/CoreTests/WithPreviousPublisherTests.swift b/Tests/CoreTests/WithPreviousPublisherTests.swift new file mode 100644 index 00000000..7cd92f10 --- /dev/null +++ b/Tests/CoreTests/WithPreviousPublisherTests.swift @@ -0,0 +1,45 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxCore + +import Combine +import PillarboxCircumspect +import XCTest + +final class WithPreviousPublisherTests: XCTestCase { + func testEmpty() { + expectNothingPublished(from: Empty().withPrevious(), during: .seconds(1)) + } + + func testPreviousValues() { + expectAtLeastEqualPublished( + values: [nil, 1, 2, 3, 4], + from: (1...5).publisher.withPrevious().map(\.previous) + ) + } + + func testCurrentValues() { + expectAtLeastEqualPublished( + values: [1, 2, 3, 4, 5], + from: (1...5).publisher.withPrevious().map(\.current) + ) + } + + func testOptionalPreviousValues() { + expectAtLeastEqualPublished( + values: [-1, 1, 2, 3, 4], + from: (1...5).publisher.withPrevious(-1).map(\.previous) + ) + } + + func testOptionalCurrentValues() { + expectAtLeastEqualPublished( + values: [1, 2, 3, 4, 5], + from: (1...5).publisher.withPrevious(-1).map(\.current) + ) + } +} diff --git a/Tests/MonitoringTests/MetricHitExpectation.swift b/Tests/MonitoringTests/MetricHitExpectation.swift new file mode 100644 index 00000000..9b6f33ce --- /dev/null +++ b/Tests/MonitoringTests/MetricHitExpectation.swift @@ -0,0 +1,67 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxMonitoring + +private struct _MetricHitExpectation: MetricHitExpectation where Data: Encodable { + let eventName: EventName + private let evaluate: (MetricPayload) -> Void + + init(eventName: EventName, evaluate: @escaping (MetricPayload) -> Void) { + self.eventName = eventName + self.evaluate = evaluate + } + + func evaluate(_ data: MetricPayload) { + evaluate(data) + } +} + +protocol MetricHitExpectation { + associatedtype Data: Encodable + + var eventName: EventName { get } + + func evaluate(_ data: MetricPayload) +} + +private extension MetricHitExpectation { + func match(payload: any Encodable, with expectation: any MetricHitExpectation) -> Bool { + guard let payload = payload as? MetricPayload, payload.eventName == expectation.eventName else { + return false + } + evaluate(payload) + return true + } +} + +extension _MetricHitExpectation: CustomDebugStringConvertible { + var debugDescription: String { + eventName.rawValue + } +} + +func match(payload: any Encodable, with expectation: any MetricHitExpectation) -> Bool { + expectation.match(payload: payload, with: expectation) +} + +extension MonitoringTestCase { + func error(evaluate: @escaping (MetricPayload) -> Void = { _ in }) -> some MetricHitExpectation { + _MetricHitExpectation(eventName: .error, evaluate: evaluate) + } + + func heartbeat(evaluate: @escaping (MetricPayload) -> Void = { _ in }) -> some MetricHitExpectation { + _MetricHitExpectation(eventName: .heartbeat, evaluate: evaluate) + } + + func start(evaluate: @escaping (MetricPayload) -> Void = { _ in }) -> some MetricHitExpectation { + _MetricHitExpectation(eventName: .start, evaluate: evaluate) + } + + func stop(evaluate: @escaping (MetricPayload) -> Void = { _ in }) -> some MetricHitExpectation { + _MetricHitExpectation(eventName: .stop, evaluate: evaluate) + } +} diff --git a/Tests/MonitoringTests/MetricPayload.swift b/Tests/MonitoringTests/MetricPayload.swift new file mode 100644 index 00000000..d18f3a8d --- /dev/null +++ b/Tests/MonitoringTests/MetricPayload.swift @@ -0,0 +1,13 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxMonitoring + +extension MetricPayload: CustomDebugStringConvertible { + public var debugDescription: String { + eventName.rawValue + } +} diff --git a/Tests/MonitoringTests/MetricsTracker.swift b/Tests/MonitoringTests/MetricsTracker.swift new file mode 100644 index 00000000..0a7a580d --- /dev/null +++ b/Tests/MonitoringTests/MetricsTracker.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import Foundation +import PillarboxMonitoring + +extension MetricsTracker.Configuration { + static let test = MetricsTracker.Configuration( + serviceUrl: URL(string: "https://localhost/ingest")! + ) + + static let heartbeatTest = MetricsTracker.Configuration( + serviceUrl: URL(string: "https://localhost/ingest")!, + heartbeatInterval: 1 + ) +} + +extension MetricsTracker.Metadata { + static let test = MetricsTracker.Metadata( + identifier: "identifier", + metadataUrl: URL(string: "https://localhost/metadata.json"), + assetUrl: URL(string: "https://localhost/asset.m3u8") + ) +} diff --git a/Tests/MonitoringTests/MetricsTrackerTests.swift b/Tests/MonitoringTests/MetricsTrackerTests.swift new file mode 100644 index 00000000..aa549333 --- /dev/null +++ b/Tests/MonitoringTests/MetricsTrackerTests.swift @@ -0,0 +1,225 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxMonitoring + +import Nimble +import PillarboxCircumspect +import PillarboxPlayer +import PillarboxStreams +import XCTest + +final class MetricsTrackerTests: MonitoringTestCase { + func testEntirePlayback() { + let player = Player(item: .simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [ + MetricsTracker.adapter(configuration: .test) { _ in .test } + ] + )) + expectAtLeastHits( + start(), + heartbeat(), + stop { payload in + expect(payload.data.position).to(beCloseTo(1000, within: 100)) + } + ) { + player.play() + } + } + + func testError() { + let player = Player(item: .simple( + url: Stream.unavailable.url, + trackerAdapters: [ + MetricsTracker.adapter(configuration: .test) { _ in .test } + ] + )) + expectAtLeastHits( + start(), + error { payload in + let data = payload.data + expect(data.name).to(equal("NSURLErrorDomain(-1100)")) + expect(data.message).to(equal("The requested URL was not found on this server.")) + expect(data.position).to(beNil()) + expect(data.vpn).to(beFalse()) + } + ) { + player.play() + } + } + + func testNoStopWithoutStart() { + var player: Player? = Player(item: .simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [ + MetricsTracker.adapter(configuration: .test) { _ in .test } + ] + )) + _ = player + expectNoHits(during: .milliseconds(500)) { + player = nil + } + } + + func testHeartbeats() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + MetricsTracker.adapter(configuration: .heartbeatTest) { _ in .test } + ] + )) + expectAtLeastHits(start(), heartbeat(), heartbeat()) { + player.play() + } + } + + func testSessionIdentifierRenewalWhenReplayingAfterEnd() { + let player = Player(item: .simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [ + MetricsTracker.adapter(configuration: .test) { _ in .test } + ] + )) + var sessionId: String? + expectAtLeastHits( + start { payload in + sessionId = payload.sessionId + }, + heartbeat { payload in + expect(payload.sessionId).to(equal(sessionId)) + }, + stop { payload in + expect(payload.sessionId).to(equal(sessionId)) + } + ) { + player.play() + } + expectAtLeastHits( + start { payload in + expect(payload.sessionId).notTo(equal(sessionId)) + } + ) { + player.replay() + } + } + + func testSessionIdentifierRenewalWhenReplayingAfterFatalError() { + let player = Player(item: .simple( + url: Stream.unavailable.url, + trackerAdapters: [ + MetricsTracker.adapter(configuration: .test) { _ in .test } + ] + )) + var sessionId: String? + expectAtLeastHits( + start { payload in + sessionId = payload.sessionId + }, + error { payload in + expect(payload.sessionId).to(equal(sessionId)) + } + ) + expectAtLeastHits( + start { payload in + expect(payload.sessionId).notTo(equal(sessionId)) + } + ) { + player.replay() + } + } + + func testSessionIdentifierClearedAfterPlaybackEnd() { + let player = Player(item: .simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [ + MetricsTracker.adapter(configuration: .test) { _ in .test } + ] + )) + expectAtLeastHits( + start(), + heartbeat(), + stop() + ) { + player.play() + } + expect(player.currentSessionIdentifiers(trackedBy: MetricsTracker.self)).to(beEmpty()) + } + + func testSessionIdentifierPersistenceAfterFatalError() { + let player = Player(item: .simple( + url: Stream.unavailable.url, + trackerAdapters: [ + MetricsTracker.adapter(configuration: .test) { _ in .test } + ] + )) + var sessionId: String? + expectAtLeastHits( + start { payload in + sessionId = payload.sessionId + }, + error() + ) + expect(player.currentSessionIdentifiers(trackedBy: MetricsTracker.self)).to(equalDiff([sessionId!])) + } + + func testPayloads() { + let player = Player(item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + MetricsTracker.adapter(configuration: .test) { .test } + ] + )) + expectAtLeastHits( + start { payload in + expect(payload.version).to(equal(1)) + + let data = payload.data + + let device = data.device + expect(device.id).notTo(beNil()) + expect(device.model).notTo(beNil()) + expect(device.type).notTo(beNil()) + + let media = data.media + expect(media.assetUrl).to(equal(URL(string: "https://localhost/asset.m3u8"))) + expect(media.id).to(equal("identifier")) + expect(media.metadataUrl).to(equal(URL(string: "https://localhost/metadata.json"))) + expect(media.origin).notTo(beNil()) + + let os = data.os + expect(os.name).notTo(beNil()) + expect(os.version).notTo(beNil()) + + let player = data.player + expect(player.name).to(equal("Pillarbox")) + expect(player.version).to(equal(Player.version)) + }, + heartbeat { payload in + expect(payload.version).to(equal(1)) + + let data = payload.data + expect(data.airplay).to(beFalse()) + expect(data.streamType).to(equal("On-demand")) + } + ) { + player.play() + } + } + + func testRepeatOne() { + let player = Player(item: .simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [ + MetricsTracker.adapter(configuration: .test) { _ in .test } + ] + )) + player.repeatMode = .one + expectAtLeastHits(start(), heartbeat(), stop(), start(), heartbeat(), stop()) { + player.play() + } + } +} diff --git a/Tests/MonitoringTests/MonitoringTestCase.swift b/Tests/MonitoringTests/MonitoringTestCase.swift new file mode 100644 index 00000000..c219fe26 --- /dev/null +++ b/Tests/MonitoringTests/MonitoringTestCase.swift @@ -0,0 +1,75 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxMonitoring + +import Dispatch +import PillarboxCircumspect +import XCTest + +class MonitoringTestCase: XCTestCase {} + +extension MonitoringTestCase { + /// Collects metric hits during some time interval and matches them against expectations. + func expectHits( + _ expectations: any MetricHitExpectation..., + during interval: DispatchTimeInterval = .seconds(20), + file: StaticString = #file, + line: UInt = #line, + while executing: (() -> Void)? = nil + ) { + MetricHitListener.captureMetricHits { publisher in + expectPublished( + values: expectations, + from: publisher, + to: match(payload:with:), + during: interval, + file: file, + line: line, + while: executing + ) + } + } + + /// Expects metric hits during some time interval and matches them against expectations. + func expectAtLeastHits( + _ expectations: any MetricHitExpectation..., + timeout: DispatchTimeInterval = .seconds(20), + file: StaticString = #file, + line: UInt = #line, + while executing: (() -> Void)? = nil + ) { + MetricHitListener.captureMetricHits { publisher in + expectAtLeastPublished( + values: expectations, + from: publisher, + to: match(payload:with:), + timeout: timeout, + file: file, + line: line, + while: executing + ) + } + } + + /// Expects no metric hits during some time interval. + func expectNoHits( + during interval: DispatchTimeInterval = .seconds(20), + file: StaticString = #file, + line: UInt = #line, + while executing: (() -> Void)? = nil + ) { + MetricHitListener.captureMetricHits { publisher in + expectNothingPublished( + from: publisher, + during: interval, + file: file, + line: line, + while: executing + ) + } + } +} diff --git a/Tests/MonitoringTests/TrackingSessionTests.swift b/Tests/MonitoringTests/TrackingSessionTests.swift new file mode 100644 index 00000000..2032f791 --- /dev/null +++ b/Tests/MonitoringTests/TrackingSessionTests.swift @@ -0,0 +1,41 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxMonitoring + +import Nimble +import XCTest + +final class TrackingSessionTests: XCTestCase { + func testEmpty() { + let session = TrackingSession() + expect(session.id).to(beNil()) + expect(session.isStarted).to(beFalse()) + } + + func testStart() { + var session = TrackingSession() + session.start() + expect(session.id).notTo(beNil()) + expect(session.isStarted).to(beTrue()) + } + + func testStop() { + var session = TrackingSession() + session.start() + session.stop() + expect(session.id).notTo(beNil()) + expect(session.isStarted).to(beFalse()) + } + + func testReset() { + var session = TrackingSession() + session.start() + session.reset() + expect(session.id).to(beNil()) + expect(session.isStarted).to(beFalse()) + } +} diff --git a/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatAllUpdateTests.swift b/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatAllUpdateTests.swift new file mode 100644 index 00000000..e8f55d8d --- /dev/null +++ b/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatAllUpdateTests.swift @@ -0,0 +1,189 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble +import PillarboxCircumspect + +final class AVPlayerItemRepeatAllUpdateTests: TestCase { + func testPlayerItemsWithoutCurrentItem() { + let previousContents: [AssetContent] = [ + .test(id: "1"), + .test(id: "2"), + .test(id: "3"), + .test(id: "4"), + .test(id: "5") + ] + let currentContents: [AssetContent] = [ + .test(id: "A"), + .test(id: "B"), + .test(id: "C") + ] + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: nil, + repeatMode: .all, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("B"), UUID("C"), UUID("A")])) + } + + func testPlayerItemsWithPreservedCurrentItem() { + let currentItemContent = AssetContent.test(id: "3") + let previousContents: [AssetContent] = [ + .test(id: "1"), + .test(id: "2"), + currentItemContent, + .test(id: "4"), + .test(id: "5") + ] + let currentContents = [ + .test(id: "A"), + currentItemContent, + .test(id: "B"), + .test(id: "C") + ] + let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: currentItem, + repeatMode: .all, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("3"), UUID("B"), UUID("C"), UUID("A")])) + expect(items.first).to(equal(currentItem)) + } + + func testPlayerItemsWithPreservedCurrentItemAtEnd() { + let currentItemContent = AssetContent.test(id: "3") + let previousContents = [ + .test(id: "1"), + .test(id: "2"), + currentItemContent, + .test(id: "4"), + .test(id: "5") + ] + let currentContents = [ + .test(id: "A"), + .test(id: "B"), + .test(id: "C"), + currentItemContent + ] + let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: currentItem, + repeatMode: .all, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("3"), UUID("A")])) + expect(items.first).to(equal(currentItem)) + } + + func testPlayerItemsWithUnknownCurrentItem() { + let previousContents: [AssetContent] = [ + .test(id: "1"), + .test(id: "2") + ] + let currentContents: [AssetContent] = [ + .test(id: "A"), + .test(id: "B") + ] + let unknownItem = AssetContent.test(id: "1").playerItem(configuration: .default, limits: .none) + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: unknownItem, + repeatMode: .all, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("B"), UUID("A")])) + } + + func testPlayerItemsWithCurrentItemReplacedByAnotherItem() { + let currentItemContent = AssetContent.test(id: "1") + let otherContent = AssetContent.test(id: "2") + let previousContents = [ + currentItemContent, + otherContent, + .test(id: "3") + ] + let currentContents = [ + .test(id: "3"), + otherContent, + .test(id: "C") + ] + let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: currentItem, + repeatMode: .all, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("2"), UUID("C"), UUID("3")])) + } + + func testPlayerItemsWithUpdatedCurrentItem() { + let currentItemContent = AssetContent.test(id: "1") + let previousContents: [AssetContent] = [ + .test(id: "1"), + .test(id: "2"), + .test(id: "3") + ] + let currentContents = [ + currentItemContent, + .test(id: "2"), + .test(id: "3") + ] + let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: currentItem, + repeatMode: .all, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("1"), UUID("2"), UUID("3"), UUID("1")])) + expect(items.first).to(equal(currentItem)) + } + + func testPlayerItemsLength() { + let currentContents: [AssetContent] = [ + .test(id: "A"), + .test(id: "B"), + .test(id: "C"), + .test(id: "D") + ] + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: [], + currentItem: nil, + repeatMode: .all, + length: 2, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("B")])) + } +} diff --git a/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatOffUpdateTests.swift b/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatOffUpdateTests.swift new file mode 100644 index 00000000..8930e0bf --- /dev/null +++ b/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatOffUpdateTests.swift @@ -0,0 +1,189 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble +import PillarboxCircumspect + +final class AVPlayerItemRepeatOffUpdateTests: TestCase { + func testPlayerItemsWithoutCurrentItem() { + let previousContents: [AssetContent] = [ + .test(id: "1"), + .test(id: "2"), + .test(id: "3"), + .test(id: "4"), + .test(id: "5") + ] + let currentContents: [AssetContent] = [ + .test(id: "A"), + .test(id: "B"), + .test(id: "C") + ] + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: nil, + repeatMode: .off, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("B"), UUID("C")])) + } + + func testPlayerItemsWithPreservedCurrentItem() { + let currentItemContent = AssetContent.test(id: "3") + let previousContents: [AssetContent] = [ + .test(id: "1"), + .test(id: "2"), + currentItemContent, + .test(id: "4"), + .test(id: "5") + ] + let currentContents = [ + .test(id: "A"), + currentItemContent, + .test(id: "B"), + .test(id: "C") + ] + let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: currentItem, + repeatMode: .off, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("3"), UUID("B"), UUID("C")])) + expect(items.first).to(equal(currentItem)) + } + + func testPlayerItemsWithPreservedCurrentItemAtEnd() { + let currentItemContent = AssetContent.test(id: "3") + let previousContents = [ + .test(id: "1"), + .test(id: "2"), + currentItemContent, + .test(id: "4"), + .test(id: "5") + ] + let currentContents = [ + .test(id: "A"), + .test(id: "B"), + .test(id: "C"), + currentItemContent + ] + let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: currentItem, + repeatMode: .off, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("3")])) + expect(items.first).to(equal(currentItem)) + } + + func testPlayerItemsWithUnknownCurrentItem() { + let previousContents: [AssetContent] = [ + .test(id: "1"), + .test(id: "2") + ] + let currentContents: [AssetContent] = [ + .test(id: "A"), + .test(id: "B") + ] + let unknownItem = AssetContent.test(id: "1").playerItem(configuration: .default, limits: .none) + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: unknownItem, + repeatMode: .off, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("B")])) + } + + func testPlayerItemsWithCurrentItemReplacedByAnotherItem() { + let currentItemContent = AssetContent.test(id: "1") + let otherContent = AssetContent.test(id: "2") + let previousContents = [ + currentItemContent, + otherContent, + .test(id: "3") + ] + let currentContents = [ + .test(id: "3"), + otherContent, + .test(id: "C") + ] + let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: currentItem, + repeatMode: .off, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("2"), UUID("C")])) + } + + func testPlayerItemsWithUpdatedCurrentItem() { + let currentItemContent = AssetContent.test(id: "1") + let previousContents: [AssetContent] = [ + .test(id: "1"), + .test(id: "2"), + .test(id: "3") + ] + let currentContents = [ + currentItemContent, + .test(id: "2"), + .test(id: "3") + ] + let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: currentItem, + repeatMode: .off, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("1"), UUID("2"), UUID("3")])) + expect(items.first).to(equal(currentItem)) + } + + func testPlayerItemsLength() { + let currentContents: [AssetContent] = [ + .test(id: "A"), + .test(id: "B"), + .test(id: "C"), + .test(id: "D") + ] + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: [], + currentItem: nil, + repeatMode: .off, + length: 2, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("B")])) + } +} diff --git a/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatOneUpdateTests.swift b/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatOneUpdateTests.swift new file mode 100644 index 00000000..3c863d83 --- /dev/null +++ b/Tests/PlayerTests/AVPlayer/AVPlayerItemRepeatOneUpdateTests.swift @@ -0,0 +1,189 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble +import PillarboxCircumspect + +final class AVPlayerItemRepeatOneUpdateTests: TestCase { + func testPlayerItemsWithoutCurrentItem() { + let previousContents: [AssetContent] = [ + .test(id: "1"), + .test(id: "2"), + .test(id: "3"), + .test(id: "4"), + .test(id: "5") + ] + let currentContents: [AssetContent] = [ + .test(id: "A"), + .test(id: "B"), + .test(id: "C") + ] + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: nil, + repeatMode: .one, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("A"), UUID("B"), UUID("C")])) + } + + func testPlayerItemsWithPreservedCurrentItem() { + let currentItemContent = AssetContent.test(id: "3") + let previousContents: [AssetContent] = [ + .test(id: "1"), + .test(id: "2"), + currentItemContent, + .test(id: "4"), + .test(id: "5") + ] + let currentContents = [ + .test(id: "A"), + currentItemContent, + .test(id: "B"), + .test(id: "C") + ] + let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: currentItem, + repeatMode: .one, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("3"), UUID("3"), UUID("B"), UUID("C")])) + expect(items.first).to(equal(currentItem)) + } + + func testPlayerItemsWithPreservedCurrentItemAtEnd() { + let currentItemContent = AssetContent.test(id: "3") + let previousContents = [ + .test(id: "1"), + .test(id: "2"), + currentItemContent, + .test(id: "4"), + .test(id: "5") + ] + let currentContents = [ + .test(id: "A"), + .test(id: "B"), + .test(id: "C"), + currentItemContent + ] + let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: currentItem, + repeatMode: .one, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("3"), UUID("3")])) + expect(items.first).to(equal(currentItem)) + } + + func testPlayerItemsWithUnknownCurrentItem() { + let previousContents: [AssetContent] = [ + .test(id: "1"), + .test(id: "2") + ] + let currentContents: [AssetContent] = [ + .test(id: "A"), + .test(id: "B") + ] + let unknownItem = AssetContent.test(id: "1").playerItem(configuration: .default, limits: .none) + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: unknownItem, + repeatMode: .one, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("A"), UUID("B")])) + } + + func testPlayerItemsWithCurrentItemReplacedByAnotherItem() { + let currentItemContent = AssetContent.test(id: "1") + let otherContent = AssetContent.test(id: "2") + let previousContents = [ + currentItemContent, + otherContent, + .test(id: "3") + ] + let currentContents = [ + .test(id: "3"), + otherContent, + .test(id: "C") + ] + let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: currentItem, + repeatMode: .one, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("2"), UUID("2"), UUID("C")])) + } + + func testPlayerItemsWithUpdatedCurrentItem() { + let currentItemContent = AssetContent.test(id: "1") + let previousContents: [AssetContent] = [ + .test(id: "1"), + .test(id: "2"), + .test(id: "3") + ] + let currentContents = [ + currentItemContent, + .test(id: "2"), + .test(id: "3") + ] + let currentItem = currentItemContent.playerItem(configuration: .default, limits: .none) + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: previousContents, + currentItem: currentItem, + repeatMode: .one, + length: .max, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("1"), UUID("1"), UUID("2"), UUID("3")])) + expect(items.first).to(equal(currentItem)) + } + + func testPlayerItemsLength() { + let currentContents: [AssetContent] = [ + .test(id: "A"), + .test(id: "B"), + .test(id: "C"), + .test(id: "D") + ] + let items = AVPlayerItem.playerItems( + for: currentContents, + replacing: [], + currentItem: nil, + repeatMode: .one, + length: 2, + configuration: .default, + limits: .none + ) + expect(items.map(\.id)).to(equalDiff([UUID("A"), UUID("A")])) + } +} diff --git a/Tests/PlayerTests/AVPlayer/AVPlayerItemTests.swift b/Tests/PlayerTests/AVPlayer/AVPlayerItemTests.swift new file mode 100644 index 00000000..07de1710 --- /dev/null +++ b/Tests/PlayerTests/AVPlayer/AVPlayerItemTests.swift @@ -0,0 +1,101 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble +import PillarboxStreams + +final class AVPlayerItemTests: TestCase { + func testNonLoadedItem() { + let item = AVPlayerItem(url: Stream.onDemand.url) + expect(item.timeRange).toAlways(equal(.invalid), until: .seconds(1)) + } + + func testOnDemand() { + let item = AVPlayerItem(url: Stream.onDemand.url) + _ = AVPlayer(playerItem: item) + expect(item.timeRange).toEventuallyNot(equal(.invalid)) + } + + func testPlayerItemsWithRepeatOff() { + let items = [ + PlayerItem.simple(url: Stream.onDemand.url), + PlayerItem.simple(url: Stream.shortOnDemand.url), + PlayerItem.simple(url: Stream.live.url) + ] + expect { + AVPlayerItem.playerItems( + from: items, + after: 0, + repeatMode: .off, + length: .max, + reload: false, + configuration: .default, + limits: .none + ) + .compactMap(\.url) + } + .toEventually(equal([ + Stream.onDemand.url, + Stream.shortOnDemand.url, + Stream.live.url + ])) + } + + func testPlayerItemsWithRepeatOne() { + let items = [ + PlayerItem.simple(url: Stream.onDemand.url), + PlayerItem.simple(url: Stream.shortOnDemand.url), + PlayerItem.simple(url: Stream.live.url) + ] + expect { + AVPlayerItem.playerItems( + from: items, + after: 0, + repeatMode: .one, + length: .max, + reload: false, + configuration: .default, + limits: .none + ) + .compactMap(\.url) + } + .toEventually(equal([ + Stream.onDemand.url, + Stream.onDemand.url, + Stream.shortOnDemand.url, + Stream.live.url + ])) + } + + func testPlayerItemsWithRepeatAll() { + let items = [ + PlayerItem.simple(url: Stream.onDemand.url), + PlayerItem.simple(url: Stream.shortOnDemand.url), + PlayerItem.simple(url: Stream.live.url) + ] + expect { + AVPlayerItem.playerItems( + from: items, + after: 0, + repeatMode: .all, + length: .max, + reload: false, + configuration: .default, + limits: .none + ) + .compactMap(\.url) + } + .toEventually(equal([ + Stream.onDemand.url, + Stream.shortOnDemand.url, + Stream.live.url, + Stream.onDemand.url + ])) + } +} diff --git a/Tests/PlayerTests/AVPlayer/AVPlayerTests.swift b/Tests/PlayerTests/AVPlayer/AVPlayerTests.swift new file mode 100644 index 00000000..d7a7351e --- /dev/null +++ b/Tests/PlayerTests/AVPlayer/AVPlayerTests.swift @@ -0,0 +1,50 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble +import PillarboxStreams + +final class AVPlayerTests: TestCase { + func testTimeRangeWhenEmpty() { + let player = AVPlayer() + expect(player.timeRange).to(equal(.invalid)) + } + + func testTimeRangeForOnDemand() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = AVPlayer(playerItem: item) + expect(player.timeRange).toEventually(equal(CMTimeRange(start: .zero, duration: Stream.onDemand.duration))) + } + + func testDurationWhenEmpty() { + let player = AVPlayer() + expect(player.duration).to(equal(.invalid)) + } + + func testDurationForOnDemand() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = AVPlayer(playerItem: item) + expect(player.duration).to(equal(.invalid)) + expect(player.duration).toEventually(equal(Stream.onDemand.duration)) + } + + func testDurationForLive() { + let item = AVPlayerItem(url: Stream.live.url) + let player = AVPlayer(playerItem: item) + expect(player.duration).to(equal(.invalid)) + expect(player.duration).toEventually(equal(.indefinite)) + } + + func testDurationForDvr() { + let item = AVPlayerItem(url: Stream.dvr.url) + let player = AVPlayer(playerItem: item) + expect(player.duration).to(equal(.invalid)) + expect(player.duration).toEventually(equal(.indefinite)) + } +} diff --git a/Tests/PlayerTests/Asset/AssetCreationTests.swift b/Tests/PlayerTests/Asset/AssetCreationTests.swift new file mode 100644 index 00000000..6e6def6b --- /dev/null +++ b/Tests/PlayerTests/Asset/AssetCreationTests.swift @@ -0,0 +1,29 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class AssetCreationTests: TestCase { + func testSimpleAsset() { + let asset = Asset.simple(url: Stream.onDemand.url) + expect(asset.resource).to(equal(.simple(url: Stream.onDemand.url))) + } + + func testCustomAsset() { + let delegate = ResourceLoaderDelegateMock() + let asset = Asset.custom(url: Stream.onDemand.url, delegate: delegate) + expect(asset.resource).to(equal(.custom(url: Stream.onDemand.url, delegate: delegate))) + } + + func testEncryptedAsset() { + let delegate = ContentKeySessionDelegateMock() + let asset = Asset.encrypted(url: Stream.onDemand.url, delegate: delegate) + expect(asset.resource).to(equal(.encrypted(url: Stream.onDemand.url, delegate: delegate))) + } +} diff --git a/Tests/PlayerTests/Asset/AssetMetadataMock.swift b/Tests/PlayerTests/Asset/AssetMetadataMock.swift new file mode 100644 index 00000000..37943c2f --- /dev/null +++ b/Tests/PlayerTests/Asset/AssetMetadataMock.swift @@ -0,0 +1,23 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import PillarboxPlayer + +struct AssetMetadataMock: Decodable { + let title: String + let subtitle: String? + + init(title: String, subtitle: String? = nil) { + self.title = title + self.subtitle = subtitle + } +} + +extension AssetMetadataMock: AssetMetadata { + var playerMetadata: PlayerMetadata { + .init(title: title, subtitle: subtitle) + } +} diff --git a/Tests/PlayerTests/Asset/ResourceItemTests.swift b/Tests/PlayerTests/Asset/ResourceItemTests.swift new file mode 100644 index 00000000..78521d4c --- /dev/null +++ b/Tests/PlayerTests/Asset/ResourceItemTests.swift @@ -0,0 +1,41 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import PillarboxCircumspect +import PillarboxStreams + +final class ResourceItemTests: TestCase { + func testNativePlayerItem() { + let item = Resource.simple(url: Stream.onDemand.url).playerItem(configuration: .default, limits: .none) + _ = AVPlayer(playerItem: item) + expectAtLeastEqualPublished( + values: [false, true], + from: item.publisher(for: \.isPlaybackLikelyToKeepUp) + ) + } + + func testLoadingPlayerItem() { + let item = Resource.loading.playerItem(configuration: .default, limits: .none) + _ = AVPlayer(playerItem: item) + expectAtLeastEqualPublished( + values: [false], + from: item.publisher(for: \.isPlaybackLikelyToKeepUp) + ) + } + + func testFailingPlayerItem() { + let item = Resource.failing(error: StructError()).playerItem(configuration: .default, limits: .none) + _ = AVPlayer(playerItem: item) + expectEqualPublished( + values: [.unknown], + from: item.statusPublisher(), + during: .seconds(1) + ) + } +} diff --git a/Tests/PlayerTests/AudioSession/AVAudioSessionNotificationTests.swift b/Tests/PlayerTests/AudioSession/AVAudioSessionNotificationTests.swift new file mode 100644 index 00000000..b0e5489c --- /dev/null +++ b/Tests/PlayerTests/AudioSession/AVAudioSessionNotificationTests.swift @@ -0,0 +1,71 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFAudio +import Nimble + +final class AVAudioSessionNotificationTests: TestCase { + override func setUp() { + AVAudioSession.enableUpdateNotifications() + try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, policy: .default, options: []) + } + + func testUpdateWithSetCategoryModePolicyOptions() throws { + let audioSession = AVAudioSession.sharedInstance() + expect { + try audioSession.setCategory(.playback, mode: .default, policy: .default, options: [.duckOthers]) + }.to(postNotifications(equal([ + Notification(name: .didUpdateAudioSessionOptions, object: audioSession) + ]))) + } + + func testNoUpdateWithSetCategoryModePolicyOptions() throws { + let audioSession = AVAudioSession.sharedInstance() + expect { + try audioSession.setCategory(.playback, mode: .default, policy: .default, options: []) + }.notTo(postNotifications(equal([ + Notification(name: .didUpdateAudioSessionOptions, object: audioSession) + ]))) + } + + func testUpdateWithSetCategoryModeOptions() throws { + let audioSession = AVAudioSession.sharedInstance() + expect { + try audioSession.setCategory(.playback, mode: .default, options: [.duckOthers]) + }.to(postNotifications(equal([ + Notification(name: .didUpdateAudioSessionOptions, object: audioSession) + ]))) + } + + func testNoUpdateWithSetCategoryModeOptions() throws { + let audioSession = AVAudioSession.sharedInstance() + expect { + try audioSession.setCategory(.playback, mode: .default, options: []) + }.notTo(postNotifications(equal([ + Notification(name: .didUpdateAudioSessionOptions, object: audioSession) + ]))) + } + + func testUpdateWithSetCategoryOptions() throws { + let audioSession = AVAudioSession.sharedInstance() + expect { + try audioSession.setCategory(.playback, options: [.duckOthers]) + }.to(postNotifications(equal([ + Notification(name: .didUpdateAudioSessionOptions, object: audioSession) + ]))) + } + + func testNoUpdateWithSetCategoryOptions() throws { + let audioSession = AVAudioSession.sharedInstance() + expect { + try audioSession.setCategory(.playback, options: []) + }.notTo(postNotifications(equal([ + Notification(name: .didUpdateAudioSessionOptions, object: audioSession) + ]))) + } +} diff --git a/Tests/PlayerTests/Extensions/AVPlayerItem.swift b/Tests/PlayerTests/Extensions/AVPlayerItem.swift new file mode 100644 index 00000000..31d5751d --- /dev/null +++ b/Tests/PlayerTests/Extensions/AVPlayerItem.swift @@ -0,0 +1,13 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import AVFoundation + +extension AVPlayerItem { + var url: URL? { + (asset as? AVURLAsset)?.url + } +} diff --git a/Tests/PlayerTests/Extensions/AssetContent.swift b/Tests/PlayerTests/Extensions/AssetContent.swift new file mode 100644 index 00000000..51d39a87 --- /dev/null +++ b/Tests/PlayerTests/Extensions/AssetContent.swift @@ -0,0 +1,16 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Foundation +import PillarboxStreams + +extension AssetContent { + static func test(id: Character) -> Self { + AssetContent(id: UUID(id), resource: .simple(url: Stream.onDemand.url), metadata: .empty, configuration: .default, dateInterval: nil) + } +} diff --git a/Tests/PlayerTests/Extensions/MetricEvent.swift b/Tests/PlayerTests/Extensions/MetricEvent.swift new file mode 100644 index 00000000..8294d7ff --- /dev/null +++ b/Tests/PlayerTests/Extensions/MetricEvent.swift @@ -0,0 +1,16 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +private struct AnyError: Error {} + +extension MetricEvent { + static let anyMetadata = Self(kind: .metadata(experience: .init(), service: .init())) + static let anyAsset = Self(kind: .asset(experience: .init())) + static let anyFailure = Self(kind: .failure(AnyError())) + static let anyWarning = Self(kind: .warning(AnyError())) +} diff --git a/Tests/PlayerTests/Extensions/Player.swift b/Tests/PlayerTests/Extensions/Player.swift new file mode 100644 index 00000000..667f9715 --- /dev/null +++ b/Tests/PlayerTests/Extensions/Player.swift @@ -0,0 +1,15 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Foundation + +extension Player { + var urls: [URL] { + queuePlayer.items().compactMap(\.url) + } +} diff --git a/Tests/PlayerTests/Extensions/UUID.swift b/Tests/PlayerTests/Extensions/UUID.swift new file mode 100644 index 00000000..5c30bf11 --- /dev/null +++ b/Tests/PlayerTests/Extensions/UUID.swift @@ -0,0 +1,21 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import Foundation + +extension UUID { + init(_ char: Character) { + self.init( + uuidString: """ + \(String(repeating: char, count: 8))\ + -\(String(repeating: char, count: 4))\ + -\(String(repeating: char, count: 4))\ + -\(String(repeating: char, count: 4))\ + -\(String(repeating: char, count: 12)) + """ + )! + } +} diff --git a/Tests/PlayerTests/MediaSelection/AVMediaSelectionGroupTests.swift b/Tests/PlayerTests/MediaSelection/AVMediaSelectionGroupTests.swift new file mode 100644 index 00000000..d6f2d98e --- /dev/null +++ b/Tests/PlayerTests/MediaSelection/AVMediaSelectionGroupTests.swift @@ -0,0 +1,58 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble + +final class AVMediaSelectionGroupTests: TestCase { + func testPreferredMediaSelectionOptionsWithCharacteristics() { + let options: [AVMediaSelectionOptionMock] = [ + .init(displayName: "Option 1 (music)", languageCode: "fr", characteristics: [.describesMusicAndSoundForAccessibility]), + .init(displayName: "Option 2", languageCode: "fr", characteristics: []), + .init(displayName: "Option 3 (music)", languageCode: "en", characteristics: [.describesMusicAndSoundForAccessibility]), + .init(displayName: "Option 4", languageCode: "it", characteristics: []) + ] + + expect( + AVMediaSelectionGroup.preferredMediaSelectionOptions( + from: options, + withMediaCharacteristics: [.describesMusicAndSoundForAccessibility] + ) + .map(\.displayName) + .sorted() + ) + .to(equal([ + "Option 1 (music)", + "Option 3 (music)", + "Option 4" + ])) + } + + func testPreferredMediaSelectionOptionsWithoutCharacteristics() { + let options: [AVMediaSelectionOptionMock] = [ + .init(displayName: "Option 1 (music)", languageCode: "fr", characteristics: [.describesMusicAndSoundForAccessibility]), + .init(displayName: "Option 2", languageCode: "fr", characteristics: []), + .init(displayName: "Option 3 (music)", languageCode: "en", characteristics: [.describesMusicAndSoundForAccessibility]), + .init(displayName: "Option 4", languageCode: "it", characteristics: []) + ] + + expect( + AVMediaSelectionGroup.preferredMediaSelectionOptions( + from: options, + withoutMediaCharacteristics: [.describesMusicAndSoundForAccessibility] + ) + .map(\.displayName) + .sorted() + ) + .to(equal([ + "Option 2", + "Option 3 (music)", + "Option 4" + ])) + } +} diff --git a/Tests/PlayerTests/MediaSelection/AVMediaSelectionOptionTests.swift b/Tests/PlayerTests/MediaSelection/AVMediaSelectionOptionTests.swift new file mode 100644 index 00000000..870c8b51 --- /dev/null +++ b/Tests/PlayerTests/MediaSelection/AVMediaSelectionOptionTests.swift @@ -0,0 +1,30 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble + +final class AVMediaSelectionOptionTests: TestCase { + func testSortedOptions() { + let option1 = AVMediaSelectionOptionMock(displayName: "English") + let option2 = AVMediaSelectionOptionMock(displayName: "French") + expect(option1 < option2).to(beTrue()) + } + + func testEqualOptions() { + let option1 = AVMediaSelectionOptionMock(displayName: "English") + let option2 = AVMediaSelectionOptionMock(displayName: "English") + expect(option1 < option2).to(beFalse()) + } + + func testSortedOptionsWithOriginal() { + let option1 = AVMediaSelectionOptionMock(displayName: "English") + let option2 = AVMediaSelectionOptionMock(displayName: "French", characteristics: [.isOriginalContent]) + expect(option2 < option1).to(beTrue()) + } +} diff --git a/Tests/PlayerTests/MediaSelection/MediaSelectionTests.swift b/Tests/PlayerTests/MediaSelection/MediaSelectionTests.swift new file mode 100644 index 00000000..f4a8a097 --- /dev/null +++ b/Tests/PlayerTests/MediaSelection/MediaSelectionTests.swift @@ -0,0 +1,297 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class MediaSelectionTests: TestCase { + func testCharacteristicsAndOptionsWhenEmpty() { + let player = Player() + expect(player.mediaSelectionCharacteristics).toAlways(beEmpty(), until: .seconds(2)) + expect(player.mediaSelectionOptions(for: .audible)).to(beEmpty()) + expect(player.mediaSelectionOptions(for: .legible)).to(beEmpty()) + expect(player.mediaSelectionOptions(for: .visual)).to(beEmpty()) + } + + func testCharacteristicsAndOptionsWhenAvailable() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + expect(player.mediaSelectionCharacteristics).toEventually(equal([.audible, .legible])) + expect(player.mediaSelectionOptions(for: .audible)).notTo(beEmpty()) + expect(player.mediaSelectionOptions(for: .legible)).notTo(beEmpty()) + expect(player.mediaSelectionOptions(for: .visual)).to(beEmpty()) + } + + func testCharacteristicsAndOptionsWhenFailed() { + let player = Player(item: .simple(url: Stream.unavailable.url)) + expect(player.mediaSelectionCharacteristics).toAlways(beEmpty(), until: .seconds(2)) + expect(player.mediaSelectionOptions(for: .audible)).to(beEmpty()) + expect(player.mediaSelectionOptions(for: .legible)).to(beEmpty()) + expect(player.mediaSelectionOptions(for: .visual)).to(beEmpty()) + } + + func testCharacteristicsAndOptionsWhenExhausted() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + expect(player.mediaSelectionCharacteristics).toEventuallyNot(beEmpty()) + player.play() + expect(player.mediaSelectionCharacteristics).toEventually(beEmpty()) + } + + func testCharacteristicsAndOptionsWhenUnavailable() { + let player = Player(item: .simple(url: Stream.onDemandWithoutOptions.url)) + expect(player.mediaSelectionCharacteristics).toAlways(beEmpty(), until: .seconds(2)) + expect(player.mediaSelectionOptions(for: .audible)).to(beEmpty()) + expect(player.mediaSelectionOptions(for: .legible)).to(beEmpty()) + expect(player.mediaSelectionOptions(for: .visual)).to(beEmpty()) + } + + func testCharacteristicsAndOptionsUpdateWhenAdvancingToNextItem() { + let player = Player(items: [ + .simple(url: Stream.onDemandWithOptions.url), + .simple(url: Stream.onDemandWithoutOptions.url) + ]) + expect(player.mediaSelectionCharacteristics).toEventuallyNot(beEmpty()) + player.advanceToNextItem() + expect(player.mediaSelectionCharacteristics).toEventually(beEmpty()) + } + + func testSingleAudibleOptionIsNeverReturned() { + let player = Player(item: .simple(url: Stream.onDemandWithSingleAudibleOption.url)) + expect(player.mediaSelectionCharacteristics).toEventually(equal([.audible])) + expect(player.mediaSelectionOptions(for: .audible)).to(beEmpty()) + } + + func testLegibleOptionsMustNotContainForcedSubtitles() { + let player = Player(item: .simple(url: Stream.onDemandWithForcedAndUnforcedLegibleOptions.url)) + expect(player.mediaSelectionCharacteristics).toEventually(equal([.audible, .legible])) + expect(player.mediaSelectionOptions(for: .legible)).to(haveCount(6)) + } + + func testInitialAudibleOption() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + expect(player.selectedMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("en")) + expect(player.currentMediaOption(for: .audible)).to(haveLanguageIdentifier("en")) + } + + func testInitialLegibleOptionWithAlwaysOnAccessibilityDisplayType() { + MediaAccessibilityDisplayType.alwaysOn(languageCode: "ja").apply() + + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + expect(player.selectedMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("ja")) + expect(player.currentMediaOption(for: .legible)).to(haveLanguageIdentifier("ja")) + } + + func testInitialLegibleOptionWithAutomaticAccessibilityDisplayType() { + MediaAccessibilityDisplayType.automatic.apply() + + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.automatic)) + expect(player.currentMediaOption(for: .legible)).to(equal(.off)) + } + + func testInitialLegibleOptionWithForcedOnlyAccessibilityDisplayType() { + MediaAccessibilityDisplayType.forcedOnly.apply() + + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.off)) + expect(player.currentMediaOption(for: .legible)).to(equal(.off)) + } + + func testInitialAudibleOptionWithoutAvailableOptions() { + let player = Player(item: .simple(url: Stream.onDemandWithoutOptions.url)) + expect(player.selectedMediaOption(for: .audible)).toAlways(equal(.off), until: .seconds(2)) + expect(player.currentMediaOption(for: .audible)).to(equal(.off)) + } + + func testInitialLegibleOptionWithoutAvailableOptions() { + MediaAccessibilityDisplayType.forcedOnly.apply() + + let player = Player(item: .simple(url: Stream.onDemandWithoutOptions.url)) + expect(player.selectedMediaOption(for: .legible)).toAlways(equal(.off), until: .seconds(2)) + expect(player.currentMediaOption(for: .legible)).to(equal(.off)) + } + + func testAudibleOptionUpdateWhenAdvancingToNextItem() { + let player = Player(items: [ + .simple(url: Stream.onDemandWithOptions.url), + .simple(url: Stream.onDemandWithoutOptions.url) + ]) + expect(player.selectedMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("en")) + player.advanceToNextItem() + expect(player.selectedMediaOption(for: .audible)).toEventually(equal(.off)) + } + + func testLegibleOptionUpdateWhenAdvancingToNextItem() { + MediaAccessibilityDisplayType.alwaysOn(languageCode: "fr").apply() + + let player = Player(items: [ + .simple(url: Stream.onDemandWithOptions.url), + .simple(url: Stream.onDemandWithoutOptions.url) + ]) + expect(player.selectedMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("fr")) + player.advanceToNextItem() + expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.off)) + } + + // When using AirPlay the receiver might offer forced subtitle selection, thus changing subtitles externally. In + // this case the perceived selected option must be `.off`. + @MainActor + func testLegibleOptionStaysOffEvenIfForcedSubtitlesAreEnabledExternally() async throws { + MediaAccessibilityDisplayType.alwaysOn(languageCode: "ja").apply() + + let player = Player(item: .simple(url: Stream.onDemandWithForcedAndUnforcedLegibleOptions.url)) + await expect(player.mediaSelectionOptions(for: .legible)).toEventuallyNot(beEmpty()) + + let group = try await player.group(for: .legible)! + let option = AVMediaSelectionGroup.mediaSelectionOptions( + from: group.options, + withMediaCharacteristics: [.containsOnlyForcedSubtitles] + ) + .first { option in + option.languageIdentifier == "ja" + }! + + // Simulates an external change using the low-level player API directly. + player.systemPlayer.currentItem?.select(option, in: group) + + await expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.off)) + } + + func testSelectAudibleOnOption() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + expect(player.mediaSelectionOptions(for: .audible)).toEventuallyNot(beEmpty()) + + player.select(mediaOption: player.mediaSelectionOptions(for: .audible).first { option in + option.languageIdentifier == "fr" + }!, for: .audible) + expect(player.selectedMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("fr")) + expect(player.currentMediaOption(for: .audible)).to(haveLanguageIdentifier("fr")) + } + + func testSelectAudibleAutomaticOptionDoesNothing() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + expect(player.mediaSelectionOptions(for: .audible)).toEventuallyNot(beEmpty()) + + player.select(mediaOption: .automatic, for: .audible) + expect(player.selectedMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("en")) + expect(player.currentMediaOption(for: .audible)).to(haveLanguageIdentifier("en")) + } + + func testSelectAudibleOffOptionDoesNothing() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + expect(player.mediaSelectionOptions(for: .audible)).toEventuallyNot(beEmpty()) + + player.select(mediaOption: .off, for: .audible) + expect(player.selectedMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("en")) + expect(player.currentMediaOption(for: .audible)).to(haveLanguageIdentifier("en")) + } + + func testSelectLegibleOnOption() { + MediaAccessibilityDisplayType.forcedOnly.apply() + + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + expect(player.mediaSelectionOptions(for: .legible)).toEventuallyNot(beEmpty()) + + player.select(mediaOption: player.mediaSelectionOptions(for: .legible).first { option in + option.languageIdentifier == "ja" + }!, for: .legible) + expect(player.selectedMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("ja")) + expect(player.currentMediaOption(for: .legible)).to(haveLanguageIdentifier("ja")) + } + + func testSelectLegibleAutomaticOption() { + MediaAccessibilityDisplayType.forcedOnly.apply() + + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + expect(player.mediaSelectionOptions(for: .legible)).toEventuallyNot(beEmpty()) + + player.select(mediaOption: .automatic, for: .legible) + expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.automatic)) + expect(player.currentMediaOption(for: .legible)).to(equal(.off)) + } + + func testSelectLegibleOffOption() { + MediaAccessibilityDisplayType.automatic.apply() + + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + expect(player.mediaSelectionOptions(for: .legible)).toEventuallyNot(beEmpty()) + + player.select(mediaOption: .off, for: .legible) + expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.off)) + expect(player.currentMediaOption(for: .legible)).to(equal(.off)) + } + + func testAudibleSelectionIsPreservedBetweenItems() { + MediaAccessibilityDisplayType.alwaysOn(languageCode: "en").apply() + + let player = Player(items: [ + .simple(url: Stream.onDemandWithOptions.url), + .simple(url: Stream.onDemandWithOptions.url) + ]) + expect(player.mediaSelectionOptions(for: .audible)).toEventuallyNot(beEmpty()) + + player.select(mediaOption: player.mediaSelectionOptions(for: .audible).first { option in + option.languageIdentifier == "fr" + }!, for: .audible) + expect(player.currentMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("fr")) + + player.advanceToNextItem() + expect(player.currentMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("fr")) + } + + func testLegibleSelectionIsPreservedBetweenItems() { + MediaAccessibilityDisplayType.alwaysOn(languageCode: "en").apply() + + let player = Player(items: [ + .simple(url: Stream.onDemandWithOptions.url), + .simple(url: Stream.onDemandWithOptions.url) + ]) + expect(player.mediaSelectionOptions(for: .legible)).toEventuallyNot(beEmpty()) + + player.select(mediaOption: player.mediaSelectionOptions(for: .legible).first { option in + option.languageIdentifier == "fr" + }!, for: .legible) + expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("fr")) + + player.advanceToNextItem() + expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("fr")) + } + + func testLegibleOptionSwitchFromOffToAutomatic() { + MediaAccessibilityDisplayType.forcedOnly.apply() + + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + expect(player.mediaSelectionOptions(for: .legible)).toEventuallyNot(beEmpty()) + player.select(mediaOption: .automatic, for: .legible) + + expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.automatic)) + expect(player.currentMediaOption(for: .legible)).to(equal(.off)) + } + + func testObservabilityWhenTogglingBetweenOffAndAutomatic() { + MediaAccessibilityDisplayType.forcedOnly.apply() + + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + expect(player.mediaSelectionOptions(for: .legible)).toEventuallyNot(beEmpty()) + + expectChange(from: player) { + player.select(mediaOption: .automatic, for: .legible) + } + expectChange(from: player) { + player.select(mediaOption: .off, for: .legible) + } + } +} + +private extension Player { + func group(for characteristic: AVMediaCharacteristic) async throws -> AVMediaSelectionGroup? { + guard let item = systemPlayer.currentItem else { return nil } + return try await item.asset.loadMediaSelectionGroup(for: characteristic) + } +} diff --git a/Tests/PlayerTests/MediaSelection/PreferredLanguagesForMediaSelectionTests.swift b/Tests/PlayerTests/MediaSelection/PreferredLanguagesForMediaSelectionTests.swift new file mode 100644 index 00000000..624dae8a --- /dev/null +++ b/Tests/PlayerTests/MediaSelection/PreferredLanguagesForMediaSelectionTests.swift @@ -0,0 +1,144 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble +import PillarboxStreams + +final class PreferredLanguagesForMediaSelectionTests: TestCase { + func testAudibleOptionMatchesAvailablePreferredLanguage() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + player.setMediaSelection(preferredLanguages: ["fr"], for: .audible) + expect(player.selectedMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("fr")) + } + + func testLegibleOptionMatchesAvailablePreferredLanguage() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + player.setMediaSelection(preferredLanguages: ["fr"], for: .legible) + expect(player.selectedMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("fr")) + } + + func testAudibleOptionIgnoresInvalidPreferredLanguage() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + player.setMediaSelection(preferredLanguages: ["xy"], for: .audible) + expect(player.currentMediaOption(for: .audible)).toNever(haveLanguageIdentifier("xy"), until: .seconds(2)) + } + + func testLegibleOptionIgnoresInvalidPreferredLanguage() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + player.setMediaSelection(preferredLanguages: ["xy"], for: .legible) + expect(player.currentMediaOption(for: .legible)).toNever(haveLanguageIdentifier("xy"), until: .seconds(2)) + } + + func testAudibleOptionIgnoresUnsupportedPreferredLanguage() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + player.setMediaSelection(preferredLanguages: ["it"], for: .audible) + expect(player.currentMediaOption(for: .audible)).toNever(haveLanguageIdentifier("it"), until: .seconds(2)) + } + + func testLegibleOptionIgnoresUnsupportedPreferredLanguage() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + player.setMediaSelection(preferredLanguages: ["it"], for: .legible) + expect(player.currentMediaOption(for: .legible)).toNever(haveLanguageIdentifier("it"), until: .seconds(2)) + } + + func testPreferredAudibleLanguageOverrideSelection() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + expect(player.mediaSelectionOptions(for: .audible)).toEventuallyNot(beEmpty()) + + player.select(mediaOption: player.mediaSelectionOptions(for: .audible).first { option in + option.languageIdentifier == "fr" + }!, for: .audible) + + player.setMediaSelection(preferredLanguages: ["en"], for: .audible) + expect(player.currentMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("en")) + } + + func testPreferredLegibleLanguageOverrideSelection() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + expect(player.mediaSelectionOptions(for: .legible)).toEventuallyNot(beEmpty()) + + player.select(mediaOption: player.mediaSelectionOptions(for: .legible).first { option in + option.languageIdentifier == "ja" + }!, for: .legible) + + player.setMediaSelection(preferredLanguages: ["fr"], for: .legible) + expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("fr")) + } + + func testPreferredAudibleLanguageIsPreservedBetweenItems() { + let player = Player(items: [ + .simple(url: Stream.onDemandWithOptions.url), + .simple(url: Stream.onDemandWithOptions.url) + ]) + player.setMediaSelection(preferredLanguages: ["fr"], for: .audible) + expect(player.currentMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("fr")) + + player.advanceToNextItem() + expect(player.currentMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("fr")) + } + + func testPreferredLegibleLanguageIsPreservedBetweenItems() { + let player = Player(items: [ + .simple(url: Stream.onDemandWithOptions.url), + .simple(url: Stream.onDemandWithOptions.url) + ]) + player.setMediaSelection(preferredLanguages: ["fr"], for: .legible) + expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("fr")) + + player.advanceToNextItem() + expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("fr")) + } + + func testPreferredLegibleLanguageAcrossItems() { + let player = Player(items: [ + .simple(url: Stream.onDemandWithOptions.url), + .simple(url: Stream.onDemandWithManyLegibleAndAudibleOptions.url) + ]) + + player.setMediaSelection(preferredLanguages: ["en"], for: .legible) + expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("en")) + + player.advanceToNextItem() + expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("en")) + + player.setMediaSelection(preferredLanguages: ["it"], for: .legible) + expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("it")) + + player.returnToPrevious() + expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("en")) + } + + func testSelectLegibleOffOptionWithPreferredLanguage() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + + player.setMediaSelection(preferredLanguages: ["en"], for: .legible) + expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("en")) + + player.select(mediaOption: .off, for: .legible) + expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.off)) + } + + func testSelectLegibleAutomaticOptionWithPreferredLanguage() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + + player.setMediaSelection(preferredLanguages: ["en"], for: .legible) + expect(player.currentMediaOption(for: .legible)).toEventually(haveLanguageIdentifier("en")) + + player.select(mediaOption: .automatic, for: .legible) + expect(player.selectedMediaOption(for: .legible)).toEventually(equal(.automatic)) + } + + func testMediaSelectionReset() { + let player = Player(item: .simple(url: Stream.onDemandWithOptions.url)) + player.setMediaSelection(preferredLanguages: ["fr"], for: .audible) + expect(player.currentMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("fr")) + player.setMediaSelection(preferredLanguages: [], for: .audible) + expect(player.currentMediaOption(for: .audible)).toEventually(haveLanguageIdentifier("en")) + } +} diff --git a/Tests/PlayerTests/Metrics/AccessLogEventTests.swift b/Tests/PlayerTests/Metrics/AccessLogEventTests.swift new file mode 100644 index 00000000..85730db5 --- /dev/null +++ b/Tests/PlayerTests/Metrics/AccessLogEventTests.swift @@ -0,0 +1,59 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble + +final class AccessLogEventTests: TestCase { + func testNegativeValues() { + let event = AccessLogEvent( + uri: nil, + serverAddress: nil, + playbackStartDate: nil, + playbackSessionId: nil, + playbackStartOffset: -1, + playbackType: nil, + startupTime: -1, + observedBitrateStandardDeviation: -1, + indicatedBitrate: -1, + observedBitrate: -1, + averageAudioBitrate: -1, + averageVideoBitrate: -1, + indicatedAverageBitrate: -1, + numberOfServerAddressChanges: -1, + mediaRequestsWWAN: -1, + transferDuration: -1, + numberOfBytesTransferred: -1, + numberOfMediaRequests: -1, + playbackDuration: -1, + numberOfDroppedVideoFrames: -1, + numberOfStalls: -1, + segmentsDownloadedDuration: -1, + downloadOverdue: -1, + switchBitrate: -1 + ) + expect(event.playbackStartOffset).to(beNil()) + expect(event.startupTime).to(beNil()) + expect(event.observedBitrateStandardDeviation).to(beNil()) + expect(event.indicatedBitrate).to(beNil()) + expect(event.observedBitrate).to(beNil()) + expect(event.averageAudioBitrate).to(beNil()) + expect(event.averageVideoBitrate).to(beNil()) + expect(event.indicatedAverageBitrate).to(beNil()) + expect(event.numberOfServerAddressChanges).to(equal(0)) + expect(event.mediaRequestsWWAN).to(equal(0)) + expect(event.transferDuration).to(equal(0)) + expect(event.numberOfBytesTransferred).to(equal(0)) + expect(event.numberOfMediaRequests).to(equal(0)) + expect(event.playbackDuration).to(equal(0)) + expect(event.numberOfDroppedVideoFrames).to(equal(0)) + expect(event.numberOfStalls).to(equal(0)) + expect(event.segmentsDownloadedDuration).to(equal(0)) + expect(event.downloadOverdue).to(equal(0)) + expect(event.switchBitrate).to(equal(0)) + } +} diff --git a/Tests/PlayerTests/Metrics/MetricsCollectorEventsTests.swift b/Tests/PlayerTests/Metrics/MetricsCollectorEventsTests.swift new file mode 100644 index 00000000..5b0d2b4c --- /dev/null +++ b/Tests/PlayerTests/Metrics/MetricsCollectorEventsTests.swift @@ -0,0 +1,43 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble +import PillarboxStreams + +final class MetricsCollectorEventsTests: TestCase { + func testUnbound() { + let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) + expect(metricsCollector.metricEvents).toAlways(beEmpty(), until: .milliseconds(500)) + } + + func testEmptyPlayer() { + let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) + metricsCollector.player = Player() + expect(metricsCollector.metricEvents).toAlways(beEmpty(), until: .milliseconds(500)) + } + + func testPausedPlayer() { + let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) + let player = Player(item: .simple(url: Stream.onDemand.url)) + metricsCollector.player = player + expect(metricsCollector.metricEvents).toEventuallyNot(beEmpty()) + } + + func testPlayerSetToNil() { + let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + metricsCollector.player = player + player.play() + expect(metricsCollector.metricEvents).toEventuallyNot(beEmpty()) + + metricsCollector.player = nil + expect(metricsCollector.metricEvents).to(beEmpty()) + } +} diff --git a/Tests/PlayerTests/Metrics/MetricsCollectorTests.swift b/Tests/PlayerTests/Metrics/MetricsCollectorTests.swift new file mode 100644 index 00000000..b3ffa351 --- /dev/null +++ b/Tests/PlayerTests/Metrics/MetricsCollectorTests.swift @@ -0,0 +1,78 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Combine +import CoreMedia +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class MetricsCollectorTests: TestCase { + func testUnbound() { + let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) + expectAtLeastEqualPublished( + values: [[]], + from: metricsCollector.$metrics + .map { $0.compactMap(\.uri) } + .removeDuplicates() + ) + } + + func testEmptyPlayer() { + let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) + expectAtLeastEqualPublished( + values: [[]], + from: metricsCollector.$metrics + .map { $0.compactMap(\.uri) } + .removeDuplicates() + ) { + metricsCollector.player = Player() + } + } + + func testPausedPlayer() { + let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expectAtLeastEqualPublished( + values: [[]], + from: metricsCollector.$metrics + .map { $0.compactMap(\.uri) } + .removeDuplicates() + ) { + metricsCollector.player = player + } + } + + func testPlayback() { + let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expectAtLeastEqualPublished( + values: [[], [Stream.onDemand.url.absoluteString]], + from: metricsCollector.$metrics + .map { $0.compactMap(\.uri) } + .removeDuplicates() + ) { + metricsCollector.player = player + player.play() + } + } + + func testPlayerSetToNil() { + let metricsCollector = MetricsCollector(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + metricsCollector.player = player + player.play() + expect(metricsCollector.metrics).toEventuallyNot(beEmpty()) + + metricsCollector.player = nil + expect(metricsCollector.metrics).to(beEmpty()) + } +} diff --git a/Tests/PlayerTests/Metrics/MetricsStateTests.swift b/Tests/PlayerTests/Metrics/MetricsStateTests.swift new file mode 100644 index 00000000..8639a8ba --- /dev/null +++ b/Tests/PlayerTests/Metrics/MetricsStateTests.swift @@ -0,0 +1,115 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble + +final class MetricsStateTests: TestCase { + // swiftlint:disable:next function_body_length + func testMetrics() { + let state = MetricsState(with: [ + .init( + uri: "uri", + serverAddress: "serverAddress", + playbackStartDate: Date(timeIntervalSince1970: 1), + playbackSessionId: "playbackSessionId", + playbackStartOffset: 2, + playbackType: "playbackType", + startupTime: 3, + observedBitrateStandardDeviation: 4, + indicatedBitrate: 5, + observedBitrate: 6, + averageAudioBitrate: 7, + averageVideoBitrate: 8, + indicatedAverageBitrate: 9, + numberOfServerAddressChanges: 10, + mediaRequestsWWAN: 11, + transferDuration: 12, + numberOfBytesTransferred: 13, + numberOfMediaRequests: 14, + playbackDuration: 15, + numberOfDroppedVideoFrames: 16, + numberOfStalls: 17, + segmentsDownloadedDuration: 18, + downloadOverdue: 19, + switchBitrate: 20 + ) + ], at: .init(value: 12, timescale: 1)) + + let metrics = state.metrics(from: .empty) + expect(metrics.playbackStartDate).to(equal(Date(timeIntervalSince1970: 1))) + expect(metrics.time).to(equal(.init(value: 12, timescale: 1))) + expect(metrics.uri).to(equal("uri")) + expect(metrics.serverAddress).to(equal("serverAddress")) + expect(metrics.playbackSessionId).to(equal("playbackSessionId")) + expect(metrics.playbackStartOffset).to(equal(2)) + expect(metrics.playbackType).to(equal("playbackType")) + expect(metrics.startupTime).to(equal(3)) + expect(metrics.observedBitrateStandardDeviation).to(equal(4)) + expect(metrics.indicatedBitrate).to(equal(5)) + expect(metrics.observedBitrate).to(equal(6)) + expect(metrics.averageAudioBitrate).to(equal(7)) + expect(metrics.averageVideoBitrate).to(equal(8)) + expect(metrics.indicatedAverageBitrate).to(equal(9)) + + expect(metrics.increment.numberOfServerAddressChanges).to(equal(10)) + expect(metrics.increment.mediaRequestsWWAN).to(equal(11)) + expect(metrics.increment.transferDuration).to(equal(12)) + expect(metrics.increment.numberOfBytesTransferred).to(equal(13)) + expect(metrics.increment.numberOfMediaRequests).to(equal(14)) + expect(metrics.increment.playbackDuration).to(equal(15)) + expect(metrics.increment.numberOfDroppedVideoFrames).to(equal(16)) + expect(metrics.increment.numberOfStalls).to(equal(17)) + expect(metrics.increment.segmentsDownloadedDuration).to(equal(18)) + expect(metrics.increment.downloadOverdue).to(equal(19)) + expect(metrics.increment.switchBitrate).to(equal(20)) + + expect(metrics.total.numberOfServerAddressChanges).to(equal(10)) + expect(metrics.total.mediaRequestsWWAN).to(equal(11)) + expect(metrics.total.transferDuration).to(equal(12)) + expect(metrics.total.numberOfBytesTransferred).to(equal(13)) + expect(metrics.total.numberOfMediaRequests).to(equal(14)) + expect(metrics.total.playbackDuration).to(equal(15)) + expect(metrics.total.numberOfDroppedVideoFrames).to(equal(16)) + expect(metrics.total.numberOfStalls).to(equal(17)) + expect(metrics.total.segmentsDownloadedDuration).to(equal(18)) + expect(metrics.total.downloadOverdue).to(equal(19)) + expect(metrics.total.switchBitrate).to(equal(20)) + } +} + +private extension AccessLogEvent { + init(numberOfStalls: Int = -1) { + self.init( + uri: nil, + serverAddress: nil, + playbackStartDate: nil, + playbackSessionId: nil, + playbackStartOffset: -1, + playbackType: nil, + startupTime: -1, + observedBitrateStandardDeviation: -1, + indicatedBitrate: -1, + observedBitrate: -1, + averageAudioBitrate: -1, + averageVideoBitrate: -1, + indicatedAverageBitrate: -1, + numberOfServerAddressChanges: -1, + mediaRequestsWWAN: -1, + transferDuration: -1, + numberOfBytesTransferred: -1, + numberOfMediaRequests: -1, + playbackDuration: -1, + numberOfDroppedVideoFrames: -1, + numberOfStalls: numberOfStalls, + segmentsDownloadedDuration: -1, + downloadOverdue: -1, + switchBitrate: -1 + ) + } +} diff --git a/Tests/PlayerTests/Player/BlockedTimeRangeTests.swift b/Tests/PlayerTests/Player/BlockedTimeRangeTests.swift new file mode 100644 index 00000000..460410b7 --- /dev/null +++ b/Tests/PlayerTests/Player/BlockedTimeRangeTests.swift @@ -0,0 +1,80 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble +import PillarboxStreams + +private let kBlockedTimeRange = CMTimeRange(start: .init(value: 20, timescale: 1), end: .init(value: 60, timescale: 1)) +private let kOverlappingBlockedTimeRange = CMTimeRange(start: .init(value: 50, timescale: 1), end: .init(value: 100, timescale: 1)) +private let kNestedBlockedTimeRange = CMTimeRange(start: .init(value: 30, timescale: 1), end: .init(value: 50, timescale: 1)) + +private struct MetadataWithBlockedTimeRange: AssetMetadata { + var playerMetadata: PlayerMetadata { + .init(timeRanges: [ + .init(kind: .blocked, start: kBlockedTimeRange.start, end: kBlockedTimeRange.end) + ]) + } +} + +private struct MetadataWithOverlappingBlockedTimeRanges: AssetMetadata { + var playerMetadata: PlayerMetadata { + .init(timeRanges: [ + .init(kind: .blocked, start: kBlockedTimeRange.start, end: kBlockedTimeRange.end), + .init(kind: .blocked, start: kOverlappingBlockedTimeRange.start, end: kOverlappingBlockedTimeRange.end) + ]) + } +} + +private struct MetadataWithNestedBlockedTimeRanges: AssetMetadata { + var playerMetadata: PlayerMetadata { + .init(timeRanges: [ + .init(kind: .blocked, start: kBlockedTimeRange.start, end: kBlockedTimeRange.end), + .init(kind: .blocked, start: kNestedBlockedTimeRange.start, end: kNestedBlockedTimeRange.end) + ]) + } +} + +final class BlockedTimeRangeTests: TestCase { + func testSeekInBlockedTimeRange() { + let player = Player(item: .simple(url: Stream.onDemand.url, metadata: MetadataWithBlockedTimeRange())) + expect(player.streamType).toEventually(equal(.onDemand)) + player.seek(at(.init(value: 30, timescale: 1))) + expect(kBlockedTimeRange.containsTime(player.time())).toNever(beTrue(), until: .seconds(2)) + expect(player.time()).to(equal(kBlockedTimeRange.end)) + } + + func testSeekInOverlappingBlockedTimeRange() { + let player = Player(item: .simple(url: Stream.onDemand.url, metadata: MetadataWithOverlappingBlockedTimeRanges())) + expect(player.streamType).toEventually(equal(.onDemand)) + player.seek(at(.init(value: 30, timescale: 1))) + expect(kOverlappingBlockedTimeRange.containsTime(player.time())).toNever(beTrue(), until: .seconds(2)) + expect(player.time()).to(equal(kOverlappingBlockedTimeRange.end)) + } + + func testSeekInNestedBlockedTimeRange() { + let player = Player(item: .simple(url: Stream.onDemand.url, metadata: MetadataWithNestedBlockedTimeRanges())) + expect(player.streamType).toEventually(equal(.onDemand)) + player.seek(at(.init(value: 40, timescale: 1))) + expect(kNestedBlockedTimeRange.containsTime(player.time())).toNever(beTrue(), until: .seconds(2)) + expect(player.time()).to(equal(kBlockedTimeRange.end)) + } + + func testBlockedTimeRangeTraversal() { + let configuration = PlayerItemConfiguration(position: at(.init(value: 29, timescale: 1))) + let player = Player(item: .simple(url: Stream.onDemand.url, metadata: MetadataWithBlockedTimeRange(), configuration: configuration)) + player.play() + expect(player.time()).toEventually(beGreaterThan(kBlockedTimeRange.end)) + } + + func testOnDemandStartInBlockedTimeRange() { + let configuration = PlayerItemConfiguration(position: at(.init(value: 30, timescale: 1))) + let player = Player(item: .simple(url: Stream.onDemand.url, metadata: MetadataWithBlockedTimeRange(), configuration: configuration)) + expect(player.time()).toEventually(equal(kBlockedTimeRange.end)) + } +} diff --git a/Tests/PlayerTests/Player/ErrorTests.swift b/Tests/PlayerTests/Player/ErrorTests.swift index 5e3d4c0f..f42096e9 100644 --- a/Tests/PlayerTests/Player/ErrorTests.swift +++ b/Tests/PlayerTests/Player/ErrorTests.swift @@ -11,10 +11,40 @@ import Foundation import Nimble import PillarboxCircumspect import PillarboxStreams -import XCTest -final class ErrorTests: XCTestCase { - func testTruth() { - expect(true).to(beTrue()) +final class ErrorTests: TestCase { + private static func errorCodePublisher(for player: Player) -> AnyPublisher { + player.$error + .map { error in + guard let error else { return nil } + return .init(rawValue: (error as NSError).code) + } + .eraseToAnyPublisher() + } + + func testNoStream() { + let player = Player() + expectNothingPublishedNext(from: player.$error, during: .milliseconds(500)) + } + + func testValidStream() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expectNothingPublishedNext(from: player.$error, during: .milliseconds(500)) + } + + func testInvalidStream() { + let player = Player(item: .simple(url: Stream.unavailable.url)) + expectEqualPublishedNext( + values: [.init(rawValue: NSURLErrorFileDoesNotExist)], + from: Self.errorCodePublisher(for: player), + during: .seconds(1) + ) + } + + func testReset() { + let player = Player(item: .simple(url: Stream.unavailable.url)) + expect(player.error).toEventuallyNot(beNil()) + player.removeAllItems() + expect(player.error).toEventually(beNil()) } } diff --git a/Tests/PlayerTests/Player/PlaybackSpeedUpdateTests.swift b/Tests/PlayerTests/Player/PlaybackSpeedUpdateTests.swift new file mode 100644 index 00000000..c749f3c7 --- /dev/null +++ b/Tests/PlayerTests/Player/PlaybackSpeedUpdateTests.swift @@ -0,0 +1,39 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble + +final class PlaybackSpeedUpdateTests: TestCase { + func testUpdateIndefiniteWithValue() { + let speed = PlaybackSpeed.indefinite + let updatedSpeed = speed.updated(with: .value(2)) + expect(updatedSpeed.value).to(equal(2)) + expect(updatedSpeed.range).to(beNil()) + } + + func testUpdateIndefiniteWithRange() { + let speed = PlaybackSpeed.indefinite + let updatedSpeed = speed.updated(with: .range(0...2)) + expect(updatedSpeed.value).to(equal(1)) + expect(updatedSpeed.range).to(equal(0...2)) + } + + func testUpdateDefiniteWithSameRange() { + let speed = PlaybackSpeed(value: 2, range: 0...2) + let updatedSpeed = speed.updated(with: .range(0...2)) + expect(updatedSpeed.value).to(equal(2)) + expect(updatedSpeed.range).to(equal(0...2)) + } + + func testUpdateDefiniteWithIndefiniteRange() { + let speed = PlaybackSpeed(value: 2, range: 0...2) + let updatedSpeed = speed.updated(with: .range(nil)) + expect(updatedSpeed.value).to(equal(1)) + expect(updatedSpeed.range).to(beNil()) + } +} diff --git a/Tests/PlayerTests/Player/PlaybackTests.swift b/Tests/PlayerTests/Player/PlaybackTests.swift new file mode 100644 index 00000000..e1f46b28 --- /dev/null +++ b/Tests/PlayerTests/Player/PlaybackTests.swift @@ -0,0 +1,48 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Combine +import PillarboxCircumspect +import PillarboxStreams +import XCTest + +final class PlaybackTests: XCTestCase { + private func playbackStatePublisher(for player: Player) -> AnyPublisher { + player.propertiesPublisher + .slice(at: \.playbackState) + .eraseToAnyPublisher() + } + + func testHLS() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expectAtLeastEqualPublished( + values: [.idle, .paused], + from: playbackStatePublisher(for: player) + ) + } + + func testMP3() { + let item = PlayerItem.simple(url: Stream.mp3.url) + let player = Player(item: item) + expectAtLeastEqualPublished( + values: [.idle, .paused], + from: playbackStatePublisher(for: player) + ) + } + + func testUnknown() { + let item = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(item: item) + expectEqualPublished( + values: [.idle], + from: playbackStatePublisher(for: player), + during: .seconds(1) + ) + } +} diff --git a/Tests/PlayerTests/Player/PlayerTests.swift b/Tests/PlayerTests/Player/PlayerTests.swift new file mode 100644 index 00000000..dd51a251 --- /dev/null +++ b/Tests/PlayerTests/Player/PlayerTests.swift @@ -0,0 +1,76 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class PlayerTests: TestCase { + func testDeallocation() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + var player: Player? = Player(item: item) + + weak var weakPlayer = player + autoreleasepool { + player = nil + } + expect(weakPlayer).to(beNil()) + } + + func testTimesWhenEmpty() { + let player = Player() + expect(player.time()).toAlways(equal(.invalid), until: .seconds(1)) + } + + func testTimesInEmptyRange() { + let player = Player(item: .simple(url: Stream.live.url)) + expect(player.seekableTimeRange).toEventuallyNot(equal(.invalid)) + player.play() + expect(player.time()).toNever(equal(.invalid), until: .seconds(1)) + } + + func testMetadataUpdatesMustNotChangePlayerItem() { + let player = Player(item: .mock(url: Stream.onDemand.url, withMetadataUpdateAfter: 1)) + expect(player.queuePlayer.currentItem?.url).toEventually(equal(Stream.onDemand.url)) + let currentItem = player.queuePlayer.currentItem + expect(player.queuePlayer.currentItem).toAlways(equal(currentItem), until: .seconds(2)) + } + + func testRetrieveCurrentValueOnSubscription() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.properties.isBuffering).toEventually(beFalse()) + expectEqualPublished( + values: [false], + from: player.propertiesPublisher.slice(at: \.isBuffering), + during: .seconds(1) + ) + } + + func testPreloadedItems() { + let player = Player( + items: [ + .simple(url: Stream.onDemand.url), + .simple(url: Stream.onDemand.url), + .simple(url: Stream.onDemand.url) + ] + ) + let expectedResources: [Resource] = [ + .simple(url: Stream.onDemand.url), + .simple(url: Stream.onDemand.url), + .loading + ] + expect(player.items.map(\.content.resource)).toEventually(beSimilarTo(expectedResources)) + expect(player.items.map(\.content.resource)).toAlways(beSimilarTo(expectedResources), until: .seconds(1)) + } + + func testNoMetricsWhenFailed() { + let player = Player(item: .failing(loadedAfter: 0.1)) + expect(player.properties.metrics()).toAlways(beNil(), until: .seconds(1)) + } +} diff --git a/Tests/PlayerTests/Player/QueueTests.swift b/Tests/PlayerTests/Player/QueueTests.swift new file mode 100644 index 00000000..a6806eed --- /dev/null +++ b/Tests/PlayerTests/Player/QueueTests.swift @@ -0,0 +1,184 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class QueueTests: TestCase { + func testWhenEmpty() { + let player = Player() + expect(player.urls).to(beEmpty()) + expect(player.currentItem).to(beNil()) + } + + func testPlayableItem() { + let item = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(item: item) + expect(player.urls).toEventually(equal([ + Stream.shortOnDemand.url + ])) + expect(player.currentItem).to(equal(item)) + } + + func testEntirePlayback() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + player.play() + expect(player.urls).toEventually(beEmpty()) + expect(player.currentItem).to(beNil()) + } + + func testFailingUnavailableItem() { + let item = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(item: item) + // Item is consumed by `AVQueuePlayer` for some reason. + expect(player.urls).toEventually(beEmpty()) + expect(player.currentItem).to(equal(item)) + } + + func testFailingUnauthorizedItem() { + let item = PlayerItem.simple(url: Stream.unauthorized.url) + let player = Player(item: item) + expect(player.urls).toEventually(equal([ + Stream.unauthorized.url + ])) + expect(player.currentItem).to(equal(item)) + } + + func testFailingMp3Item() { + let item = PlayerItem.simple(url: Stream.unavailableMp3.url) + let player = Player(item: item) + expect(player.urls).toEventually(equal([ + Stream.unavailableMp3.url + ])) + expect(player.currentItem).to(equal(item)) + } + + func testBetweenPlayableItems() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + player.play() + + expect(player.urls).toEventually(equal([ + Stream.shortOnDemand.url, + Stream.onDemand.url + ])) + expect(player.currentItem).to(equal(item1)) + + expect(player.urls).toEventually(equal([ + Stream.onDemand.url + ])) + expect(player.currentItem).to(equal(item2)) + } + + func testFailingUnavailableItemFollowedByPlayableItem() { + let item1 = PlayerItem.simple(url: Stream.unavailable.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + // Item is consumed by `AVQueuePlayer` for some reason. + expect(player.urls).toEventually(beEmpty()) + expect(player.currentItem).to(equal(item1)) + } + + func testFailingUnauthorizedItemFollowedByPlayableItem() { + let item1 = PlayerItem.simple(url: Stream.unauthorized.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.urls).toEventually(equal([ + Stream.unauthorized.url + ])) + expect(player.currentItem).to(equal(item1)) + } + + func testFailingMp3ItemFollowedByPlayableItem() { + let item1 = PlayerItem.simple(url: Stream.unavailableMp3.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.urls).toEventually(equal([ + Stream.unavailableMp3.url + ])) + expect(player.currentItem).to(equal(item1)) + } + + func testFailingItemUnavailableBetweenPlayableItems() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.unavailable.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.play() + expect(player.urls).toEventually(beEmpty()) + expect(player.currentItem).to(equal(item2)) + } + + func testFailingMp3ItemBetweenPlayableItems() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.unavailableMp3.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.play() + expect(player.urls).toEventually(beEmpty()) + expect(player.currentItem).to(equal(item2)) + } + + func testPlayableItemReplacingFailingUnavailableItem() { + let player = Player(item: .simple(url: Stream.unavailable.url)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + player.items = [item] + expect(player.urls).toEventually(equal([ + Stream.onDemand.url + ])) + expect(player.currentItem).to(equal(item)) + } + + func testPlayableItemReplacingFailingUnauthorizedItem() { + let player = Player(item: .simple(url: Stream.unauthorized.url)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + player.items = [item] + expect(player.urls).toEventually(equal([ + Stream.onDemand.url + ])) + expect(player.currentItem).to(equal(item)) + } + + func testPlayableItemReplacingFailingMp3Item() { + let player = Player(item: .simple(url: Stream.unavailableMp3.url)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + player.items = [item] + expect(player.urls).toEventually(equal([ + Stream.onDemand.url + ])) + expect(player.currentItem).to(equal(item)) + } + + func testReplaceCurrentItem() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + player.items = [item] + expect(player.urls).toEventually(equal([ + Stream.onDemand.url + ])) + expect(player.currentItem).to(equal(item)) + } + + func testRemoveCurrentItemFollowedByPlayableItem() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + player.remove(player.items.first!) + expect(player.urls).toEventually(equal([ + Stream.onDemand.url + ])) + expect(player.currentItem).to(equal(item2)) + } + + func testRemoveAllItems() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + player.removeAllItems() + expect(player.urls).to(beEmpty()) + } +} diff --git a/Tests/PlayerTests/Player/ReplayChecksTests.swift b/Tests/PlayerTests/Player/ReplayChecksTests.swift new file mode 100644 index 00000000..6a6f3f73 --- /dev/null +++ b/Tests/PlayerTests/Player/ReplayChecksTests.swift @@ -0,0 +1,76 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class ReplayChecksTests: TestCase { + func testEmptyPlayer() { + let player = Player() + expect(player.canReplay()).to(beFalse()) + } + + func testWithOneGoodItem() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + expect(player.canReplay()).to(beFalse()) + } + + func testWithOneGoodItemPlayedEntirely() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + player.play() + expect(player.canReplay()).toEventually(beTrue()) + } + + func testWithOneBadItemConsumed() { + // This item is consumed by the player when failing. + let player = Player(item: .simple(url: Stream.unavailable.url)) + expect(player.canReplay()).toEventually(beTrue()) + } + + func testWithOneBadItemNotConsumed() { + // This item is not consumed by the player when failing (for an unknown reason). + let player = Player(item: .simple(url: Stream.unauthorized.url)) + expect(player.canReplay()).toEventually(beTrue()) + } + + func testWithManyGoodItems() { + let player = Player(items: [ + .simple(url: Stream.shortOnDemand.url), + .simple(url: Stream.shortOnDemand.url) + ]) + player.play() + expect(player.canReplay()).toEventually(beTrue()) + } + + func testWithManyBadItems() { + let player = Player(items: [ + .simple(url: Stream.unavailable.url), + .simple(url: Stream.unavailable.url) + ]) + player.play() + expect(player.canReplay()).toEventually(beTrue()) + } + + func testWithOneGoodItemAndOneBadItem() { + let player = Player(items: [ + .simple(url: Stream.shortOnDemand.url), + .simple(url: Stream.unavailable.url) + ]) + player.play() + expect(player.canReplay()).toEventually(beTrue()) + } + + func testWithOneLongGoodItemAndOneBadItem() { + let player = Player(items: [ + .simple(url: Stream.onDemand.url), + .simple(url: Stream.unavailable.url) + ]) + player.play() + expect(player.canReplay()).toNever(beTrue(), until: .milliseconds(500)) + } +} diff --git a/Tests/PlayerTests/Player/ReplayTests.swift b/Tests/PlayerTests/Player/ReplayTests.swift new file mode 100644 index 00000000..1553af19 --- /dev/null +++ b/Tests/PlayerTests/Player/ReplayTests.swift @@ -0,0 +1,77 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class ReplayTests: TestCase { + func testWithOneGoodItem() { + let item = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(item: item) + player.replay() + expect(player.currentItem).to(equal(item)) + } + + func testWithOneGoodItemPlayedEntirely() { + let item = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(item: item) + player.play() + expect(player.currentItem).toEventually(beNil()) + player.replay() + expect(player.currentItem).toEventually(equal(item)) + } + + func testWithOneBadItem() { + let item = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(item: item) + expect(player.currentItem).toAlways(equal(item), until: .milliseconds(500)) + player.replay() + expect(player.currentItem).toAlways(equal(item), until: .milliseconds(500)) + } + + func testWithManyGoodItems() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(items: [item1, item2]) + player.play() + expect(player.currentItem).toEventually(equal(item2)) + player.replay() + expect(player.currentItem).to(equal(item2)) + } + + func testWithManyBadItems() { + let item1 = PlayerItem.simple(url: Stream.unavailable.url) + let item2 = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(items: [item1, item2]) + player.play() + expect(player.currentItem).toAlways(equal(item1), until: .milliseconds(500)) + player.replay() + expect(player.currentItem).toAlways(equal(item1), until: .milliseconds(500)) + } + + func testWithOneGoodItemAndOneBadItem() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(items: [item1, item2]) + player.play() + expect(player.currentItem).toEventually(equal(item2)) + player.replay() + expect(player.currentItem).to(equal(item2)) + } + + func testResumePlaybackIfNeeded() { + let item = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(item: item) + player.play() + expect(player.currentItem).toEventually(beNil()) + player.pause() + player.replay() + expect(player.currentItem).toEventually(equal(item)) + expect(player.playbackState).toEventually(equal(.playing)) + } +} diff --git a/Tests/PlayerTests/Player/SeekChecksTests.swift b/Tests/PlayerTests/Player/SeekChecksTests.swift new file mode 100644 index 00000000..dccc164a --- /dev/null +++ b/Tests/PlayerTests/Player/SeekChecksTests.swift @@ -0,0 +1,54 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble +import PillarboxStreams + +final class SeekChecksTests: TestCase { + func testCannotSeekWithEmptyPlayer() { + let player = Player() + expect(player.canSeek(to: .zero)).to(beFalse()) + } + + func testCanSeekInTimeRange() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.canSeek(to: CMTimeMultiplyByFloat64(Stream.onDemand.duration, multiplier: 0.5))).to(beTrue()) + } + + func testCannotSeekInEmptyTimeRange() { + let player = Player(item: .simple(url: Stream.live.url)) + expect(player.streamType).toEventually(equal(.live)) + expect(player.canSeek(to: .zero)).to(beFalse()) + } + + func testCanSeekToTimeRangeStart() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.canSeek(to: player.seekableTimeRange.start)).to(beTrue()) + } + + func testCanSeekToTimeRangeEnd() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.canSeek(to: player.seekableTimeRange.end)).to(beTrue()) + } + + func testCannotSeekBeforeTimeRangeStart() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.canSeek(to: CMTime(value: -10, timescale: 1))).to(beFalse()) + } + + func testCannotSeekAfterTimeRangeEnd() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.canSeek(to: player.seekableTimeRange.end + CMTime(value: 1, timescale: 1))).to(beFalse()) + } +} diff --git a/Tests/PlayerTests/Player/SeekTests.swift b/Tests/PlayerTests/Player/SeekTests.swift new file mode 100644 index 00000000..1e2d04e5 --- /dev/null +++ b/Tests/PlayerTests/Player/SeekTests.swift @@ -0,0 +1,120 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +private struct MockMetadata: AssetMetadata { + var playerMetadata: PlayerMetadata { + .init(timeRanges: [ + .init(kind: .blocked, start: .init(value: 20, timescale: 1), end: .init(value: 60, timescale: 1)) + ]) + } +} + +final class SeekTests: TestCase { + func testSeekWhenEmpty() { + let player = Player() + waitUntil { done in + player.seek(near(.zero)) { finished in + expect(finished).to(beTrue()) + done() + } + } + } + + func testSeekInTimeRange() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + waitUntil { done in + player.seek(near(CMTimeMultiplyByFloat64(Stream.onDemand.duration, multiplier: 0.5))) { finished in + expect(finished).to(beTrue()) + done() + } + } + } + + func testSeekInEmptyTimeRange() { + let player = Player(item: .simple(url: Stream.live.url)) + expect(player.streamType).toEventually(equal(.live)) + waitUntil { done in + player.seek(near(.zero)) { finished in + expect(finished).to(beTrue()) + done() + } + } + } + + func testSeekToTimeRangeStart() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + waitUntil { done in + player.seek(near(player.seekableTimeRange.start)) { finished in + expect(finished).to(beTrue()) + done() + } + } + } + + func testSeekToTimeRangeEnd() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + waitUntil { done in + player.seek(near(player.seekableTimeRange.end)) { finished in + expect(finished).to(beTrue()) + done() + } + } + } + + func testSeekBeforeTimeRangeStart() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + waitUntil { done in + player.seek(near(CMTime(value: -10, timescale: 1))) { finished in + expect(finished).to(beTrue()) + expect(player.time()).to(equal(.zero)) + done() + } + } + } + + func testSeekAfterTimeRangeEnd() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + waitUntil { done in + player.seek(near(player.seekableTimeRange.end + CMTime(value: 10, timescale: 1))) { finished in + expect(finished).to(beTrue()) + expect(player.time()).to(equal(player.seekableTimeRange.end, by: beClose(within: 1))) + done() + } + } + } + + func testTimesDuringSeekBeforeTimeRangeStart() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + player.play() + player.seek(near(CMTime(value: -10, timescale: 1))) + expect(player.time()).toAlways(beGreaterThanOrEqualTo(player.seekableTimeRange.start), until: .seconds(1)) + } + + func testOnDemandStartAtTime() { + let configuration = PlayerItemConfiguration(position: at(.init(value: 10, timescale: 1))) + let player = Player(item: .simple(url: Stream.onDemand.url, configuration: configuration)) + expect(player.time().seconds).toEventually(equal(10)) + } + + func testDvrStartAtTime() { + let configuration = PlayerItemConfiguration(position: at(.init(value: 10, timescale: 1))) + let player = Player(item: .simple(url: Stream.dvr.url, configuration: configuration)) + expect(player.time().seconds).toEventually(equal(10)) + } +} diff --git a/Tests/PlayerTests/Player/SpeedTests.swift b/Tests/PlayerTests/Player/SpeedTests.swift new file mode 100644 index 00000000..d70a1421 --- /dev/null +++ b/Tests/PlayerTests/Player/SpeedTests.swift @@ -0,0 +1,205 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class SpeedTests: TestCase { + func testEmpty() { + let player = Player() + expect(player.effectivePlaybackSpeed).toAlways(equal(1), until: .seconds(2)) + expect(player.playbackSpeedRange).toAlways(equal(1...1), until: .seconds(2)) + } + + func testNoSpeedUpdateWhenEmpty() { + let player = Player() + player.setDesiredPlaybackSpeed(2) + expect(player.effectivePlaybackSpeed).toAlways(equal(1), until: .seconds(2)) + expect(player.playbackSpeedRange).toAlways(equal(1...1), until: .seconds(2)) + } + + func testOnDemand() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + player.setDesiredPlaybackSpeed(2) + expect(player.effectivePlaybackSpeed).toEventually(equal(2)) + expect(player.playbackSpeedRange).toEventually(equal(0.1...2)) + } + + func testDvr() { + let player = Player(item: .simple(url: Stream.dvr.url)) + player.setDesiredPlaybackSpeed(0.5) + expect(player.effectivePlaybackSpeed).toEventually(equal(0.5)) + expect(player.playbackSpeedRange).toEventually(equal(0.1...1)) + } + + func testLive() { + let player = Player(item: .simple(url: Stream.live.url)) + player.setDesiredPlaybackSpeed(2) + expect(player.effectivePlaybackSpeed).toAlways(equal(1), until: .seconds(2)) + expect(player.playbackSpeedRange).toAlways(equal(1...1), until: .seconds(2)) + } + + func testDvrInThePast() { + let player = Player(item: .simple(url: Stream.dvr.url)) + expect(player.seekableTimeRange).toEventuallyNot(equal(.invalid)) + waitUntil { done in + player.seek(at(.init(value: 1, timescale: 1))) { _ in + done() + } + } + + expect(player.playbackSpeedRange).toEventually(equal(0.1...2)) + player.setDesiredPlaybackSpeed(2) + expect(player.effectivePlaybackSpeed).toEventually(equal(2)) + } + + func testPlaylistOnDemandToLive() { + let item1 = PlayerItem(asset: .simple(url: Stream.onDemand.url)) + let item2 = PlayerItem(asset: .simple(url: Stream.live.url)) + let player = Player(items: [item1, item2]) + + player.setDesiredPlaybackSpeed(2) + expect(player.effectivePlaybackSpeed).toEventually(equal(2)) + + player.advanceToNextItem() + expect(player.effectivePlaybackSpeed).toEventually(equal(1)) + expect(player.playbackSpeedRange).toEventually(equal(1...1)) + } + + func testPlaylistOnDemandToOnDemand() { + let item1 = PlayerItem(asset: .simple(url: Stream.onDemand.url)) + let item2 = PlayerItem(asset: .simple(url: Stream.onDemand.url)) + let player = Player(items: [item1, item2]) + player.setDesiredPlaybackSpeed(2) + expect(player.effectivePlaybackSpeed).toEventually(equal(2)) + + player.advanceToNextItem() + expect(player.effectivePlaybackSpeed).toEventually(equal(2)) + expect(player.playbackSpeedRange).toEventually(equal(0.1...2)) + } + + func testSpeedUpdateWhenStartingPlayback() { + let player = Player(item: .simple(url: Stream.dvr.url)) + expectAtLeastEqualPublished( + values: [1, 0.5], + from: player.changePublisher(at: \.effectivePlaybackSpeed).removeDuplicates() + ) { + player.setDesiredPlaybackSpeed(0.5) + } + } + + func testSpeedRangeUpdateWhenStartingPlayback() { + let player = Player(item: .simple(url: Stream.dvr.url)) + expectAtLeastEqualPublished( + values: [1...1, 0.1...1], + from: player.changePublisher(at: \.playbackSpeedRange).removeDuplicates() + ) { + player.setDesiredPlaybackSpeed(0.5) + } + } + + func testSpeedUpdateWhenApproachingLiveEdge() { + let player = Player(item: .simple(url: Stream.dvr.url)) + player.play() + expect(player.seekableTimeRange).toEventuallyNot(equal(.invalid)) + waitUntil { done in + player.seek(at(.init(value: 10, timescale: 1))) { _ in + done() + } + } + + player.setDesiredPlaybackSpeed(2) + expect(player.effectivePlaybackSpeed).toEventually(equal(2)) + expect(player.playbackSpeedRange).toEventually(equal(0.1...2)) + + expect(player.effectivePlaybackSpeed).toEventually(equal(1)) + expect(player.playbackSpeedRange).toEventually(equal(0.1...1)) + } + + func testPlaylistEnd() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + player.setDesiredPlaybackSpeed(2) + player.play() + expect(player.currentItem).toEventually(beNil()) + + expect(player.effectivePlaybackSpeed).toEventually(equal(1)) + expect(player.playbackSpeedRange).toEventually(equal(1...1)) + } + + func testItemAppendMustStartAtCurrentSpeed() { + let player = Player() + player.setDesiredPlaybackSpeed(2) + player.append(.simple(url: Stream.onDemand.url)) + expect(player.effectivePlaybackSpeed).toEventually(equal(2)) + } + + func testInitialSpeedMustSetRate() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + player.setDesiredPlaybackSpeed(2) + player.play() + expect(player.queuePlayer.defaultRate).toEventually(equal(2)) + expect(player.queuePlayer.rate).toEventually(equal(2)) + } + + func testSpeedUpdateMustUpdateRate() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + player.play() + expect(player.playbackState).toEventually(equal(.playing)) + + player.setDesiredPlaybackSpeed(2) + expect(player.queuePlayer.defaultRate).toEventually(equal(2)) + expect(player.queuePlayer.rate).toEventually(equal(2)) + } + + func testSpeedUpdateWhilePausedMustUpdateRate() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.playbackState).toEventually(equal(.paused)) + + player.setDesiredPlaybackSpeed(2) + player.play() + + expect(player.queuePlayer.defaultRate).toEventually(equal(2)) + expect(player.queuePlayer.rate).toEventually(equal(2)) + } + + func testSpeedUpdateMustNotResumePlayback() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.playbackState).toEventually(equal(.paused)) + player.setDesiredPlaybackSpeed(2) + expect(player.playbackState).toAlways(equal(.paused), until: .seconds(2)) + } + + func testPlayMustNotResetSpeed() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + player.setDesiredPlaybackSpeed(2) + player.play() + expect(player.effectivePlaybackSpeed).toEventually(equal(2)) + } + + func testRateChangeMustNotUpdatePlaybackSpeedOutsideAVPlayerViewController() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + player.queuePlayer.rate = 2 + expect(player.effectivePlaybackSpeed).toAlways(equal(1), until: .seconds(2)) + } + + func testNoDesiredUpdateIsIgnored() { + let player = Player() + expectAtLeastEqualPublished(values: [ + .value(1), + .value(2), + .value(2) + ], from: player.desiredPlaybackSpeedUpdatePublisher()) { + player.setDesiredPlaybackSpeed(1) + player.setDesiredPlaybackSpeed(2) + player.setDesiredPlaybackSpeed(2) + } + } +} diff --git a/Tests/PlayerTests/Player/TextStyleRulesTests.swift b/Tests/PlayerTests/Player/TextStyleRulesTests.swift new file mode 100644 index 00000000..8507ce40 --- /dev/null +++ b/Tests/PlayerTests/Player/TextStyleRulesTests.swift @@ -0,0 +1,48 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble +import PillarboxStreams + +final class TextStyleRulesTests: TestCase { + private static let textStyleRules = [ + AVTextStyleRule(textMarkupAttributes: [ + kCMTextMarkupAttribute_ForegroundColorARGB: [1, 1, 0, 0], + kCMTextMarkupAttribute_ItalicStyle: true + ]) + ] + + func testDefaultWithEmptyPlayer() { + let player = Player() + expect(player.textStyleRules).to(beEmpty()) + } + + func testDefaultWithLoadedPlayer() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.textStyleRules).to(beEmpty()) + expect(player.queuePlayer.currentItem?.textStyleRules).to(beEmpty()) + } + + func testStyleUpdate() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + player.textStyleRules = Self.textStyleRules + expect(player.textStyleRules).to(equal(Self.textStyleRules)) + expect(player.queuePlayer.currentItem?.textStyleRules).to(equal(Self.textStyleRules)) + } + + func testStylePreservedBetweenItems() { + let player = Player(items: [ + .simple(url: Stream.shortOnDemand.url), + .simple(url: Stream.onDemand.url) + ]) + player.textStyleRules = Self.textStyleRules + player.advanceToNextItem() + expect(player.queuePlayer.currentItem?.textStyleRules).to(equal(Self.textStyleRules)) + } +} diff --git a/Tests/PlayerTests/PlayerItem/PlayerItemAssetPublisherTests.swift b/Tests/PlayerTests/PlayerItem/PlayerItemAssetPublisherTests.swift new file mode 100644 index 00000000..f56e3c92 --- /dev/null +++ b/Tests/PlayerTests/PlayerItem/PlayerItemAssetPublisherTests.swift @@ -0,0 +1,51 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import PillarboxCircumspect +import PillarboxStreams + +final class PlayerItemAssetPublisherTests: TestCase { + func testNoLoad() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + expectSimilarPublished( + values: [.loading], + from: item.$content.map(\.resource), + during: .milliseconds(500) + ) + } + + func testLoad() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + expectSimilarPublished( + values: [.loading, .simple(url: Stream.onDemand.url)], + from: item.$content.map(\.resource), + during: .milliseconds(500) + ) { + PlayerItem.load(for: item.id) + } + } + + func testReload() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + expectSimilarPublished( + values: [.loading, .simple(url: Stream.onDemand.url)], + from: item.$content.map(\.resource), + during: .milliseconds(500) + ) { + PlayerItem.load(for: item.id) + } + + expectSimilarPublishedNext( + values: [.simple(url: Stream.onDemand.url)], + from: item.$content.map(\.resource), + during: .milliseconds(500) + ) { + PlayerItem.reload(for: item.id) + } + } +} diff --git a/Tests/PlayerTests/PlayerItem/PlayerItemTests.swift b/Tests/PlayerTests/PlayerItem/PlayerItemTests.swift new file mode 100644 index 00000000..14704a8f --- /dev/null +++ b/Tests/PlayerTests/PlayerItem/PlayerItemTests.swift @@ -0,0 +1,129 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class PlayerItemTests: TestCase { + private static let limits = PlayerLimits( + preferredPeakBitRate: 100, + preferredPeakBitRateForExpensiveNetworks: 200, + preferredMaximumResolution: .init(width: 100, height: 200), + preferredMaximumResolutionForExpensiveNetworks: .init(width: 300, height: 400) + ) + + func testSimpleItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + PlayerItem.load(for: item.id) + expect(item.content.resource).toEventually(equal(.simple(url: Stream.onDemand.url))) + let playerItem = item.content.playerItem(configuration: .default, limits: .none) + expect(playerItem.preferredForwardBufferDuration).to(equal(0)) + expect(playerItem.preferredPeakBitRate).to(equal(0)) + expect(playerItem.preferredPeakBitRateForExpensiveNetworks).to(equal(0)) + expect(playerItem.preferredMaximumResolution).to(equal(.zero)) + expect(playerItem.preferredMaximumResolutionForExpensiveNetworks).to(equal(.zero)) + } + + func testSimpleItemWithConfiguration() { + let item = PlayerItem.simple(url: Stream.onDemand.url, configuration: .init(preferredForwardBufferDuration: 4)) + PlayerItem.load(for: item.id) + expect(item.content.resource).toEventually(equal(.simple(url: Stream.onDemand.url))) + expect(item.content.playerItem(configuration: .default, limits: .none).preferredForwardBufferDuration).to(equal(4)) + } + + func testSimpleItemWithLimits() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + PlayerItem.load(for: item.id) + expect(item.content.resource).toEventually(equal(.simple(url: Stream.onDemand.url))) + let playerItem = item.content.playerItem(configuration: .default, limits: Self.limits) + expect(playerItem.preferredPeakBitRate).to(equal(100)) + expect(playerItem.preferredPeakBitRateForExpensiveNetworks).to(equal(200)) + expect(playerItem.preferredMaximumResolution).to(equal(.init(width: 100, height: 200))) + expect(playerItem.preferredMaximumResolutionForExpensiveNetworks).to(equal(.init(width: 300, height: 400))) + } + + func testCustomItem() { + let delegate = ResourceLoaderDelegateMock() + let item = PlayerItem.custom(url: Stream.onDemand.url, delegate: delegate) + PlayerItem.load(for: item.id) + expect(item.content.resource).toEventually(equal(.custom(url: Stream.onDemand.url, delegate: delegate))) + let playerItem = item.content.playerItem(configuration: .default, limits: .none) + expect(playerItem.preferredForwardBufferDuration).to(equal(0)) + expect(playerItem.preferredPeakBitRate).to(equal(0)) + expect(playerItem.preferredPeakBitRateForExpensiveNetworks).to(equal(0)) + expect(playerItem.preferredMaximumResolution).to(equal(.zero)) + expect(playerItem.preferredMaximumResolutionForExpensiveNetworks).to(equal(.zero)) + } + + func testCustomItemWithConfiguration() { + let delegate = ResourceLoaderDelegateMock() + let item = PlayerItem.custom( + url: Stream.onDemand.url, + delegate: delegate, + configuration: .init(preferredForwardBufferDuration: 4) + ) + PlayerItem.load(for: item.id) + expect(item.content.resource).toEventually(equal(.custom(url: Stream.onDemand.url, delegate: delegate))) + expect(item.content.playerItem( + configuration: .default, + limits: .none + ).preferredForwardBufferDuration).to(equal(4)) + } + + func testCustomItemWithLimits() { + let delegate = ResourceLoaderDelegateMock() + let item = PlayerItem.custom(url: Stream.onDemand.url, delegate: delegate) + PlayerItem.load(for: item.id) + expect(item.content.resource).toEventually(equal(.custom(url: Stream.onDemand.url, delegate: delegate))) + let playerItem = item.content.playerItem(configuration: .default, limits: Self.limits) + expect(playerItem.preferredPeakBitRate).to(equal(100)) + expect(playerItem.preferredPeakBitRateForExpensiveNetworks).to(equal(200)) + expect(playerItem.preferredMaximumResolution).to(equal(.init(width: 100, height: 200))) + expect(playerItem.preferredMaximumResolutionForExpensiveNetworks).to(equal(.init(width: 300, height: 400))) + } + + func testEncryptedItem() { + let delegate = ContentKeySessionDelegateMock() + let item = PlayerItem.encrypted(url: Stream.onDemand.url, delegate: delegate) + PlayerItem.load(for: item.id) + expect(item.content.resource).toEventually(equal(.encrypted(url: Stream.onDemand.url, delegate: delegate))) + let playerItem = item.content.playerItem(configuration: .default, limits: .none) + expect(playerItem.preferredForwardBufferDuration).to(equal(0)) + expect(playerItem.preferredPeakBitRate).to(equal(0)) + expect(playerItem.preferredPeakBitRateForExpensiveNetworks).to(equal(0)) + expect(playerItem.preferredMaximumResolution).to(equal(.zero)) + expect(playerItem.preferredMaximumResolutionForExpensiveNetworks).to(equal(.zero)) + } + + func testEncryptedItemWithConfiguration() { + let delegate = ContentKeySessionDelegateMock() + let item = PlayerItem.encrypted( + url: Stream.onDemand.url, + delegate: delegate, + configuration: .init(preferredForwardBufferDuration: 4) + ) + PlayerItem.load(for: item.id) + expect(item.content.resource).toEventually(equal(.encrypted(url: Stream.onDemand.url, delegate: delegate))) + expect(item.content.playerItem( + configuration: .default, + limits: .none + ).preferredForwardBufferDuration).to(equal(4)) + } + + func testEncryptedItemWithNonStandardPlayerConfiguration() { + let delegate = ContentKeySessionDelegateMock() + let item = PlayerItem.encrypted(url: Stream.onDemand.url, delegate: delegate) + PlayerItem.load(for: item.id) + expect(item.content.resource).toEventually(equal(.encrypted(url: Stream.onDemand.url, delegate: delegate))) + let playerItem = item.content.playerItem(configuration: .default, limits: Self.limits) + expect(playerItem.preferredPeakBitRate).to(equal(100)) + expect(playerItem.preferredPeakBitRateForExpensiveNetworks).to(equal(200)) + expect(playerItem.preferredMaximumResolution).to(equal(.init(width: 100, height: 200))) + expect(playerItem.preferredMaximumResolutionForExpensiveNetworks).to(equal(.init(width: 300, height: 400))) + } +} diff --git a/Tests/PlayerTests/Playlist/CurrentItemTests.swift b/Tests/PlayerTests/Playlist/CurrentItemTests.swift new file mode 100644 index 00000000..72079426 --- /dev/null +++ b/Tests/PlayerTests/Playlist/CurrentItemTests.swift @@ -0,0 +1,191 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class CurrentItemTests: TestCase { + func testCurrentItem() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(items: [item1, item2]) + expectAtLeastEqualPublished( + values: [item1, item2, nil], + from: player.changePublisher(at: \.currentItem).removeDuplicates() + ) { + player.play() + } + } + + func testCurrentItemWithFirstFailedItem() { + let item1 = PlayerItem.simple(url: Stream.unavailable.url) + let item2 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(items: [item1, item2]) + expectEqualPublished( + values: [item1], + from: player.changePublisher(at: \.currentItem).removeDuplicates(), + during: .milliseconds(500) + ) { + player.play() + } + } + + func testCurrentItemWithMiddleFailedItem() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.unavailable.url) + let item3 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(items: [item1, item2, item3]) + expectEqualPublished( + values: [item1, item2], + from: player.changePublisher(at: \.currentItem).removeDuplicates(), + during: .seconds(2) + ) { + player.play() + } + } + + func testCurrentItemWithLastFailedItem() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(items: [item1, item2]) + expectAtLeastEqualPublished( + values: [item1, item2], + from: player.changePublisher(at: \.currentItem).removeDuplicates() + ) { + player.play() + } + } + + func testCurrentItemWithFirstItemRemoved() { + let item1 = PlayerItem.simple(url: Stream.unavailable.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.error).toEventuallyNot(beNil()) + player.remove(item1) + expect(player.currentItem).toAlways(equal(item2), until: .seconds(1)) + } + + func testCurrentItemWithSecondItemRemoved() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.unavailable.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + expect(player.currentItem).toEventually(equal(item2)) + expect(player.error).toEventuallyNot(beNil()) + player.remove(item2) + expect(player.currentItem).toAlways(equal(item3), until: .seconds(1)) + } + + func testCurrentItemWithFailedItem() { + let item = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(item: item) + expectEqualPublished( + values: [item], + from: player.changePublisher(at: \.currentItem).removeDuplicates(), + during: .milliseconds(500) + ) + } + + func testCurrentItemWithEmptyPlayer() { + let player = Player() + expect(player.currentItem).to(beNil()) + } + + func testSlowFirstCurrentItem() { + let item1 = PlayerItem.mock(url: Stream.shortOnDemand.url, loadedAfter: 1) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expectAtLeastEqualPublished( + values: [item1, item2], + from: player.changePublisher(at: \.currentItem).removeDuplicates() + ) { + player.play() + } + } + + func testCurrentItemAfterPlayerEnded() { + let item = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(items: [item]) + expectAtLeastEqualPublished( + values: [item, nil], + from: player.changePublisher(at: \.currentItem).removeDuplicates() + ) { + player.play() + } + } + + func testSetCurrentItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(items: [item1, item2]) + expectEqualPublished( + values: [item1, item2], + from: player.changePublisher(at: \.currentItem).removeDuplicates(), + during: .milliseconds(500) + ) { + player.currentItem = item2 + } + } + + func testSetCurrentItemUpdatePlayerCurrentItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(items: [item1, item2]) + player.currentItem = item2 + expect(player.queuePlayer.currentItem?.url).toEventually(equal(Stream.shortOnDemand.url)) + } + + func testPlayerPreloadedItemCount() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let item4 = PlayerItem.simple(url: Stream.onDemand.url) + let item5 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3, item4, item5]) + player.currentItem = item3 + + let items = player.queuePlayer.items() + expect(items).to(haveCount(player.configuration.preloadedItems)) + } + + func testSetCurrentItemWithUnknownItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item3 = PlayerItem.simple(url: Stream.mediumOnDemand.url) + let player = Player(items: [item1, item2]) + player.currentItem = item3 + expect(player.currentItem).to(equal(item3)) + expect(player.items).to(equalDiff([item3, item2])) + } + + func testSetCurrentItemToNil() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expect(player.currentItem).to(equal(item)) + player.currentItem = nil + expect(player.currentItem).to(beNil()) + expect(player.items).to(equalDiff([item])) + expect(player.queuePlayer.items()).to(beEmpty()) + } + + func testSetCurrentItemToSameItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item]) + player.play() + expect(player.time().seconds).toEventually(beGreaterThan(1)) + player.pause() + player.currentItem = item + expect(player.playbackState).toAlways(equal(.paused), until: .seconds(1)) + expect(player.currentItem).to(equal(item)) + expect(player.items).to(equalDiff([item])) + expect(player.time().seconds).to(equal(0)) + } +} diff --git a/Tests/PlayerTests/Playlist/ItemInsertionAfterTests.swift b/Tests/PlayerTests/Playlist/ItemInsertionAfterTests.swift new file mode 100644 index 00000000..7452d337 --- /dev/null +++ b/Tests/PlayerTests/Playlist/ItemInsertionAfterTests.swift @@ -0,0 +1,88 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class ItemInsertionAfterTests: TestCase { + func testInsertItemAfterNextItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) + expect(player.insert(insertedItem, after: item2)).to(beTrue()) + expect(player.items).to(equalDiff([item1, item2, insertedItem, item3])) + } + + func testInsertItemAfterCurrentItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) + expect(player.insert(insertedItem, after: item2)).to(beTrue()) + expect(player.items).to(equalDiff([item1, item2, insertedItem, item3])) + } + + func testInsertItemAfterPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + player.advanceToNextItem() + let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) + expect(player.insert(insertedItem, after: item2)).to(beTrue()) + expect(player.items).to(equalDiff([item1, item2, insertedItem, item3])) + } + + func testInsertItemAfterLastItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) + expect(player.insert(insertedItem, after: item2)).to(beTrue()) + expect(player.items).to(equalDiff([item1, item2, insertedItem])) + } + + func testInsertItemAfterIdenticalItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.insert(item1, after: item2)).to(beFalse()) + expect(player.items).to(equalDiff([item1, item2])) + } + + func testInsertItemAfterForeignItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) + let foreignItem = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item]) + expect(player.insert(insertedItem, after: foreignItem)).to(beFalse()) + expect(player.items).to(equalDiff([item])) + } + + func testInsertItemAfterNil() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item]) + let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) + expect(player.insert(insertedItem, after: nil)).to(beTrue()) + expect(player.items).to(equalDiff([item, insertedItem])) + } + + func testAppendItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item]) + let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) + expect(player.append(insertedItem)).to(beTrue()) + expect(player.items).to(equalDiff([item, insertedItem])) + } +} diff --git a/Tests/PlayerTests/Playlist/ItemInsertionBeforeTests.swift b/Tests/PlayerTests/Playlist/ItemInsertionBeforeTests.swift new file mode 100644 index 00000000..f36f303b --- /dev/null +++ b/Tests/PlayerTests/Playlist/ItemInsertionBeforeTests.swift @@ -0,0 +1,88 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class ItemInsertionBeforeTests: TestCase { + func testInsertItemBeforeNextItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) + expect(player.insert(insertedItem, before: item2)).to(beTrue()) + expect(player.items).to(equalDiff([item1, insertedItem, item2, item3])) + } + + func testInsertItemBeforeCurrentItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) + expect(player.insert(insertedItem, before: item2)).to(beTrue()) + expect(player.items).to(equalDiff([item1, insertedItem, item2, item3])) + } + + func testInsertItemBeforePreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + player.advanceToNextItem() + let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) + expect(player.insert(insertedItem, before: item2)).to(beTrue()) + expect(player.items).to(equalDiff([item1, insertedItem, item2, item3])) + } + + func testInsertItemBeforeFirstItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) + expect(player.insert(insertedItem, before: item1)).to(beTrue()) + expect(player.items).to(equalDiff([insertedItem, item1, item2])) + } + + func testInsertItemBeforeIdenticalItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.insert(item1, before: item2)).to(beFalse()) + expect(player.items).to(equalDiff([item1, item2])) + } + + func testInsertItemBeforeForeignItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) + let foreignItem = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item]) + expect(player.insert(insertedItem, before: foreignItem)).to(beFalse()) + expect(player.items).to(equalDiff([item])) + } + + func testInsertItemBeforeNil() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item]) + let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) + expect(player.insert(insertedItem, before: nil)).to(beTrue()) + expect(player.items).to(equalDiff([insertedItem, item])) + } + + func testPrependItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item]) + let insertedItem = PlayerItem.simple(url: Stream.onDemand.url) + expect(player.prepend(insertedItem)).to(beTrue()) + expect(player.items).to(equalDiff([insertedItem, item])) + } +} diff --git a/Tests/PlayerTests/Playlist/ItemMoveAfterTests.swift b/Tests/PlayerTests/Playlist/ItemMoveAfterTests.swift new file mode 100644 index 00000000..df0b0945 --- /dev/null +++ b/Tests/PlayerTests/Playlist/ItemMoveAfterTests.swift @@ -0,0 +1,147 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class ItemMoveAfterTests: TestCase { + func testMovePreviousItemAfterNextItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + expect(player.move(item1, after: item3)).to(beTrue()) + expect(player.items).to(equalDiff([item2, item3, item1])) + } + + func testMovePreviousItemAfterCurrentItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + player.advanceToNextItem() + expect(player.move(item1, after: item3)).to(beTrue()) + expect(player.items).to(equalDiff([item2, item3, item1])) + } + + func testMovePreviousItemAfterPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + player.advanceToNextItem() + expect(player.move(item1, after: item2)).to(beTrue()) + expect(player.items).to(equalDiff([item2, item1, item3])) + } + + func testMoveCurrentItemAfterNextItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + expect(player.move(item1, after: item2)).to(beTrue()) + expect(player.items).to(equalDiff([item2, item1, item3])) + } + + func testMoveCurrentItemAfterPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + player.advanceToNextItem() + expect(player.move(item3, after: item1)).to(beTrue()) + expect(player.items).to(equalDiff([item1, item3, item2])) + } + + func testMoveNextItemAfterPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + expect(player.move(item3, after: item1)).to(beTrue()) + expect(player.items).to(equalDiff([item1, item3, item2])) + } + + func testMoveNextItemAfterCurrentItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + expect(player.move(item3, after: item1)).to(beTrue()) + expect(player.items).to(equalDiff([item1, item3, item2])) + } + + func testMoveNextItemAfterNextItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + expect(player.move(item2, after: item3)).to(beTrue()) + expect(player.items).to(equalDiff([item1, item3, item2])) + } + + func testMoveItemAfterIdenticalItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item]) + expect(player.move(item, after: item)).to(beFalse()) + expect(player.items).to(equalDiff([item])) + } + + func testMoveItemAfterItemAlreadyAtExpectedLocation() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.move(item2, after: item1)).to(beFalse()) + expect(player.items).to(equalDiff([item1, item2])) + } + + func testMoveForeignItemAfterItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let foreignItem = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item]) + expect(player.move(foreignItem, after: item)).to(beFalse()) + expect(player.items).to(equalDiff([item])) + } + + func testMoveItemAfterForeignItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let foreignItem = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item]) + expect(player.move(item, after: foreignItem)).to(beFalse()) + expect(player.items).to(equalDiff([item])) + } + + func testMoveItemAfterLastItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.move(item1, after: item2)).to(beTrue()) + expect(player.items).to(equalDiff([item2, item1])) + } + + func testMoveItemAfterNil() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.move(item1, after: nil)).to(beTrue()) + expect(player.items).to(equalDiff([item2, item1])) + } + + func testMoveLastItemAfterNil() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item]) + expect(player.move(item, after: nil)).to(beFalse()) + expect(player.items).to(equalDiff([item])) + } +} diff --git a/Tests/PlayerTests/Playlist/ItemMoveBeforeTests.swift b/Tests/PlayerTests/Playlist/ItemMoveBeforeTests.swift new file mode 100644 index 00000000..c9cabc1d --- /dev/null +++ b/Tests/PlayerTests/Playlist/ItemMoveBeforeTests.swift @@ -0,0 +1,147 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class ItemMoveBeforeTests: TestCase { + func testMovePreviousItemBeforeNextItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + expect(player.move(item1, before: item3)).to(beTrue()) + expect(player.items).to(equalDiff([item2, item1, item3])) + } + + func testMovePreviousItemBeforeCurrentItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + player.advanceToNextItem() + expect(player.move(item1, before: item3)).to(beTrue()) + expect(player.items).to(equalDiff([item2, item1, item3])) + } + + func testMovePreviousItemBeforePreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + player.advanceToNextItem() + expect(player.move(item2, before: item1)).to(beTrue()) + expect(player.items).to(equalDiff([item2, item1, item3])) + } + + func testMoveCurrentItemBeforeNextItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + expect(player.move(item1, before: item3)).to(beTrue()) + expect(player.items).to(equalDiff([item2, item1, item3])) + } + + func testMoveCurrentItemBeforePreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + expect(player.move(item2, before: item1)).to(beTrue()) + expect(player.items).to(equalDiff([item2, item1, item3])) + } + + func testMoveNextItemBeforePreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + expect(player.move(item3, before: item1)).to(beTrue()) + expect(player.items).to(equalDiff([item3, item1, item2])) + } + + func testMoveNextItemBeforeCurrentItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + expect(player.move(item3, before: item2)).to(beTrue()) + expect(player.items).to(equalDiff([item1, item3, item2])) + } + + func testMoveNextItemBeforeNextItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + expect(player.move(item3, before: item2)).to(beTrue()) + expect(player.items).to(equalDiff([item1, item3, item2])) + } + + func testMoveItemBeforeIdenticalItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item]) + expect(player.move(item, before: item)).to(beFalse()) + expect(player.items).to(equalDiff([item])) + } + + func testMoveItemBeforeItemAlreadyAtExpectedLocation() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.move(item1, before: item2)).to(beFalse()) + expect(player.items).to(equalDiff([item1, item2])) + } + + func testMoveForeignItemBeforeItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let foreignItem = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item]) + expect(player.move(foreignItem, before: item)).to(beFalse()) + expect(player.items).to(equalDiff([item])) + } + + func testMoveBeforeForeignItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let foreignItem = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item]) + expect(player.move(item, before: foreignItem)).to(beFalse()) + expect(player.items).to(equalDiff([item])) + } + + func testMoveItemBeforeFirstItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.move(item2, before: item1)).to(beTrue()) + expect(player.items).to(equalDiff([item2, item1])) + } + + func testMoveBeforeNil() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.move(item2, before: nil)).to(beTrue()) + expect(player.items).to(equalDiff([item2, item1])) + } + + func testMoveFirstItemBeforeNil() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item]) + expect(player.move(item, before: nil)).to(beFalse()) + expect(player.items).to(equalDiff([item])) + } +} diff --git a/Tests/PlayerTests/Playlist/ItemNavigationBackwardChecksTests.swift b/Tests/PlayerTests/Playlist/ItemNavigationBackwardChecksTests.swift new file mode 100644 index 00000000..6ff6bc0b --- /dev/null +++ b/Tests/PlayerTests/Playlist/ItemNavigationBackwardChecksTests.swift @@ -0,0 +1,38 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class ItemNavigationBackwardChecksTests: TestCase { + func testCanReturnToPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + player.advanceToNextItem() + expect(player.canReturnToPreviousItem()).to(beTrue()) + } + + func testCannotReturnToPreviousItemAtFront() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.canReturnToPreviousItem()).to(beFalse()) + } + + func testCannotReturnToPreviousItemWhenEmpty() { + let player = Player() + expect(player.canReturnToPreviousItem()).to(beFalse()) + } + + func testWrapAtFrontWithRepeatAll() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + player.repeatMode = .all + expect(player.canReturnToPreviousItem()).to(beTrue()) + } +} diff --git a/Tests/PlayerTests/Playlist/ItemNavigationBackwardTests.swift b/Tests/PlayerTests/Playlist/ItemNavigationBackwardTests.swift new file mode 100644 index 00000000..174fc0fc --- /dev/null +++ b/Tests/PlayerTests/Playlist/ItemNavigationBackwardTests.swift @@ -0,0 +1,48 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class ItemNavigationBackwardTests: TestCase { + func testReturnToPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + player.advanceToNextItem() + player.returnToPreviousItem() + expect(player.currentItem).to(equal(item1)) + } + + func testReturnToPreviousItemAtFront() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + player.returnToPreviousItem() + expect(player.currentItem).to(equal(item1)) + } + + func testReturnToPreviousItemOnFailedItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.unavailable.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + player.returnToPreviousItem() + expect(player.currentItem).to(equal(item1)) + } + + func testWrapAtFrontWithRepeatAll() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + player.repeatMode = .all + player.returnToPreviousItem() + expect(player.currentItem).to(equal(item2)) + } +} diff --git a/Tests/PlayerTests/Playlist/ItemNavigationForwardChecksTests.swift b/Tests/PlayerTests/Playlist/ItemNavigationForwardChecksTests.swift new file mode 100644 index 00000000..98a6d21c --- /dev/null +++ b/Tests/PlayerTests/Playlist/ItemNavigationForwardChecksTests.swift @@ -0,0 +1,38 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class ItemNavigationForwardChecksTests: TestCase { + func testCanAdvanceToNextItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.canAdvanceToNextItem()).to(beTrue()) + } + + func testCannotAdvanceToNextItemAtBack() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + player.advanceToNextItem() + expect(player.canAdvanceToNextItem()).to(beFalse()) + } + + func testCannotAdvanceToNextItemWhenEmpty() { + let player = Player() + expect(player.canAdvanceToNextItem()).to(beFalse()) + } + + func testWrapAtBackWithRepeatAll() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + player.repeatMode = .all + expect(player.canAdvanceToNextItem()).to(beTrue()) + } +} diff --git a/Tests/PlayerTests/Playlist/ItemNavigationForwardTests.swift b/Tests/PlayerTests/Playlist/ItemNavigationForwardTests.swift new file mode 100644 index 00000000..92377e48 --- /dev/null +++ b/Tests/PlayerTests/Playlist/ItemNavigationForwardTests.swift @@ -0,0 +1,63 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class ItemNavigationForwardTests: TestCase { + func testAdvanceToNextItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + player.advanceToNextItem() + expect(player.currentItem).to(equal(item2)) + } + + func testAdvanceToNextItemAtBack() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + player.advanceToNextItem() + player.advanceToNextItem() + expect(player.currentItem).to(equal(item2)) + } + + func testAdvanceToNextItemOnFailedItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.unavailable.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + player.advanceToNextItem() + expect(player.currentItem).to(equal(item3)) + } + + func testPlayerPreloadedItemCount() { + let player = Player(items: [ + PlayerItem.simple(url: Stream.onDemand.url), + PlayerItem.simple(url: Stream.squareOnDemand.url), + PlayerItem.simple(url: Stream.mediumOnDemand.url), + PlayerItem.simple(url: Stream.onDemand.url), + PlayerItem.simple(url: Stream.shortOnDemand.url) + ]) + player.advanceToNextItem() + + let items = player.queuePlayer.items() + expect(items).to(haveCount(player.configuration.preloadedItems)) + } + + func testWrapAtBackWithRepeatAll() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + player.repeatMode = .all + player.advanceToNextItem() + player.advanceToNextItem() + expect(player.currentItem).to(equal(item1)) + } +} diff --git a/Tests/PlayerTests/Playlist/ItemRemovalTests.swift b/Tests/PlayerTests/Playlist/ItemRemovalTests.swift new file mode 100644 index 00000000..583300d3 --- /dev/null +++ b/Tests/PlayerTests/Playlist/ItemRemovalTests.swift @@ -0,0 +1,62 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class ItemRemovalTests: TestCase { + func testRemovePreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + player.remove(item1) + expect(player.items).to(equalDiff([item2, item3])) + } + + func testRemoveCurrentItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + player.remove(item2) + expect(player.currentItem).to(equal(item3)) + expect(player.items).to(equalDiff([item1, item3])) + } + + func testRemoveNextItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + player.remove(item3) + expect(player.items).to(equalDiff([item1, item2])) + } + + func testRemoveForeignItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let foreignItem = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item]) + player.remove(foreignItem) + expect(player.items).to(equalDiff([item])) + } + + func testRemoveAllItems() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.removeAllItems() + expect(player.items).to(beEmpty()) + expect(player.currentItem).to(beNil()) + } +} diff --git a/Tests/PlayerTests/Playlist/ItemsTests.swift b/Tests/PlayerTests/Playlist/ItemsTests.swift new file mode 100644 index 00000000..bbcb8f90 --- /dev/null +++ b/Tests/PlayerTests/Playlist/ItemsTests.swift @@ -0,0 +1,70 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class ItemsTests: TestCase { + func testItemsOnFirstItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + expect(player.items).to(equalDiff([item1, item2, item3])) + expect(player.previousItems).to(beEmpty()) + expect(player.nextItems).to(equalDiff([item2, item3])) + } + + func testItemsOnMiddleItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + expect(player.items).to(equalDiff([item1, item2, item3])) + expect(player.previousItems).to(equalDiff([item1])) + expect(player.nextItems).to(equalDiff([item3])) + } + + func testItemsOnLastItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.advanceToNextItem() + player.advanceToNextItem() + expect(player.items).to(equalDiff([item1, item2, item3])) + expect(player.previousItems).to(equalDiff([item1, item2])) + expect(player.nextItems).to(beEmpty()) + } + + func testEmpty() { + let player = Player() + expect(player.currentItem).to(beNil()) + expect(player.items).to(beEmpty()) + expect(player.nextItems).to(beEmpty()) + expect(player.previousItems).to(beEmpty()) + } + + func testRemoveAll() { + let item = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(item: item) + expect(player.currentItem).to(equal(item)) + player.removeAllItems() + expect(player.currentItem).to(beNil()) + } + + func testAppendAfterRemoveAll() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + player.removeAllItems() + let item = PlayerItem.simple(url: Stream.onDemand.url) + player.append(item) + expect(player.currentItem).to(equal(item)) + } +} diff --git a/Tests/PlayerTests/Playlist/ItemsUpdateTests.swift b/Tests/PlayerTests/Playlist/ItemsUpdateTests.swift new file mode 100644 index 00000000..c98459d4 --- /dev/null +++ b/Tests/PlayerTests/Playlist/ItemsUpdateTests.swift @@ -0,0 +1,49 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class ItemsUpdateTests: TestCase { + func testUpdateWithCurrentItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let item4 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.items = [item4, item3, item1] + expect(player.items).to(equalDiff([item4, item3, item1])) + expect(player.currentItem).to(equal(item1)) + } + + func testUpdateWithCurrentItemMustNotInterruptPlayback() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemandWithForcedAndUnforcedLegibleOptions.url) + let item3 = PlayerItem.simple(url: Stream.onDemandWithSingleAudibleOption.url) + let item4 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(items: [item1, item2, item3]) + expect(player.queuePlayer.currentItem?.url).toEventually(equal(Stream.onDemand.url)) + player.items = [item4, item3, item1] + expect(player.queuePlayer.currentItem?.url).toAlways(equal(Stream.onDemand.url), until: .seconds(2)) + } + + func testUpdateWithoutCurrentItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let item3 = PlayerItem.simple(url: Stream.onDemand.url) + let item4 = PlayerItem.simple(url: Stream.onDemand.url) + let item5 = PlayerItem.simple(url: Stream.onDemand.url) + let item6 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2, item3]) + player.items = [item4, item5, item6] + expect(player.items).to(equalDiff([item4, item5, item6])) + expect(player.currentItem).to(equal(item4)) + } +} diff --git a/Tests/PlayerTests/Playlist/NavigationBackwardChecksTests.swift b/Tests/PlayerTests/Playlist/NavigationBackwardChecksTests.swift new file mode 100644 index 00000000..15a5e2c8 --- /dev/null +++ b/Tests/PlayerTests/Playlist/NavigationBackwardChecksTests.swift @@ -0,0 +1,111 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble +import PillarboxStreams + +final class NavigationBackwardChecksTests: TestCase { + private static func configuration() -> PlayerConfiguration { + .init(navigationMode: .immediate) + } + + func testCannotReturnForOnDemandAtBeginningWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item, configuration: Self.configuration()) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.canReturnToPrevious()).to(beFalse()) + } + + func testCanReturnForOnDemandNearBeginningWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item, configuration: Self.configuration()) + expect(player.streamType).toEventually(equal(.onDemand)) + + waitUntil { done in + player.seek(at(CMTime(value: 1, timescale: 1))) { _ in + done() + } + } + + expect(player.canReturnToPrevious()).to(beFalse()) + } + + func testCanReturnForOnDemandAtBeginningWithPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2], configuration: Self.configuration()) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.canReturnToPrevious()).to(beTrue()) + } + + func testCanReturnForOnDemandNotAtBeginning() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2], configuration: Self.configuration()) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.onDemand)) + + waitUntil { done in + player.seek(at(CMTime(value: 5, timescale: 1))) { _ in + done() + } + } + + expect(player.canReturnToPrevious()).to(beTrue()) + } + + func testCanReturnForLiveWithPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.live.url) + let player = Player(items: [item1, item2], configuration: Self.configuration()) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.live)) + expect(player.canReturnToPrevious()).to(beTrue()) + } + + func testCannotReturnForLiveWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.live.url) + let player = Player(item: item, configuration: Self.configuration()) + expect(player.streamType).toEventually(equal(.live)) + expect(player.canReturnToPrevious()).to(beFalse()) + } + + func testCanReturnForDvrWithPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(items: [item1, item2], configuration: Self.configuration()) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.dvr)) + expect(player.canReturnToPrevious()).to(beTrue()) + } + + func testCannotReturnForDvrWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(item: item, configuration: Self.configuration()) + expect(player.streamType).toEventually(equal(.dvr)) + expect(player.canReturnToPrevious()).to(beFalse()) + } + + func testCanReturnForUnknownWithPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(items: [item1, item2], configuration: Self.configuration()) + player.advanceToNextItem() + expect(player.streamType).to(equal(.unknown)) + expect(player.canReturnToPrevious()).to(beTrue()) + } + + func testCannotReturnForUnknownWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(item: item, configuration: Self.configuration()) + expect(player.streamType).to(equal(.unknown)) + expect(player.canReturnToPrevious()).to(beFalse()) + } +} diff --git a/Tests/PlayerTests/Playlist/NavigationBackwardTests.swift b/Tests/PlayerTests/Playlist/NavigationBackwardTests.swift new file mode 100644 index 00000000..b7c086c1 --- /dev/null +++ b/Tests/PlayerTests/Playlist/NavigationBackwardTests.swift @@ -0,0 +1,137 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble +import PillarboxStreams + +final class NavigationBackwardTests: TestCase { + private static func configuration() -> PlayerConfiguration { + .init(navigationMode: .immediate) + } + + func testReturnForOnDemandAtBeginningWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item, configuration: Self.configuration()) + expect(player.streamType).toEventually(equal(.onDemand)) + player.returnToPrevious() + expect(player.currentItem).to(equal(item)) + } + + func testReturnForOnDemandNearBeginningWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item, configuration: Self.configuration()) + expect(player.streamType).toEventually(equal(.onDemand)) + + waitUntil { done in + player.seek(at(CMTime(value: 1, timescale: 1))) { _ in + done() + } + } + + player.returnToPrevious() + expect(player.currentItem).to(equal(item)) + expect(player.time()).toNever(equal(.zero), until: .seconds(3)) + } + + func testReturnForOnDemandAtBeginningWithPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2], configuration: Self.configuration()) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.onDemand)) + player.returnToPrevious() + expect(player.currentItem).to(equal(item1)) + } + + func testReturnForOnDemandNotAtBeginning() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2], configuration: Self.configuration()) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.onDemand)) + + waitUntil { done in + player.seek(at(CMTime(value: 5, timescale: 1))) { _ in + done() + } + } + player.returnToPrevious() + expect(player.currentItem).to(equal(item1)) + expect(player.time()).toEventually(equal(.zero)) + } + + func testReturnForLiveWithPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.live.url) + let player = Player(items: [item1, item2], configuration: Self.configuration()) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.live)) + player.returnToPreviousItem() + expect(player.currentItem).to(equal(item1)) + } + + func testReturnForLiveWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.live.url) + let player = Player(item: item, configuration: Self.configuration()) + expect(player.streamType).toEventually(equal(.live)) + player.returnToPreviousItem() + expect(player.currentItem).to(equal(item)) + } + + func testReturnForDvrWithPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(items: [item1, item2], configuration: Self.configuration()) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.dvr)) + player.returnToPreviousItem() + expect(player.currentItem).to(equal(item1)) + } + + func testReturnForDvrWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(item: item, configuration: Self.configuration()) + expect(player.streamType).toEventually(equal(.dvr)) + player.returnToPreviousItem() + expect(player.currentItem).to(equal(item)) + } + + func testReturnForUnknownWithPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(items: [item1, item2], configuration: Self.configuration()) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.unknown)) + player.returnToPreviousItem() + expect(player.currentItem).to(equal(item1)) + } + + func testReturnForUnknownWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(item: item, configuration: Self.configuration()) + expect(player.streamType).toEventually(equal(.unknown)) + player.returnToPreviousItem() + expect(player.currentItem).to(equal(item)) + } + + func testPlayerPreloadedItemCount() { + let player = Player(items: [ + PlayerItem.simple(url: Stream.onDemand.url), + PlayerItem.simple(url: Stream.squareOnDemand.url), + PlayerItem.simple(url: Stream.mediumOnDemand.url), + PlayerItem.simple(url: Stream.onDemand.url), + PlayerItem.simple(url: Stream.shortOnDemand.url) + ]) + player.advanceToNextItem() + player.returnToPrevious() + + let items = player.queuePlayer.items() + expect(items).to(haveCount(player.configuration.preloadedItems)) + } +} diff --git a/Tests/PlayerTests/Playlist/NavigationForwardChecksTests.swift b/Tests/PlayerTests/Playlist/NavigationForwardChecksTests.swift new file mode 100644 index 00000000..2e075dc8 --- /dev/null +++ b/Tests/PlayerTests/Playlist/NavigationForwardChecksTests.swift @@ -0,0 +1,72 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class NavigationForwardChecksTests: TestCase { + func testCanAdvanceForOnDemandWithNextItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.live.url) + let player = Player(items: [item1, item2]) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.canAdvanceToNext()).to(beTrue()) + } + + func testCannotAdvanceForOnDemandWithoutNextItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.canAdvanceToNext()).to(beFalse()) + } + + func testCanAdvanceForLiveWithNextItem() { + let item1 = PlayerItem.simple(url: Stream.live.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.streamType).toEventually(equal(.live)) + expect(player.canAdvanceToNext()).to(beTrue()) + } + + func testCannotAdvanceForLiveWithoutNextItem() { + let item = PlayerItem.simple(url: Stream.live.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.live)) + expect(player.canAdvanceToNext()).to(beFalse()) + } + + func testCanAdvanceForDvrWithNextItem() { + let item1 = PlayerItem.simple(url: Stream.dvr.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.streamType).toEventually(equal(.dvr)) + expect(player.canAdvanceToNext()).to(beTrue()) + } + + func testCannotAdvanceForDvrWithoutNextItem() { + let item = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.dvr)) + expect(player.canAdvanceToNext()).to(beFalse()) + } + + func testCanAdvanceForUnknownWithNextItem() { + let item1 = PlayerItem.simple(url: Stream.unavailable.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.streamType).to(equal(.unknown)) + expect(player.canAdvanceToNext()).to(beTrue()) + } + + func testCannotAdvanceForUnknownWithoutNextItem() { + let item = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(item: item) + expect(player.streamType).to(equal(.unknown)) + expect(player.canAdvanceToNext()).to(beFalse()) + } +} diff --git a/Tests/PlayerTests/Playlist/NavigationForwardTests.swift b/Tests/PlayerTests/Playlist/NavigationForwardTests.swift new file mode 100644 index 00000000..0e40acfd --- /dev/null +++ b/Tests/PlayerTests/Playlist/NavigationForwardTests.swift @@ -0,0 +1,80 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class NavigationForwardTests: TestCase { + func testAdvanceForOnDemandWithNextItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.live.url) + let player = Player(items: [item1, item2]) + expect(player.streamType).toEventually(equal(.onDemand)) + player.advanceToNext() + expect(player.currentItem).to(equal(item2)) + } + + func testAdvanceForOnDemandWithoutNextItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.onDemand)) + player.advanceToNext() + expect(player.currentItem).to(equal(item)) + } + + func testAdvanceForLiveWithNextItem() { + let item1 = PlayerItem.simple(url: Stream.live.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.streamType).toEventually(equal(.live)) + player.advanceToNext() + expect(player.currentItem).to(equal(item2)) + } + + func testAdvanceForLiveWithoutNextItem() { + let item = PlayerItem.simple(url: Stream.live.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.live)) + player.advanceToNext() + expect(player.currentItem).to(equal(item)) + } + + func testAdvanceForDvrWithNextItem() { + let item1 = PlayerItem.simple(url: Stream.dvr.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.streamType).toEventually(equal(.dvr)) + player.advanceToNext() + expect(player.currentItem).to(equal(item2)) + } + + func testAdvanceForDvrWithoutNextItem() { + let item = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.dvr)) + player.advanceToNext() + expect(player.currentItem).to(equal(item)) + } + + func testAdvanceForUnknownWithNextItem() { + let item1 = PlayerItem.simple(url: Stream.unavailable.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.streamType).to(equal(.unknown)) + player.advanceToNext() + expect(player.currentItem).to(equal(item2)) + } + + func testAdvanceForUnknownWithoutNextItem() { + let item = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(item: item) + expect(player.streamType).to(equal(.unknown)) + player.advanceToNext() + expect(player.currentItem).to(equal(item)) + } +} diff --git a/Tests/PlayerTests/Playlist/NavigationSmartBackwardChecksTests.swift b/Tests/PlayerTests/Playlist/NavigationSmartBackwardChecksTests.swift new file mode 100644 index 00000000..5eb6d1fe --- /dev/null +++ b/Tests/PlayerTests/Playlist/NavigationSmartBackwardChecksTests.swift @@ -0,0 +1,107 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble +import PillarboxStreams + +final class NavigationSmartBackwardChecksTests: TestCase { + func testCanReturnForOnDemandAtBeginningWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.canReturnToPrevious()).to(beTrue()) + } + + func testCanReturnForOnDemandNearBeginningWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.onDemand)) + + waitUntil { done in + player.seek(at(CMTime(value: 1, timescale: 1))) { _ in + done() + } + } + + expect(player.canReturnToPrevious()).to(beTrue()) + } + + func testCanReturnForOnDemandAtBeginningWithPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.canReturnToPrevious()).to(beTrue()) + } + + func testCanReturnForOnDemandNotAtBeginning() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.onDemand)) + + waitUntil { done in + player.seek(at(CMTime(value: 5, timescale: 1))) { _ in + done() + } + } + + expect(player.canReturnToPrevious()).to(beTrue()) + } + + func testCanReturnForLiveWithPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.live.url) + let player = Player(items: [item1, item2]) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.live)) + expect(player.canReturnToPrevious()).to(beTrue()) + } + + func testCannotReturnForLiveWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.live.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.live)) + expect(player.canReturnToPrevious()).to(beFalse()) + } + + func testCanReturnForDvrWithPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(items: [item1, item2]) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.dvr)) + expect(player.canReturnToPrevious()).to(beTrue()) + } + + func testCannotReturnForDvrWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.dvr)) + expect(player.canReturnToPrevious()).to(beFalse()) + } + + func testCanReturnForUnknownWithPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(items: [item1, item2]) + player.advanceToNextItem() + expect(player.streamType).to(equal(.unknown)) + expect(player.canReturnToPrevious()).to(beTrue()) + } + + func testCannotReturnForUnknownWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(item: item) + expect(player.streamType).to(equal(.unknown)) + expect(player.canReturnToPrevious()).to(beFalse()) + } +} diff --git a/Tests/PlayerTests/Playlist/NavigationSmartBackwardTests.swift b/Tests/PlayerTests/Playlist/NavigationSmartBackwardTests.swift new file mode 100644 index 00000000..222a445f --- /dev/null +++ b/Tests/PlayerTests/Playlist/NavigationSmartBackwardTests.swift @@ -0,0 +1,118 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble +import PillarboxStreams + +final class NavigationSmartBackwardTests: TestCase { + func testReturnForOnDemandAtBeginningWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.onDemand)) + player.returnToPrevious() + expect(player.currentItem).to(equal(item)) + } + + func testReturnForOnDemandNearBeginningWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.onDemand)) + + waitUntil { done in + player.seek(at(CMTime(value: 1, timescale: 1))) { _ in + done() + } + } + + player.returnToPrevious() + expect(player.currentItem).to(equal(item)) + expect(player.time()).toEventually(equal(.zero)) + } + + func testReturnForOnDemandAtBeginningWithPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.onDemand)) + player.returnToPrevious() + expect(player.currentItem).to(equal(item1)) + } + + func testReturnForOnDemandNotAtBeginning() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.onDemand)) + + waitUntil { done in + player.seek(at(CMTime(value: 5, timescale: 1))) { _ in + done() + } + } + player.returnToPrevious() + expect(player.currentItem).to(equal(item2)) + expect(player.time()).toEventually(equal(.zero)) + } + + func testReturnForLiveWithPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.live.url) + let player = Player(items: [item1, item2]) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.live)) + player.returnToPreviousItem() + expect(player.currentItem).to(equal(item1)) + } + + func testReturnForLiveWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.live.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.live)) + player.returnToPreviousItem() + expect(player.currentItem).to(equal(item)) + } + + func testReturnForDvrWithPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(items: [item1, item2]) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.dvr)) + player.returnToPreviousItem() + expect(player.currentItem).to(equal(item1)) + } + + func testReturnForDvrWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.dvr)) + player.returnToPreviousItem() + expect(player.currentItem).to(equal(item)) + } + + func testReturnForUnknownWithPreviousItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(items: [item1, item2]) + player.advanceToNextItem() + expect(player.streamType).toEventually(equal(.unknown)) + player.returnToPreviousItem() + expect(player.currentItem).to(equal(item1)) + } + + func testReturnForUnknownWithoutPreviousItem() { + let item = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.unknown)) + player.returnToPreviousItem() + expect(player.currentItem).to(equal(item)) + } +} diff --git a/Tests/PlayerTests/Playlist/NavigationSmartForwardChecksTests.swift b/Tests/PlayerTests/Playlist/NavigationSmartForwardChecksTests.swift new file mode 100644 index 00000000..e6c1e496 --- /dev/null +++ b/Tests/PlayerTests/Playlist/NavigationSmartForwardChecksTests.swift @@ -0,0 +1,72 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class NavigationSmartForwardChecksTests: TestCase { + func testCanAdvanceForOnDemandWithNextItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.live.url) + let player = Player(items: [item1, item2]) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.canAdvanceToNext()).to(beTrue()) + } + + func testCannotAdvanceForOnDemandWithoutNextItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.canAdvanceToNext()).to(beFalse()) + } + + func testCanAdvanceForLiveWithNextItem() { + let item1 = PlayerItem.simple(url: Stream.live.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.streamType).toEventually(equal(.live)) + expect(player.canAdvanceToNext()).to(beTrue()) + } + + func testCannotAdvanceForLiveWithoutNextItem() { + let item = PlayerItem.simple(url: Stream.live.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.live)) + expect(player.canAdvanceToNext()).to(beFalse()) + } + + func testCanAdvanceForDvrWithNextItem() { + let item1 = PlayerItem.simple(url: Stream.dvr.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.streamType).toEventually(equal(.dvr)) + expect(player.canAdvanceToNext()).to(beTrue()) + } + + func testCannotAdvanceForDvrWithoutNextItem() { + let item = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.dvr)) + expect(player.canAdvanceToNext()).to(beFalse()) + } + + func testCanAdvanceForUnknownWithNextItem() { + let item1 = PlayerItem.simple(url: Stream.unavailable.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.streamType).to(equal(.unknown)) + expect(player.canAdvanceToNext()).to(beTrue()) + } + + func testCannotAdvanceForUnknownWithoutNextItem() { + let item = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(item: item) + expect(player.streamType).to(equal(.unknown)) + expect(player.canAdvanceToNext()).to(beFalse()) + } +} diff --git a/Tests/PlayerTests/Playlist/NavigationSmartForwardTests.swift b/Tests/PlayerTests/Playlist/NavigationSmartForwardTests.swift new file mode 100644 index 00000000..6f579725 --- /dev/null +++ b/Tests/PlayerTests/Playlist/NavigationSmartForwardTests.swift @@ -0,0 +1,80 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class NavigationSmartForwardTests: TestCase { + func testAdvanceForOnDemandWithNextItem() { + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let item2 = PlayerItem.simple(url: Stream.live.url) + let player = Player(items: [item1, item2]) + expect(player.streamType).toEventually(equal(.onDemand)) + player.advanceToNext() + expect(player.currentItem).to(equal(item2)) + } + + func testAdvanceForOnDemandWithoutNextItem() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.onDemand)) + player.advanceToNext() + expect(player.currentItem).to(equal(item)) + } + + func testAdvanceForLiveWithNextItem() { + let item1 = PlayerItem.simple(url: Stream.live.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.streamType).toEventually(equal(.live)) + player.advanceToNext() + expect(player.currentItem).to(equal(item2)) + } + + func testAdvanceForLiveWithoutNextItem() { + let item = PlayerItem.simple(url: Stream.live.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.live)) + player.advanceToNext() + expect(player.currentItem).to(equal(item)) + } + + func testAdvanceForDvrWithNextItem() { + let item1 = PlayerItem.simple(url: Stream.dvr.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.streamType).toEventually(equal(.dvr)) + player.advanceToNext() + expect(player.currentItem).to(equal(item2)) + } + + func testAdvanceForDvrWithoutNextItem() { + let item = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.dvr)) + player.advanceToNext() + expect(player.currentItem).to(equal(item)) + } + + func testAdvanceForUnknownWithNextItem() { + let item1 = PlayerItem.simple(url: Stream.unavailable.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + expect(player.streamType).to(equal(.unknown)) + player.advanceToNext() + expect(player.currentItem).to(equal(item2)) + } + + func testAdvanceForUnknownWithoutNextItem() { + let item = PlayerItem.simple(url: Stream.unavailable.url) + let player = Player(item: item) + expect(player.streamType).to(equal(.unknown)) + player.advanceToNext() + expect(player.currentItem).to(equal(item)) + } +} diff --git a/Tests/PlayerTests/Playlist/RepeatModeTests.swift b/Tests/PlayerTests/Playlist/RepeatModeTests.swift new file mode 100644 index 00000000..298cc51b --- /dev/null +++ b/Tests/PlayerTests/Playlist/RepeatModeTests.swift @@ -0,0 +1,53 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class RepeatModeTests: TestCase { + func testRepeatOne() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(items: [item1, item2]) + player.repeatMode = .one + player.play() + expect(player.currentItem).toAlways(equal(item1), until: .seconds(2)) + player.repeatMode = .off + expect(player.currentItem).toEventually(equal(item2)) + } + + func testRepeatAll() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(items: [item1, item2]) + player.repeatMode = .all + player.play() + expect(player.currentItem).toEventually(equal(item1)) + expect(player.currentItem).toEventually(equal(item2)) + expect(player.currentItem).toEventually(equal(item1)) + player.repeatMode = .off + expect(player.currentItem).toEventually(equal(item2)) + expect(player.currentItem).toEventually(beNil()) + } + + func testRepeatModeUpdateDoesNotRestartPlayback() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + player.play() + expect(player.streamType).toEventually(equal(.onDemand)) + player.repeatMode = .one + expect(player.streamType).toNever(equal(.unknown), until: .milliseconds(100)) + } + + func testRepeatModeUpdateDoesNotReplay() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + player.play() + expect(player.currentItem).toEventually(beNil()) + player.repeatMode = .one + expect(player.currentItem).toAlways(beNil(), until: .milliseconds(100)) + } +} diff --git a/Tests/PlayerTests/ProgressTracker/ProgressTrackerPlaybackStateTests.swift b/Tests/PlayerTests/ProgressTracker/ProgressTrackerPlaybackStateTests.swift new file mode 100644 index 00000000..b8ee8729 --- /dev/null +++ b/Tests/PlayerTests/ProgressTracker/ProgressTrackerPlaybackStateTests.swift @@ -0,0 +1,65 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble +import PillarboxStreams + +final class ProgressTrackerPlaybackStateTests: TestCase { + func testInteractionPausesPlayback() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + progressTracker.player = player + player.play() + expect(player.playbackState).toEventually(equal(.playing)) + + progressTracker.isInteracting = true + expect(player.playbackState).toEventually(equal(.paused)) + + progressTracker.isInteracting = false + expect(player.playbackState).toEventually(equal(.playing)) + } + + func testInteractionDoesUpdateAlreadyPausedPlayback() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + progressTracker.player = player + expect(player.playbackState).toEventually(equal(.paused)) + + progressTracker.isInteracting = true + expect(player.playbackState).toAlways(equal(.paused), until: .seconds(1)) + + progressTracker.isInteracting = false + expect(player.playbackState).toAlways(equal(.paused), until: .seconds(1)) + } + + func testTransferInteractionBetweenPlayers() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + + let item1 = PlayerItem.simple(url: Stream.onDemand.url) + let player1 = Player(item: item1) + progressTracker.player = player1 + player1.play() + expect(player1.playbackState).toEventually(equal(.playing)) + + progressTracker.isInteracting = true + expect(player1.playbackState).toEventually(equal(.paused)) + + let item2 = PlayerItem.simple(url: Stream.onDemand.url) + let player2 = Player(item: item2) + progressTracker.player = player2 + player2.play() + expect(player2.playbackState).toEventually(equal(.playing)) + + progressTracker.player = player2 + expect(player1.playbackState).toEventually(equal(.playing)) + expect(player2.playbackState).toEventually(equal(.paused)) + } +} diff --git a/Tests/PlayerTests/ProgressTracker/ProgressTrackerProgressAvailabilityTests.swift b/Tests/PlayerTests/ProgressTracker/ProgressTrackerProgressAvailabilityTests.swift new file mode 100644 index 00000000..5dc223fe --- /dev/null +++ b/Tests/PlayerTests/ProgressTracker/ProgressTrackerProgressAvailabilityTests.swift @@ -0,0 +1,132 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Combine +import CoreMedia +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class ProgressTrackerProgressAvailabilityTests: TestCase { + func testUnbound() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + expectAtLeastEqualPublished( + values: [false], + from: progressTracker.changePublisher(at: \.isProgressAvailable) + .removeDuplicates() + ) + } + + func testEmptyPlayer() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + expectAtLeastEqualPublished( + values: [false], + from: progressTracker.changePublisher(at: \.isProgressAvailable) + .removeDuplicates() + ) { + progressTracker.player = Player() + } + } + + func testPausedPlayer() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expectAtLeastEqualPublished( + values: [false, true], + from: progressTracker.changePublisher(at: \.isProgressAvailable) + .removeDuplicates() + ) { + progressTracker.player = player + } + } + + func testEntirePlayback() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(item: item) + expectAtLeastEqualPublished( + values: [false, true, false], + from: progressTracker.changePublisher(at: \.isProgressAvailable) + .removeDuplicates() + ) { + progressTracker.player = player + player.play() + } + } + + func testPausedDvrStream() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(item: item) + expectAtLeastEqualPublished( + values: [false, true], + from: progressTracker.changePublisher(at: \.isProgressAvailable) + .removeDuplicates() + ) { + progressTracker.player = player + } + } + + func testPlayerChange() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + progressTracker.player = player + player.play() + expect(progressTracker.isProgressAvailable).toEventually(beTrue()) + + expectAtLeastEqualPublished( + values: [true, false], + from: progressTracker.changePublisher(at: \.isProgressAvailable) + .removeDuplicates() + ) { + progressTracker.player = Player() + } + } + + func testPlayerSetToNil() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + progressTracker.player = player + player.play() + expect(progressTracker.isProgressAvailable).toEventually(beTrue()) + + expectAtLeastEqualPublished( + values: [true, false], + from: progressTracker.changePublisher(at: \.isProgressAvailable) + .removeDuplicates() + ) { + progressTracker.player = nil + } + } + + func testBoundToPlayerAtSomeTime() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + + expect(player.seekableTimeRange).toEventuallyNot(equal(.invalid)) + let time = CMTime(value: 20, timescale: 1) + + waitUntil { done in + player.seek(at(time)) { _ in + done() + } + } + + expectAtLeastEqualPublished( + values: [false, true], + from: progressTracker.changePublisher(at: \.isProgressAvailable) + .removeDuplicates() + ) { + progressTracker.player = player + } + } +} diff --git a/Tests/PlayerTests/ProgressTracker/ProgressTrackerProgressTests.swift b/Tests/PlayerTests/ProgressTracker/ProgressTrackerProgressTests.swift new file mode 100644 index 00000000..559fb6f6 --- /dev/null +++ b/Tests/PlayerTests/ProgressTracker/ProgressTrackerProgressTests.swift @@ -0,0 +1,157 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Combine +import CoreMedia +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class ProgressTrackerProgressTests: TestCase { + func testUnbound() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + expectAtLeastEqualPublished( + values: [0], + from: progressTracker.changePublisher(at: \.progress) + .removeDuplicates() + ) + } + + func testEmptyPlayer() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + expectAtLeastEqualPublished( + values: [0], + from: progressTracker.changePublisher(at: \.progress) + .removeDuplicates() + ) { + progressTracker.player = Player() + } + } + + func testPausedPlayer() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expectAtLeastEqualPublished( + values: [0], + from: progressTracker.changePublisher(at: \.progress) + .removeDuplicates() + ) { + progressTracker.player = player + } + } + + func testEntirePlayback() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(item: item) + expectPublished( + values: [0, 0.25, 0.5, 0.75, 1, 0], + from: progressTracker.changePublisher(at: \.progress) + .removeDuplicates(), + to: beClose(within: 0.1), + during: .seconds(2) + ) { + progressTracker.player = player + player.play() + } + } + + func testPausedDvrStream() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(item: item) + expectAtLeastPublished( + values: [0, 1, 0.95, 0.9, 0.85, 0.8], + from: progressTracker.changePublisher(at: \.progress) + .removeDuplicates(), + to: beClose(within: 0.1) + ) { + progressTracker.player = player + } + } + + func testPlayerChange() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + progressTracker.player = player + player.play() + expect(progressTracker.progress).toEventuallyNot(equal(0)) + + let progress = progressTracker.progress + expectAtLeastEqualPublished( + values: [progress, 0], + from: progressTracker.changePublisher(at: \.progress) + .removeDuplicates() + ) { + progressTracker.player = Player() + } + } + + func testPlayerSetToNil() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + progressTracker.player = player + player.play() + expect(progressTracker.progress).toEventuallyNot(equal(0)) + + let progress = progressTracker.progress + expectAtLeastEqualPublished( + values: [progress, 0], + from: progressTracker.changePublisher(at: \.progress) + .removeDuplicates() + ) { + progressTracker.player = nil + } + } + + func testBoundToPlayerAtSomeTime() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + + expect(player.seekableTimeRange).toEventuallyNot(equal(.invalid)) + let time = CMTime(value: 20, timescale: 1) + + waitUntil { done in + player.seek(at(time)) { _ in + done() + } + } + + let progress = Float(20.0 / Stream.onDemand.duration.seconds) + expectAtLeastPublished( + values: [0, progress], + from: progressTracker.changePublisher(at: \.progress) + .removeDuplicates(), + to: beClose(within: 0.1) + ) { + progressTracker.player = player + } + } + + func testProgressForTimeInTimeRange() { + let timeRange = CMTimeRange(start: .zero, end: .init(value: 10, timescale: 1)) + expect(ProgressTracker.progress(for: .init(value: 5, timescale: 1), in: timeRange)).to(equal(0.5)) + expect(ProgressTracker.progress(for: .init(value: 15, timescale: 1), in: timeRange)).to(equal(1.5)) + } + + func testValidProgressInRange() { + expect(ProgressTracker.validProgress(nil, in: 0...1)).to(equal(0)) + expect(ProgressTracker.validProgress(0.5, in: 0...1)).to(equal(0.5)) + expect(ProgressTracker.validProgress(1.5, in: 0...1)).to(equal(1)) + } + + func testTimeForProgressInTimeRange() { + let timeRange = CMTimeRange(start: .zero, end: .init(value: 10, timescale: 1)) + expect(ProgressTracker.time(forProgress: 0.5, in: timeRange)).to(equal(CMTime(value: 5, timescale: 1))) + expect(ProgressTracker.time(forProgress: 1.5, in: timeRange)).to(equal(CMTime(value: 15, timescale: 1))) + } +} diff --git a/Tests/PlayerTests/ProgressTracker/ProgressTrackerRangeTests.swift b/Tests/PlayerTests/ProgressTracker/ProgressTrackerRangeTests.swift new file mode 100644 index 00000000..1c80b1eb --- /dev/null +++ b/Tests/PlayerTests/ProgressTracker/ProgressTrackerRangeTests.swift @@ -0,0 +1,132 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Combine +import CoreMedia +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class ProgressTrackerRangeTests: TestCase { + func testUnbound() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + expectAtLeastEqualPublished( + values: [0...0], + from: progressTracker.changePublisher(at: \.range) + .removeDuplicates() + ) + } + + func testEmptyPlayer() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + expectAtLeastEqualPublished( + values: [0...0], + from: progressTracker.changePublisher(at: \.range) + .removeDuplicates() + ) { + progressTracker.player = Player() + } + } + + func testPausedPlayer() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expectAtLeastEqualPublished( + values: [0...0, 0...1], + from: progressTracker.changePublisher(at: \.range) + .removeDuplicates() + ) { + progressTracker.player = player + } + } + + func testEntirePlayback() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(item: item) + expectAtLeastEqualPublished( + values: [0...0, 0...1, 0...0], + from: progressTracker.changePublisher(at: \.range) + .removeDuplicates() + ) { + progressTracker.player = player + player.play() + } + } + + func testPausedDvrStream() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(item: item) + expectAtLeastEqualPublished( + values: [0...0, 0...1], + from: progressTracker.changePublisher(at: \.range) + .removeDuplicates() + ) { + progressTracker.player = player + } + } + + func testPlayerChange() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + progressTracker.player = player + player.play() + expect(progressTracker.range).toEventuallyNot(equal(0...0)) + + expectAtLeastEqualPublished( + values: [0...1, 0...0], + from: progressTracker.changePublisher(at: \.range) + .removeDuplicates() + ) { + progressTracker.player = Player() + } + } + + func testPlayerSetToNil() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + progressTracker.player = player + player.play() + expect(progressTracker.range).toEventuallyNot(equal(0...0)) + + expectAtLeastEqualPublished( + values: [0...1, 0...0], + from: progressTracker.changePublisher(at: \.range) + .removeDuplicates() + ) { + progressTracker.player = nil + } + } + + func testBoundToPlayerAtSomeTime() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + + expect(player.seekableTimeRange).toEventuallyNot(equal(.invalid)) + let time = CMTime(value: 20, timescale: 1) + + waitUntil { done in + player.seek(at(time)) { _ in + done() + } + } + + expectAtLeastEqualPublished( + values: [0...0, 0...1], + from: progressTracker.changePublisher(at: \.range) + .removeDuplicates() + ) { + progressTracker.player = player + } + } +} diff --git a/Tests/PlayerTests/ProgressTracker/ProgressTrackerSeekBehaviorTests.swift b/Tests/PlayerTests/ProgressTracker/ProgressTrackerSeekBehaviorTests.swift new file mode 100644 index 00000000..f7088c0d --- /dev/null +++ b/Tests/PlayerTests/ProgressTracker/ProgressTrackerSeekBehaviorTests.swift @@ -0,0 +1,68 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Combine +import CoreMedia +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class ProgressTrackerSeekBehaviorTests: TestCase { + private func isSeekingPublisher(for player: Player) -> AnyPublisher { + player.propertiesPublisher + .slice(at: \.isSeeking) + .eraseToAnyPublisher() + } + + func testImmediateSeek() { + let progressTracker = ProgressTracker( + interval: CMTime(value: 1, timescale: 4), + seekBehavior: .immediate + ) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + progressTracker.player = player + expect(progressTracker.range).toEventually(equal(0...1)) + + expectAtLeastEqualPublished( + values: [false, true, false], + from: isSeekingPublisher(for: player) + ) { + progressTracker.isInteracting = true + progressTracker.progress = 0.5 + } + expect(progressTracker.progress).to(equal(0.5)) + } + + func testDeferredSeek() { + let progressTracker = ProgressTracker( + interval: CMTime(value: 1, timescale: 4), + seekBehavior: .deferred + ) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + progressTracker.player = player + expect(progressTracker.range).toEventually(equal(0...1)) + + expectAtLeastEqualPublished( + values: [false], + from: isSeekingPublisher(for: player) + ) { + progressTracker.isInteracting = true + progressTracker.progress = 0.5 + } + + expectAtLeastEqualPublishedNext( + values: [true, false], + from: isSeekingPublisher(for: player) + ) { + progressTracker.isInteracting = false + } + expect(progressTracker.progress).toEventually(equal(0.5)) + } +} diff --git a/Tests/PlayerTests/ProgressTracker/ProgressTrackerTimeTests.swift b/Tests/PlayerTests/ProgressTracker/ProgressTrackerTimeTests.swift new file mode 100644 index 00000000..0752bafe --- /dev/null +++ b/Tests/PlayerTests/ProgressTracker/ProgressTrackerTimeTests.swift @@ -0,0 +1,153 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Combine +import CoreMedia +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class ProgressTrackerTimeTests: TestCase { + func testUnbound() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + expectAtLeastEqualPublished( + values: [.invalid], + from: progressTracker.changePublisher(at: \.time) + .removeDuplicates() + ) + } + + func testEmptyPlayer() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + expectAtLeastEqualPublished( + values: [.invalid], + from: progressTracker.changePublisher(at: \.time) + .removeDuplicates() + ) { + progressTracker.player = Player() + } + } + + func testPausedPlayer() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expectAtLeastEqualPublished( + values: [.invalid, .zero], + from: progressTracker.changePublisher(at: \.time) + .removeDuplicates() + ) { + progressTracker.player = player + } + } + + func testEntirePlayback() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.shortOnDemand.url) + let player = Player(item: item) + expectPublished( + values: [ + .invalid, + .zero, + CMTime(value: 1, timescale: 4), + CMTime(value: 1, timescale: 2), + CMTime(value: 3, timescale: 4), + CMTime(value: 1, timescale: 1), + .invalid + ], + from: progressTracker.changePublisher(at: \.time) + .removeDuplicates(), + to: beClose(within: 0.1), + during: .seconds(2) + ) { + progressTracker.player = player + player.play() + } + } + + func testPausedDvrStream() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(item: item) + expectAtLeastPublished( + values: [ + .invalid, + CMTime(value: 17, timescale: 1), + CMTime(value: 17, timescale: 1), + CMTime(value: 17, timescale: 1), + CMTime(value: 17, timescale: 1), + CMTime(value: 17, timescale: 1) + ], + from: progressTracker.changePublisher(at: \.time) + .removeDuplicates(), + to: beClose(within: 0.1) + ) { + progressTracker.player = player + } + } + + func testPlayerChange() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + progressTracker.player = player + player.play() + expect(progressTracker.time).toEventuallyNot(equal(.invalid)) + + let time = progressTracker.time + expectAtLeastEqualPublished( + values: [time, .invalid], + from: progressTracker.changePublisher(at: \.time) + .removeDuplicates() + ) { + progressTracker.player = Player() + } + } + + func testPlayerSetToNil() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + progressTracker.player = player + player.play() + expect(progressTracker.time).toEventuallyNot(equal(.invalid)) + + let time = progressTracker.time + expectAtLeastEqualPublished( + values: [time, .invalid], + from: progressTracker.changePublisher(at: \.time) + .removeDuplicates() + ) { + progressTracker.player = nil + } + } + + func testBoundToPlayerAtSomeTime() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + + expect(player.seekableTimeRange).toEventuallyNot(equal(.invalid)) + let time = CMTime(value: 20, timescale: 1) + + waitUntil { done in + player.seek(at(time)) { _ in + done() + } + } + + expectAtLeastPublished( + values: [.invalid, time], + from: progressTracker.changePublisher(at: \.time) + .removeDuplicates(), + to: beClose(within: 0.1) + ) { + progressTracker.player = player + } + } +} diff --git a/Tests/PlayerTests/ProgressTracker/ProgressTrackerValueTests.swift b/Tests/PlayerTests/ProgressTracker/ProgressTrackerValueTests.swift new file mode 100644 index 00000000..830d10a8 --- /dev/null +++ b/Tests/PlayerTests/ProgressTracker/ProgressTrackerValueTests.swift @@ -0,0 +1,56 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble +import PillarboxStreams + +final class ProgressTrackerValueTests: TestCase { + func testProgressValueInRange() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + progressTracker.player = player + player.play() + expect(progressTracker.range).toEventuallyNot(equal(0...0)) + progressTracker.progress = 0.5 + expect(progressTracker.progress).to(beCloseTo(0.5, within: 0.1)) + } + + func testProgressValueBelowZero() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + progressTracker.player = player + player.play() + expect(progressTracker.range).toEventuallyNot(equal(0...0)) + progressTracker.progress = -10 + expect(progressTracker.progress).to(beCloseTo(0, within: 0.1)) + } + + func testProgressValueAboveOne() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + progressTracker.player = player + player.play() + expect(progressTracker.range).toEventuallyNot(equal(0...0)) + progressTracker.progress = 10 + expect(progressTracker.progress).to(beCloseTo(1, within: 0.1)) + } + + func testCannotChangeProgressWhenUnavailable() { + let progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 4)) + let player = Player() + progressTracker.player = player + expect(progressTracker.isProgressAvailable).to(equal(false)) + expect(progressTracker.progress).to(beCloseTo(0, within: 0.1)) + progressTracker.progress = 0.5 + expect(progressTracker.progress).to(beCloseTo(0, within: 0.1)) + } +} diff --git a/Tests/PlayerTests/Publishers/AVAssetMediaSelectionGroupsPublisherTests.swift b/Tests/PlayerTests/Publishers/AVAssetMediaSelectionGroupsPublisherTests.swift new file mode 100644 index 00000000..9dd24d60 --- /dev/null +++ b/Tests/PlayerTests/Publishers/AVAssetMediaSelectionGroupsPublisherTests.swift @@ -0,0 +1,37 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble +import PillarboxStreams + +// swiftlint:disable:next type_name +final class AVAssetMediaSelectionGroupsPublisherTests: TestCase { + func testFetch() throws { + let asset = AVURLAsset(url: Stream.onDemandWithOptions.url) + let groups = try waitForSingleOutput(from: asset.mediaSelectionGroupsPublisher()) + expect(groups[.audible]).notTo(beNil()) + expect(groups[.legible]).notTo(beNil()) + } + + func testFetchWithoutSelectionAvailable() throws { + let asset = AVURLAsset(url: Stream.onDemandWithoutOptions.url) + let groups = try waitForSingleOutput(from: asset.mediaSelectionGroupsPublisher()) + expect(groups).to(beEmpty()) + } + + func testRepeatedFetch() throws { + let asset = AVURLAsset(url: Stream.onDemandWithOptions.url) + + let groups1 = try waitForSingleOutput(from: asset.mediaSelectionGroupsPublisher()) + expect(groups1).notTo(beEmpty()) + + let groups2 = try waitForSingleOutput(from: asset.mediaSelectionGroupsPublisher()) + expect(groups2).to(equal(groups1)) + } +} diff --git a/Tests/PlayerTests/Publishers/AVAsynchronousKeyValueLoadingPublisherTests.swift b/Tests/PlayerTests/Publishers/AVAsynchronousKeyValueLoadingPublisherTests.swift new file mode 100644 index 00000000..b2ad7d09 --- /dev/null +++ b/Tests/PlayerTests/Publishers/AVAsynchronousKeyValueLoadingPublisherTests.swift @@ -0,0 +1,62 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +// swiftlint:disable:next type_name +final class AVAsynchronousKeyValueLoadingPublisherTests: TestCase { + func testAssetFetch() throws { + let asset = AVURLAsset(url: Stream.onDemand.url) + let duration = try waitForSingleOutput(from: asset.propertyPublisher(.duration)) + expect(duration).to(equal(Stream.onDemand.duration, by: beClose(within: 1))) + } + + func testAssetRepeatedFetch() throws { + let asset = AVURLAsset(url: Stream.onDemand.url) + + let duration1 = try waitForSingleOutput(from: asset.propertyPublisher(.duration)) + expect(duration1).to(equal(Stream.onDemand.duration, by: beClose(within: 1))) + + let duration2 = try waitForSingleOutput(from: asset.propertyPublisher(.duration)) + expect(duration2).to(equal(Stream.onDemand.duration, by: beClose(within: 1))) + } + + func testAssetFailedFetch() throws { + let asset = AVURLAsset(url: Stream.unavailable.url) + let error = try waitForFailure(from: asset.propertyPublisher(.duration)) + expect(error).notTo(beNil()) + } + + func testAssetMultipleFetch() throws { + let asset = AVURLAsset(url: Stream.onDemand.url) + let (duration, preferredRate) = try waitForSingleOutput(from: asset.propertyPublisher(.duration, .preferredRate)) + expect(duration).to(equal(Stream.onDemand.duration, by: beClose(within: 1))) + expect(preferredRate).to(equal(1)) + } + + func testAssetFailedMultipleFetch() throws { + let asset = AVURLAsset(url: Stream.unavailable.url) + let error = try waitForFailure(from: asset.propertyPublisher(.duration, .preferredRate)) + expect(error).notTo(beNil()) + } + + func testMetadataItemFetch() throws { + let item = AVMetadataItem(identifier: .commonIdentifierTitle, value: "Title")! + let title = try waitForSingleOutput(from: item.propertyPublisher(.stringValue)) + expect(title).to(equal("Title")) + } + + func testMetadataItemFetchWithTypeMismatch() throws { + let item = AVMetadataItem(identifier: .commonIdentifierTitle, value: "Title")! + let title = try waitForSingleOutput(from: item.propertyPublisher(.dateValue)) + expect(title).to(beNil()) + } +} diff --git a/Tests/PlayerTests/Publishers/AVPlayerBoundaryTimePublisherTests.swift b/Tests/PlayerTests/Publishers/AVPlayerBoundaryTimePublisherTests.swift new file mode 100644 index 00000000..3a4c2e7f --- /dev/null +++ b/Tests/PlayerTests/Publishers/AVPlayerBoundaryTimePublisherTests.swift @@ -0,0 +1,74 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Combine +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class AVPlayerBoundaryTimePublisherTests: TestCase { + func testEmpty() { + let player = AVPlayer() + expectNothingPublished( + from: Publishers.BoundaryTimePublisher( + for: player, + times: [CMTimeMake(value: 1, timescale: 2)] + ), + during: .seconds(2) + ) + } + + func testNoPlayback() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = AVPlayer(playerItem: item) + expectNothingPublished( + from: Publishers.BoundaryTimePublisher( + for: player, + times: [CMTimeMake(value: 1, timescale: 2)] + ), + during: .seconds(2) + ) + } + + func testPlayback() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = AVPlayer(playerItem: item) + expectAtLeastEqualPublished( + values: [ + "tick", "tick" + ], + from: Publishers.BoundaryTimePublisher( + for: player, + times: [ + CMTimeMake(value: 1, timescale: 2), + CMTimeMake(value: 2, timescale: 2) + ] + ) + .map { "tick" } + ) { + player.play() + } + } + + func testDeallocation() { + var player: AVPlayer? = AVPlayer() + _ = Publishers.BoundaryTimePublisher( + for: player!, + times: [ + CMTimeMake(value: 1, timescale: 2) + ] + ) + + weak var weakPlayer = player + autoreleasepool { + player = nil + } + expect(weakPlayer).to(beNil()) + } +} diff --git a/Tests/PlayerTests/Publishers/AVPlayerItemErrorPublisherTests.swift b/Tests/PlayerTests/Publishers/AVPlayerItemErrorPublisherTests.swift new file mode 100644 index 00000000..0240d286 --- /dev/null +++ b/Tests/PlayerTests/Publishers/AVPlayerItemErrorPublisherTests.swift @@ -0,0 +1,41 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Combine +import PillarboxStreams + +final class AVPlayerItemErrorPublisherTests: TestCase { + private static func errorCodePublisher(for item: AVPlayerItem) -> AnyPublisher { + item.errorPublisher() + .map { .init(rawValue: ($0 as NSError).code) } + .eraseToAnyPublisher() + } + + func testNoError() { + let item = AVPlayerItem(url: Stream.onDemand.url) + _ = AVPlayer(playerItem: item) + expectNothingPublished(from: item.errorPublisher(), during: .milliseconds(500)) + } + + func testM3u8Error() { + let item = AVPlayerItem(url: Stream.unavailable.url) + _ = AVPlayer(playerItem: item) + expectAtLeastEqualPublished(values: [ + URLError.fileDoesNotExist + ], from: Self.errorCodePublisher(for: item)) + } + + func testMp3Error() { + let item = AVPlayerItem(url: Stream.unavailableMp3.url) + _ = AVPlayer(playerItem: item) + expectAtLeastEqualPublished(values: [ + URLError.fileDoesNotExist + ], from: Self.errorCodePublisher(for: item)) + } +} diff --git a/Tests/PlayerTests/Publishers/AVPlayerItemMetricEventPublisherTests.swift b/Tests/PlayerTests/Publishers/AVPlayerItemMetricEventPublisherTests.swift new file mode 100644 index 00000000..f4811e4f --- /dev/null +++ b/Tests/PlayerTests/Publishers/AVPlayerItemMetricEventPublisherTests.swift @@ -0,0 +1,54 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import PillarboxStreams + +final class AVPlayerItemMetricEventPublisherTests: TestCase { + func testPlayableItemAssetMetricEvent() { + let item = AVPlayerItem(url: Stream.onDemand.url) + _ = AVPlayer(playerItem: item) + expectOnlySimilarPublished( + values: [.anyAsset], + from: item.assetMetricEventPublisher() + ) + } + + func testFailingItemAssetMetricEvent() { + let item = AVPlayerItem(url: Stream.unavailable.url) + _ = AVPlayer(playerItem: item) + expectNothingPublished(from: item.assetMetricEventPublisher(), during: .milliseconds(500)) + } + + func testPlayableItemFailureMetricEvent() { + let item = AVPlayerItem(url: Stream.onDemand.url) + _ = AVPlayer(playerItem: item) + expectNothingPublished(from: item.failureMetricEventPublisher(), during: .milliseconds(500)) + } + + func testFailingItemFailureMetricEvent() { + let item = AVPlayerItem(url: Stream.unavailable.url) + _ = AVPlayer(playerItem: item) + expectOnlySimilarPublished( + values: [.anyFailure], + from: item.failureMetricEventPublisher() + ) + } + + func testPlayableItemWarningMetricEvent() { + let item = AVPlayerItem(url: Stream.onDemand.url) + _ = AVPlayer(playerItem: item) + expectNothingPublished(from: item.warningMetricEventPublisher(), during: .milliseconds(500)) + } + + func testPlayableItemStallMetricEvent() { + let item = AVPlayerItem(url: Stream.onDemand.url) + _ = AVPlayer(playerItem: item) + expectNothingPublished(from: item.stallEventPublisher(), during: .milliseconds(500)) + } +} diff --git a/Tests/PlayerTests/Publishers/AVPlayerPeriodicTimePublisherTests.swift b/Tests/PlayerTests/Publishers/AVPlayerPeriodicTimePublisherTests.swift new file mode 100644 index 00000000..17f281de --- /dev/null +++ b/Tests/PlayerTests/Publishers/AVPlayerPeriodicTimePublisherTests.swift @@ -0,0 +1,60 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Combine +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class AVPlayerPeriodicTimePublisherTests: TestCase { + func testEmpty() { + let player = AVPlayer() + expectNothingPublished( + from: Publishers.PeriodicTimePublisher( + for: player, + interval: CMTimeMake(value: 1, timescale: 10) + ), + during: .milliseconds(500) + ) + } + + func testPlayback() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = AVPlayer(playerItem: item) + expectAtLeastPublished( + values: [ + .zero, + CMTimeMake(value: 1, timescale: 10), + CMTimeMake(value: 2, timescale: 10), + CMTimeMake(value: 3, timescale: 10) + ], + from: Publishers.PeriodicTimePublisher( + for: player, + interval: CMTimeMake(value: 1, timescale: 10) + ), + to: beClose(within: 0.1) + ) { + player.play() + } + } + + func testDeallocation() { + var player: AVPlayer? = AVPlayer() + _ = Publishers.PeriodicTimePublisher( + for: player!, + interval: CMTime(value: 1, timescale: 1) + ) + + weak var weakPlayer = player + autoreleasepool { + player = nil + } + expect(weakPlayer).to(beNil()) + } +} diff --git a/Tests/PlayerTests/Publishers/MetadataPublisherTests.swift b/Tests/PlayerTests/Publishers/MetadataPublisherTests.swift new file mode 100644 index 00000000..ceee5204 --- /dev/null +++ b/Tests/PlayerTests/Publishers/MetadataPublisherTests.swift @@ -0,0 +1,94 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Combine +import PillarboxCircumspect +import PillarboxStreams + +final class MetadataPublisherTests: TestCase { + private static func titlePublisherTest(for player: Player) -> AnyPublisher { + player.metadataPublisher.map(\.title).eraseToAnyPublisher() + } + + func testEmpty() { + let player = Player() + expectEqualPublished( + values: [nil], + from: Self.titlePublisherTest(for: player), + during: .milliseconds(100) + ) + } + + func testImmediatelyAvailableWithoutMetadata() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expectEqualPublished( + values: [nil], + from: Self.titlePublisherTest(for: player), + during: .milliseconds(100) + ) + } + + func testAvailableAfterDelay() { + let player = Player( + item: .mock(url: Stream.onDemand.url, loadedAfter: 0.1, withMetadata: AssetMetadataMock(title: "title")) + ) + expectEqualPublished( + values: [nil, "title"], + from: Self.titlePublisherTest(for: player), + during: .milliseconds(200) + ) + } + + func testImmediatelyAvailableWithMetadata() { + let player = Player(item: .mock( + url: Stream.onDemand.url, + loadedAfter: 0, + withMetadata: AssetMetadataMock(title: "title") + )) + expectEqualPublished( + values: [nil, "title"], + from: Self.titlePublisherTest(for: player), + during: .milliseconds(200) + ) + } + + func testUpdate() { + let player = Player(item: .mock(url: Stream.onDemand.url, withMetadataUpdateAfter: 0.1)) + expectEqualPublished( + values: [nil, "title0", "title1"], + from: Self.titlePublisherTest(for: player), + during: .milliseconds(500) + ) + } + + func testNetworkItemReloading() { + let player = Player(item: .webServiceMock(media: .media1)) + expectAtLeastEqualPublished( + values: [nil, "Title 1"], + from: Self.titlePublisherTest(for: player) + ) + expectEqualPublishedNext( + values: [nil, "Title 2"], + from: Self.titlePublisherTest(for: player), + during: .milliseconds(500) + ) { + player.items = [.webServiceMock(media: .media2)] + } + } + + func testEntirePlayback() { + let player = Player(item: .mock(url: Stream.shortOnDemand.url, loadedAfter: 0, withMetadata: AssetMetadataMock(title: "title"))) + expectEqualPublished( + values: [nil, "title", nil], + from: Self.titlePublisherTest(for: player), + during: .seconds(2) + ) { + player.play() + } + } +} diff --git a/Tests/PlayerTests/Publishers/NowPlayingInfoPublisherTests.swift b/Tests/PlayerTests/Publishers/NowPlayingInfoPublisherTests.swift new file mode 100644 index 00000000..0b179ae5 --- /dev/null +++ b/Tests/PlayerTests/Publishers/NowPlayingInfoPublisherTests.swift @@ -0,0 +1,47 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Combine +import MediaPlayer +import PillarboxCircumspect +import PillarboxStreams + +final class NowPlayingInfoPublisherTests: TestCase { + private static func nowPlayingInfoPublisher(for player: Player) -> AnyPublisher { + player.nowPlayingPublisher() + .map(\.info) + .eraseToAnyPublisher() + } + + func testInactive() { + let player = Player(item: .mock(url: Stream.onDemand.url, loadedAfter: 0, withMetadata: AssetMetadataMock(title: "title"))) + expectSimilarPublished( + values: [[:]], + from: Self.nowPlayingInfoPublisher(for: player), + during: .milliseconds(100) + ) + } + + func testToggleActive() { + let player = Player(item: .mock(url: Stream.onDemand.url, loadedAfter: 0, withMetadata: AssetMetadataMock(title: "title"))) + expectAtLeastSimilarPublished( + values: [[:], [MPNowPlayingInfoPropertyIsLiveStream: false]], + from: Self.nowPlayingInfoPublisher(for: player) + ) { + player.isActive = true + } + + expectSimilarPublishedNext( + values: [[:]], + from: Self.nowPlayingInfoPublisher(for: player), + during: .milliseconds(100) + ) { + player.isActive = false + } + } +} diff --git a/Tests/PlayerTests/Publishers/PeriodicMetricsPublisherTests.swift b/Tests/PlayerTests/Publishers/PeriodicMetricsPublisherTests.swift new file mode 100644 index 00000000..9fe33272 --- /dev/null +++ b/Tests/PlayerTests/Publishers/PeriodicMetricsPublisherTests.swift @@ -0,0 +1,72 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Combine +import PillarboxCircumspect +import PillarboxStreams + +final class PeriodicMetricsPublisherTests: TestCase { + func testEmpty() { + let player = Player() + expectEqualPublished( + values: [0], + from: player.periodicMetricsPublisher(forInterval: .init(value: 1, timescale: 4)).map(\.count), + during: .seconds(1) + ) + } + + func testPlayback() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expectAtLeastEqualPublished( + values: [0, 1, 2], + from: player.periodicMetricsPublisher(forInterval: .init(value: 1, timescale: 4)).map(\.count) + ) { + player.play() + } + } + + func testPlaylist() { + let item1 = PlayerItem.simple(url: Stream.shortOnDemand.url) + let item2 = PlayerItem.simple(url: Stream.mediumOnDemand.url) + let player = Player(items: [item1, item2]) + let publisher = player.periodicMetricsPublisher(forInterval: .init(value: 1, timescale: 1)).map(\.count) + expectAtLeastEqualPublished(values: [0, 1, 0, 1], from: publisher) { + player.play() + } + } + + func testNoMetricsForLiveMp3() { + let player = Player(item: .simple(url: Stream.liveMp3.url)) + let publisher = player.periodicMetricsPublisher(forInterval: .init(value: 1, timescale: 4)).map(\.count) + expectEqualPublished(values: [0], from: publisher, during: .milliseconds(500)) { + player.play() + } + } + + func testLimit() { + let item = PlayerItem.simple(url: Stream.onDemand.url) + let player = Player(item: item) + expectAtLeastEqualPublished( + values: [0, 1, 2, 2, 2, 2], + from: player.periodicMetricsPublisher(forInterval: .init(value: 1, timescale: 4), limit: 2).map(\.count) + ) { + player.play() + } + } + + func testFailure() { + let item = PlayerItem.failing(loadedAfter: 0.1) + let player = Player(item: item) + expectEqualPublished( + values: [0], + from: player.periodicMetricsPublisher(forInterval: .init(value: 1, timescale: 4)).map(\.count), + during: .seconds(1) + ) + } +} diff --git a/Tests/PlayerTests/Publishers/PlayerItemMetricEventPublisherTests.swift b/Tests/PlayerTests/Publishers/PlayerItemMetricEventPublisherTests.swift new file mode 100644 index 00000000..108c5624 --- /dev/null +++ b/Tests/PlayerTests/Publishers/PlayerItemMetricEventPublisherTests.swift @@ -0,0 +1,28 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import PillarboxStreams + +final class PlayerItemMetricEventPublisherTests: TestCase { + func testPlayableItemMetricEvent() { + let item = PlayerItem.mock(url: Stream.onDemand.url, loadedAfter: 0.1) + expectAtLeastSimilarPublished( + values: [.anyMetadata], + from: item.metricEventPublisher() + ) { + PlayerItem.load(for: item.id) + } + } + + func testFailingItemMetricEvent() { + let item = PlayerItem.failing(loadedAfter: 0.1) + expectNothingPublished(from: item.metricEventPublisher(), during: .milliseconds(500)) { + PlayerItem.load(for: item.id) + } + } +} diff --git a/Tests/PlayerTests/Publishers/PlayerPublisherTests.swift b/Tests/PlayerTests/Publishers/PlayerPublisherTests.swift new file mode 100644 index 00000000..814e75c3 --- /dev/null +++ b/Tests/PlayerTests/Publishers/PlayerPublisherTests.swift @@ -0,0 +1,159 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Combine +import PillarboxCircumspect +import PillarboxCore +import PillarboxStreams + +final class PlayerPublisherTests: TestCase { + private static func bufferingPublisher(for player: Player) -> AnyPublisher { + player.propertiesPublisher + .slice(at: \.isBuffering) + .eraseToAnyPublisher() + } + + private static func presentationSizePublisher(for player: Player) -> AnyPublisher { + player.propertiesPublisher + .slice(at: \.presentationSize) + .eraseToAnyPublisher() + } + + private static func itemStatusPublisher(for player: Player) -> AnyPublisher { + player.propertiesPublisher + .slice(at: \.itemStatus) + .eraseToAnyPublisher() + } + + private static func durationPublisher(for player: Player) -> AnyPublisher { + player.propertiesPublisher + .slice(at: \.duration) + .eraseToAnyPublisher() + } + + private static func seekableTimeRangePublisher(for player: Player) -> AnyPublisher { + player.propertiesPublisher + .slice(at: \.seekableTimeRange) + .eraseToAnyPublisher() + } + + func testBufferingEmpty() { + let player = Player() + expectEqualPublished( + values: [false], + from: Self.bufferingPublisher(for: player), + during: .milliseconds(500) + ) + } + + func testBuffering() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expectEqualPublished( + values: [true, false], + from: Self.bufferingPublisher(for: player), + during: .seconds(1) + ) + } + + func testPresentationSizeEmpty() { + let player = Player() + expectAtLeastEqualPublished( + values: [nil], + from: Self.presentationSizePublisher(for: player) + ) + } + + func testPresentationSize() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + expectAtLeastEqualPublished( + values: [nil, CGSize(width: 640, height: 360), nil], + from: Self.presentationSizePublisher(for: player) + ) { + player.play() + } + } + + func testItemStatusEmpty() { + let player = Player() + expectAtLeastEqualPublished( + values: [.unknown], + from: Self.itemStatusPublisher(for: player) + ) + } + + func testConsumedItemStatusLifeCycle() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + expectAtLeastEqualPublished( + values: [.unknown, .readyToPlay, .ended, .unknown], + from: Self.itemStatusPublisher(for: player) + ) { + player.play() + } + } + + func testPausedItemStatusLifeCycle() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + expectAtLeastEqualPublished( + values: [.unknown, .readyToPlay, .ended], + from: Self.itemStatusPublisher(for: player) + ) { + player.actionAtItemEnd = .pause + player.play() + } + expectAtLeastEqualPublishedNext( + values: [.readyToPlay], + from: Self.itemStatusPublisher(for: player) + ) { + player.seek(to: .zero) + } + } + + func testDurationEmpty() { + let player = Player() + expectAtLeastPublished( + values: [.invalid], + from: Self.durationPublisher(for: player), + to: beClose(within: 1) + ) + } + + func testDuration() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + expectAtLeastPublished( + values: [.invalid, Stream.shortOnDemand.duration, .invalid], + from: Self.durationPublisher(for: player), + to: beClose(within: 1) + ) { + player.play() + } + } + + func testSeekableTimeRangeEmpty() { + let player = Player() + expectAtLeastEqualPublished( + values: [.invalid], + from: Self.seekableTimeRangePublisher(for: player) + ) + } + + func testSeekableTimeRangeLifeCycle() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + expectAtLeastPublished( + values: [ + .invalid, + CMTimeRange(start: .zero, duration: Stream.shortOnDemand.duration), + .invalid + ], + from: Self.seekableTimeRangePublisher(for: player), + to: beClose(within: 1) + ) { + player.play() + } + } +} diff --git a/Tests/PlayerTests/QueuePlayer/QueuePlayerItemsTests.swift b/Tests/PlayerTests/QueuePlayer/QueuePlayerItemsTests.swift new file mode 100644 index 00000000..cc078414 --- /dev/null +++ b/Tests/PlayerTests/QueuePlayer/QueuePlayerItemsTests.swift @@ -0,0 +1,88 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class QueuePlayerItemsTests: TestCase { + func testReplaceItemsWithEmptyList() { + let item1 = AVPlayerItem(url: Stream.onDemand.url) + let item2 = AVPlayerItem(url: Stream.onDemand.url) + let item3 = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(items: [item1, item2, item3]) + player.replaceItems(with: []) + expect(player.items()).to(beEmpty()) + } + + func testReplaceItemsWhenEmpty() { + let player = QueuePlayer() + let item1 = AVPlayerItem(url: Stream.onDemand.url) + let item2 = AVPlayerItem(url: Stream.onDemand.url) + let item3 = AVPlayerItem(url: Stream.onDemand.url) + player.replaceItems(with: [item1, item2, item3]) + expect(player.items()).to(equalDiff([item1, item2, item3])) + } + + func testReplaceItemsWithOtherItems() { + let item1 = AVPlayerItem(url: Stream.onDemand.url) + let item2 = AVPlayerItem(url: Stream.onDemand.url) + let item3 = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(items: [item1, item2, item3]) + let item4 = AVPlayerItem(url: Stream.onDemand.url) + let item5 = AVPlayerItem(url: Stream.onDemand.url) + player.replaceItems(with: [item4, item5]) + expect(player.items()).to(equalDiff([item4, item5])) + } + + func testReplaceItemsWithPreservedCurrentItem() { + let item1 = AVPlayerItem(url: Stream.onDemand.url) + let item2 = AVPlayerItem(url: Stream.onDemand.url) + let item3 = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(items: [item1, item2, item3]) + let item4 = AVPlayerItem(url: Stream.onDemand.url) + player.replaceItems(with: [item1, item4]) + expect(player.items()).to(equalDiff([item1, item4])) + } + + func testReplaceItemsWithIdenticalItems() { + let item1 = AVPlayerItem(url: Stream.onDemand.url) + let item2 = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(items: [item1, item2]) + player.replaceItems(with: [item1, item2]) + expect(player.items()).to(equalDiff([item1, item2])) + } + + func testReplaceItemsWithNextItems() { + let item1 = AVPlayerItem(url: Stream.onDemand.url) + let item2 = AVPlayerItem(url: Stream.onDemand.url) + let item3 = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(items: [item1, item2, item3]) + player.replaceItems(with: [item2, item3]) + expect(player.items()).to(equalDiff([item2, item3])) + } + + func testReplaceItemsWithPreviousItems() { + let item2 = AVPlayerItem(url: Stream.onDemand.url) + let item3 = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(items: [item2, item3]) + let item1 = AVPlayerItem(url: Stream.onDemand.url) + player.replaceItems(with: [item1, item2, item3]) + expect(player.items()).to(equalDiff([item1, item2, item3])) + } + + func testReplaceItemsLastReplacementWins() { + let player = QueuePlayer() + let item1 = AVPlayerItem(url: Stream.onDemand.url) + let item2 = AVPlayerItem(url: Stream.onDemand.url) + player.replaceItems(with: [item1, item2]) + player.replaceItems(with: [item1]) + expect(player.items()).to(equalDiff([item1])) + } +} diff --git a/Tests/PlayerTests/QueuePlayer/QueuePlayerSeekTests.swift b/Tests/PlayerTests/QueuePlayer/QueuePlayerSeekTests.swift new file mode 100644 index 00000000..6e799634 --- /dev/null +++ b/Tests/PlayerTests/QueuePlayer/QueuePlayerSeekTests.swift @@ -0,0 +1,286 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble +import OrderedCollections +import PillarboxCircumspect +import PillarboxStreams + +private class QueuePlayerMock: QueuePlayer { + var seeks: Int = 0 + + override func enqueue(seek: Seek, completion: @escaping () -> Void) { + self.seeks += 1 + super.enqueue(seek: seek, completion: completion) + } +} + +final class QueuePlayerSeekTests: TestCase { + func testNotificationsForSeekWithInvalidTime() { + guard nimbleThrowAssertionsAvailable() else { return } + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + expect { player.seek(to: .invalid) }.to(throwAssertion()) + } + + func testNotificationsForSeekWithEmptyPlayer() { + let player = QueuePlayer() + expect { + player.seek(to: CMTime(value: 1, timescale: 1)) { finished in + expect(finished).to(beTrue()) + } + }.to(postNotifications(equalDiff([]), from: QueuePlayer.notificationCenter)) + } + + func testNotificationsForSeek() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + let time = CMTime(value: 1, timescale: 1) + expect { + player.seek(to: time) { finished in + expect(finished).to(beTrue()) + } + }.to(postNotifications(equalDiff([ + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time]), + Notification(name: .didSeek, object: player) + ]), from: QueuePlayer.notificationCenter)) + } + + func testNotificationsForMultipleSeeks() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + let time1 = CMTime(value: 1, timescale: 1) + let time2 = CMTime(value: 2, timescale: 1) + expect { + player.seek(to: time1) { finished in + expect(finished).to(beTrue()) + } + player.seek(to: time2) { finished in + expect(finished).to(beTrue()) + } + }.to(postNotifications(equalDiff([ + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time1]), + Notification(name: .didSeek, object: player), + + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time2]), + Notification(name: .didSeek, object: player) + ]), from: QueuePlayer.notificationCenter)) + } + + func testNotificationsForMultipleSeeksWithinTimeRange() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + expect(item.timeRange).toEventuallyNot(equal(.invalid)) + + let time1 = CMTime(value: 1, timescale: 1) + let time2 = CMTime(value: 2, timescale: 1) + expect { + player.seek(to: time1) { finished in + expect(finished).to(beFalse()) + } + player.seek(to: time2) { finished in + expect(finished).to(beTrue()) + } + }.toEventually(postNotifications(equalDiff([ + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time1]), + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time2]), + Notification(name: .didSeek, object: player) + ]), from: QueuePlayer.notificationCenter)) + } + + func testNotificationsForSeekAfterSmoothSeekWithinTimeRange() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + expect(item.timeRange).toEventuallyNot(equal(.invalid)) + + let time1 = CMTime(value: 1, timescale: 1) + let time2 = CMTime(value: 2, timescale: 1) + expect { + player.seek(to: time1, smooth: true) { finished in + expect(finished).to(beFalse()) + } + player.seek(to: time2) { finished in + expect(finished).to(beTrue()) + } + }.toEventually(postNotifications(equalDiff([ + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time1]), + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time2]), + Notification(name: .didSeek, object: player) + ]), from: QueuePlayer.notificationCenter)) + } + + func testCompletionsForMultipleSeeks() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + expect(item.timeRange).toEventuallyNot(equal(.invalid)) + + let time1 = CMTime(value: 1, timescale: 1) + let time2 = CMTime(value: 2, timescale: 1) + let time3 = CMTime(value: 3, timescale: 1) + + var results = OrderedDictionary() + + func completion(index: Int) -> ((Bool) -> Void) { + { finished in + expect(results[index]).to(beNil()) + results[index] = finished + } + } + + player.seek(to: time1, completionHandler: completion(index: 1)) + player.seek(to: time2, completionHandler: completion(index: 2)) + player.seek(to: time3, completionHandler: completion(index: 3)) + + expect(results).toEventually(equalDiff([ + 1: false, + 2: false, + 3: true + ])) + } + + func testCompletionsForMultipleSmoothSeeksEndingWithSeek() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + expect(item.timeRange).toEventuallyNot(equal(.invalid)) + + let time1 = CMTime(value: 1, timescale: 1) + let time2 = CMTime(value: 2, timescale: 1) + let time3 = CMTime(value: 3, timescale: 1) + + var results = OrderedDictionary() + + func completion(index: Int) -> ((Bool) -> Void) { + { finished in + expect(results[index]).to(beNil()) + results[index] = finished + } + } + + player.seek(to: time1, smooth: true, completionHandler: completion(index: 1)) + player.seek(to: time2, smooth: true, completionHandler: completion(index: 2)) + player.seek(to: time3, smooth: false, completionHandler: completion(index: 3)) + + expect(results).toEventually(equalDiff([ + 1: false, + 2: false, + 3: true + ])) + } + + // Checks that time is not jumping back when seeking forward several times in a row (no tolerance before is allowed + // in this test as otherwise the player is allowed to pick a position before the desired position), + func testMultipleSeekMonotonicity() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + player.play() + expect(item.timeRange).toEventuallyNot(equal(.invalid)) + + let values = collectOutput(from: player.smoothCurrentTimePublisher(interval: CMTime(value: 1, timescale: 10), queue: .main), during: .seconds(3)) { + player.seek(to: CMTime(value: 8, timescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) { _ in + player.seek(to: CMTime(value: 10, timescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) { _ in + player.seek(to: CMTime(value: 12, timescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) { _ in + player.seek(to: CMTime(value: 100, timescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) { _ in + player.seek(to: CMTime(value: 100, timescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) + } + } + } + } + } + expect(values.sorted()).to(equal(values)) + } + + func testNotificationCompletionOrder() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + expect(item.timeRange).toEventuallyNot(equal(.invalid)) + + let time = CMTime(value: 1, timescale: 1) + let notificationName = Notification.Name("SeekCompleted") + expect { + player.seek(to: time) { _ in + QueuePlayer.notificationCenter.post(name: notificationName, object: self) + } + }.toEventually(postNotifications(equalDiff([ + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time]), + Notification(name: .didSeek, object: player), + Notification(name: notificationName, object: self) + ]), from: QueuePlayer.notificationCenter)) + } + + func testNotificationCompletionOrderWithMultipleSeeks() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + expect(item.timeRange).toEventuallyNot(equal(.invalid)) + + let time1 = CMTime(value: 1, timescale: 1) + let time2 = CMTime(value: 2, timescale: 1) + let notificationName1 = Notification.Name("SeekCompleted1") + let notificationName2 = Notification.Name("SeekCompleted2") + expect { + player.seek(to: time1) { _ in + QueuePlayer.notificationCenter.post(name: notificationName1, object: self) + } + player.seek(to: time2) { _ in + QueuePlayer.notificationCenter.post(name: notificationName2, object: self) + } + }.toEventually(postNotifications(equalDiff([ + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time1]), + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time2]), + Notification(name: notificationName1, object: self), + Notification(name: .didSeek, object: player), + Notification(name: notificationName2, object: self) + ]), from: QueuePlayer.notificationCenter)) + } + + func testEnqueue() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayerMock(playerItem: item) + expect(player.timeRange).toEventuallyNot(equal(.invalid)) + waitUntil { done in + player.seek(to: CMTime(value: 1, timescale: 1)) + player.seek(to: CMTime(value: 2, timescale: 1)) + player.seek(to: CMTime(value: 3, timescale: 1)) + player.seek(to: CMTime(value: 4, timescale: 1)) + player.seek(to: CMTime(value: 5, timescale: 1)) { _ in + done() + } + } + expect(player.seeks).to(equal(5)) + } + + func testEnqueueSmooth() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayerMock(playerItem: item) + expect(player.timeRange).toEventuallyNot(equal(.invalid)) + waitUntil { done in + player.seek(to: CMTime(value: 1, timescale: 1), smooth: true) { _ in } + player.seek(to: CMTime(value: 2, timescale: 1), smooth: true) { _ in } + player.seek(to: CMTime(value: 3, timescale: 1), smooth: true) { _ in } + player.seek(to: CMTime(value: 4, timescale: 1), smooth: true) { _ in } + player.seek(to: CMTime(value: 5, timescale: 1), smooth: true) { _ in + done() + } + } + expect(player.seeks).to(equal(2)) + } + + func testTargetSeekTimeWithMultipleSeeks() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + expect(player.timeRange).toEventuallyNot(equal(.invalid)) + + let time1 = CMTime(value: 1, timescale: 1) + player.seek(to: time1) + expect(player.targetSeekTime).to(equal(time1)) + + let time2 = CMTime(value: 2, timescale: 1) + player.seek(to: time2) + expect(player.targetSeekTime).to(equal(time2)) + } +} diff --git a/Tests/PlayerTests/QueuePlayer/QueuePlayerSeekTimePublisherTests.swift b/Tests/PlayerTests/QueuePlayer/QueuePlayerSeekTimePublisherTests.swift new file mode 100644 index 00000000..427cb1c5 --- /dev/null +++ b/Tests/PlayerTests/QueuePlayer/QueuePlayerSeekTimePublisherTests.swift @@ -0,0 +1,94 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class QueuePlayerSeekTimePublisherTests: TestCase { + func testEmpty() { + let player = QueuePlayer() + expectAtLeastEqualPublished( + values: [nil], + from: player.seekTimePublisher() + ) + } + + func testSeek() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + let time = CMTime(value: 1, timescale: 1) + expectAtLeastEqualPublished( + values: [nil, time, nil], + from: player.seekTimePublisher() + ) { + player.seek(to: time) + } + } + + func testMultipleSeek() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + let time1 = CMTime(value: 1, timescale: 1) + let time2 = CMTime(value: 2, timescale: 1) + expectAtLeastEqualPublished( + values: [nil, time1, nil, time2, nil], + from: player.seekTimePublisher() + ) { + player.seek(to: time1) + player.seek(to: time2) + } + } + + func testMultipleSeeksAtTheSameLocation() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + let time = CMTime(value: 1, timescale: 1) + expectAtLeastEqualPublished( + values: [nil, time, nil, time, nil], + from: player.seekTimePublisher() + ) { + player.seek(to: time) + player.seek(to: time) + } + } + + func testMultipleSeeksWithinTimeRange() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + player.play() + expect(item.timeRange).toEventuallyNot(equal(.invalid)) + + let time1 = CMTime(value: 1, timescale: 1) + let time2 = CMTime(value: 2, timescale: 1) + expectAtLeastEqualPublished( + values: [nil, time1, time2, nil], + from: player.seekTimePublisher() + ) { + player.seek(to: time1) + player.seek(to: time2) + } + } + + func testMultipleSeeksAtTheSameLocationWithinTimeRange() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + player.play() + expect(item.timeRange).toEventuallyNot(equal(.invalid)) + + let time = CMTime(value: 1, timescale: 1) + expectAtLeastEqualPublished( + values: [nil, time, nil], + from: player.seekTimePublisher() + ) { + player.seek(to: time) + player.seek(to: time) + } + } +} diff --git a/Tests/PlayerTests/QueuePlayer/QueuePlayerSmoothSeekTests.swift b/Tests/PlayerTests/QueuePlayer/QueuePlayerSmoothSeekTests.swift new file mode 100644 index 00000000..c775b03e --- /dev/null +++ b/Tests/PlayerTests/QueuePlayer/QueuePlayerSmoothSeekTests.swift @@ -0,0 +1,173 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Nimble +import OrderedCollections +import PillarboxCircumspect +import PillarboxStreams + +final class QueuePlayerSmoothSeekTests: TestCase { + func testNotificationsForSeekWithEmptyPlayer() { + let player = QueuePlayer() + expect { + player.seek(to: CMTime(value: 1, timescale: 1), smooth: true) { finished in + expect(finished).to(beTrue()) + } + }.to(postNotifications(equalDiff([]), from: QueuePlayer.notificationCenter)) + } + + func testNotificationsForSeek() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + let time = CMTime(value: 1, timescale: 1) + expect { + player.seek(to: time, smooth: true) { finished in + expect(finished).to(beTrue()) + } + }.to(postNotifications(equalDiff([ + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time]), + Notification(name: .didSeek, object: player) + ]), from: QueuePlayer.notificationCenter)) + } + + func testNotificationsForMultipleSeeks() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + let time1 = CMTime(value: 1, timescale: 1) + let time2 = CMTime(value: 2, timescale: 1) + expect { + player.seek(to: time1, smooth: true) { finished in + expect(finished).to(beTrue()) + } + player.seek(to: time2, smooth: true) { finished in + expect(finished).to(beTrue()) + } + }.to(postNotifications(equalDiff([ + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time1]), + Notification(name: .didSeek, object: player), + + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time2]), + Notification(name: .didSeek, object: player) + ]), from: QueuePlayer.notificationCenter)) + } + + func testNotificationsForMultipleSeeksWithinTimeRange() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + expect(item.timeRange).toEventuallyNot(equal(.invalid)) + + let time1 = CMTime(value: 1, timescale: 1) + let time2 = CMTime(value: 2, timescale: 1) + expect { + player.seek(to: time1, smooth: true) { finished in + expect(finished).to(beTrue()) + } + player.seek(to: time2, smooth: true) { finished in + expect(finished).to(beTrue()) + } + }.toEventually(postNotifications(equalDiff([ + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time1]), + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time2]), + Notification(name: .didSeek, object: player) + ]), from: QueuePlayer.notificationCenter)) + } + + func testNotificationsForSmoothSeekAfterSeekWithinTimeRange() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + expect(item.timeRange).toEventuallyNot(equal(.invalid)) + + let time1 = CMTime(value: 1, timescale: 1) + let time2 = CMTime(value: 2, timescale: 1) + expect { + player.seek(to: time1) { finished in + expect(finished).to(beTrue()) + } + player.seek(to: time2, smooth: true) { finished in + expect(finished).to(beTrue()) + } + }.toEventually(postNotifications(equalDiff([ + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time1]), + Notification(name: .willSeek, object: player, userInfo: [SeekKey.time: time2]), + Notification(name: .didSeek, object: player) + ]), from: QueuePlayer.notificationCenter)) + } + + func testCompletionsForMultipleSeeks() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + expect(item.timeRange).toEventuallyNot(equal(.invalid)) + + let time1 = CMTime(value: 1, timescale: 1) + let time2 = CMTime(value: 2, timescale: 1) + let time3 = CMTime(value: 3, timescale: 1) + + var results = OrderedDictionary() + + func completion(index: Int) -> ((Bool) -> Void) { + { finished in + expect(results[index]).to(beNil()) + results[index] = finished + } + } + + player.seek(to: time1, smooth: true, completionHandler: completion(index: 1)) + player.seek(to: time2, smooth: true, completionHandler: completion(index: 2)) + player.seek(to: time3, smooth: true, completionHandler: completion(index: 3)) + + expect(results).toEventually(equalDiff([ + 1: true, + 2: true, + 3: true + ])) + } + + func testCompletionsForMultipleSeeksEndingWithSmoothSeek() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + expect(item.timeRange).toEventuallyNot(equal(.invalid)) + + let time1 = CMTime(value: 1, timescale: 1) + let time2 = CMTime(value: 2, timescale: 1) + let time3 = CMTime(value: 3, timescale: 1) + + var results = OrderedDictionary() + + func completion(index: Int) -> ((Bool) -> Void) { + { finished in + expect(results[index]).to(beNil()) + results[index] = finished + } + } + + player.seek(to: time1, smooth: false, completionHandler: completion(index: 1)) + player.seek(to: time2, smooth: false, completionHandler: completion(index: 2)) + player.seek(to: time3, smooth: true, completionHandler: completion(index: 3)) + + expect(results).toEventually(equalDiff([ + 1: false, + 2: true, + 3: true + ])) + } + + func testTargetSeekTimeWithMultipleSeeks() { + let item = AVPlayerItem(url: Stream.onDemand.url) + let player = QueuePlayer(playerItem: item) + expect(player.timeRange).toEventuallyNot(equal(.invalid)) + + let time1 = CMTime(value: 1, timescale: 1) + player.seek(to: time1, smooth: true) { _ in } + expect(player.targetSeekTime).to(equal(time1)) + + let time2 = CMTime(value: 2, timescale: 1) + player.seek(to: time2, smooth: true) { _ in } + expect(player.targetSeekTime).to(equal(time2)) + } +} diff --git a/Tests/PlayerTests/Resources/invalid.jpg b/Tests/PlayerTests/Resources/invalid.jpg new file mode 100644 index 00000000..e69de29b diff --git a/Tests/PlayerTests/Resources/pixel.jpg b/Tests/PlayerTests/Resources/pixel.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d07bd67dd7ff3b18ec990a01c72726844270b086 GIT binary patch literal 373 zcmex=0PFu-3>+Z0Gca(26hN3rO4z^(m;``AmRJ?AgB37?6l8-G2%uZR0JVbAo}nJ3 RKoGkECPo4Zm>~*o0su)5Gfn^i literal 0 HcmV?d00001 diff --git a/Tests/PlayerTests/Skips/SkipBackwardChecksTests.swift b/Tests/PlayerTests/Skips/SkipBackwardChecksTests.swift new file mode 100644 index 00000000..dede8231 --- /dev/null +++ b/Tests/PlayerTests/Skips/SkipBackwardChecksTests.swift @@ -0,0 +1,35 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class SkipBackwardChecksTests: TestCase { + func testCannotSkipWhenEmpty() { + let player = Player() + expect(player.canSkipBackward()).to(beFalse()) + } + + func testCanSkipForOnDemand() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.canSkipBackward()).to(beTrue()) + } + + func testCannotSkipForLive() { + let player = Player(item: .simple(url: Stream.live.url)) + expect(player.streamType).toEventually(equal(.live)) + expect(player.canSkipBackward()).to(beFalse()) + } + + func testCanSkipForDvr() { + let player = Player(item: .simple(url: Stream.dvr.url)) + expect(player.streamType).toEventually(equal(.dvr)) + expect(player.canSkipBackward()).to(beTrue()) + } +} diff --git a/Tests/PlayerTests/Skips/SkipBackwardTests.swift b/Tests/PlayerTests/Skips/SkipBackwardTests.swift new file mode 100644 index 00000000..d46555db --- /dev/null +++ b/Tests/PlayerTests/Skips/SkipBackwardTests.swift @@ -0,0 +1,79 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class SkipBackwardTests: TestCase { + func testSkipWhenEmpty() { + let player = Player() + waitUntil { done in + player.skipBackward { finished in + expect(finished).to(beTrue()) + done() + } + } + } + + func testSkipForOnDemand() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.time()).to(equal(.zero)) + + waitUntil { done in + player.skipBackward { _ in + expect(player.time()).to(equal(.zero)) + done() + } + } + } + + func testMultipleSkipsForOnDemand() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.time()).to(equal(.zero)) + + waitUntil { done in + player.skipBackward { finished in + expect(finished).to(beFalse()) + } + + player.skipBackward { finished in + expect(player.time()).to(equal(.zero)) + expect(finished).to(beTrue()) + done() + } + } + } + + func testSkipForLive() { + let player = Player(item: .simple(url: Stream.live.url)) + expect(player.streamType).toEventually(equal(.live)) + waitUntil { done in + player.skipBackward { finished in + expect(finished).to(beTrue()) + done() + } + } + } + + func testSkipForDvr() { + let player = Player(item: .simple(url: Stream.dvr.url)) + expect(player.streamType).toEventually(equal(.dvr)) + let headTime = player.time() + waitUntil { done in + player.skipBackward { finished in + expect(player.time()).to(equal(headTime + player.backwardSkipTime, by: beClose(within: player.chunkDuration.seconds))) + expect(finished).to(beTrue()) + done() + } + } + } +} diff --git a/Tests/PlayerTests/Skips/SkipForwardChecksTests.swift b/Tests/PlayerTests/Skips/SkipForwardChecksTests.swift new file mode 100644 index 00000000..45a8bc82 --- /dev/null +++ b/Tests/PlayerTests/Skips/SkipForwardChecksTests.swift @@ -0,0 +1,35 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class SkipForwardChecksTests: TestCase { + func testCannotSkipWhenEmpty() { + let player = Player() + expect(player.canSkipForward()).to(beFalse()) + } + + func testCanSkipForOnDemand() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.canSkipForward()).to(beTrue()) + } + + func testCannotSkipForLive() { + let player = Player(item: .simple(url: Stream.live.url)) + expect(player.streamType).toEventually(equal(.live)) + expect(player.canSkipForward()).to(beFalse()) + } + + func testCannotSkipForDvr() { + let player = Player(item: .simple(url: Stream.dvr.url)) + expect(player.streamType).toEventually(equal(.dvr)) + expect(player.canSkipForward()).to(beFalse()) + } +} diff --git a/Tests/PlayerTests/Skips/SkipForwardTests.swift b/Tests/PlayerTests/Skips/SkipForwardTests.swift new file mode 100644 index 00000000..94b48028 --- /dev/null +++ b/Tests/PlayerTests/Skips/SkipForwardTests.swift @@ -0,0 +1,126 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Combine +import CoreMedia +import Nimble +import PillarboxCircumspect +import PillarboxStreams + +final class SkipForwardTests: TestCase { + private func isSeekingPublisher(for player: Player) -> AnyPublisher { + player.propertiesPublisher + .slice(at: \.isSeeking) + .eraseToAnyPublisher() + } + + func testSkipWhenEmpty() { + let player = Player() + waitUntil { done in + player.skipForward { finished in + expect(finished).to(beTrue()) + done() + } + } + } + + func testSkipForOnDemand() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.time()).to(equal(.zero)) + + waitUntil { done in + player.skipForward { _ in + expect(player.time()).to(equal(player.forwardSkipTime, by: beClose(within: player.chunkDuration.seconds))) + done() + } + } + } + + func testMultipleSkipsForOnDemand() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.time()).to(equal(.zero)) + + waitUntil { done in + player.skipForward { finished in + expect(finished).to(beFalse()) + } + + player.skipForward { finished in + expect(player.time()).to(equal(CMTimeMultiply(player.forwardSkipTime, multiplier: 2), by: beClose(within: player.chunkDuration.seconds))) + expect(finished).to(beTrue()) + done() + } + } + } + + func testSkipForLive() { + let player = Player(item: .simple(url: Stream.live.url)) + expect(player.streamType).toEventually(equal(.live)) + waitUntil { done in + player.skipForward { finished in + expect(finished).to(beTrue()) + done() + } + } + } + + func testSkipForDvr() { + let player = Player(item: .simple(url: Stream.dvr.url)) + expect(player.streamType).toEventually(equal(.dvr)) + let headTime = player.time() + waitUntil { done in + player.skipForward { finished in + expect(player.time()).to(equal(headTime, by: beClose(within: player.chunkDuration.seconds))) + expect(finished).to(beTrue()) + done() + } + } + } + + func testSkipNearEndDoesNotSeekAnymore() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.time()).to(equal(.zero)) + let seekTo = Stream.onDemand.duration - CMTime(value: 1, timescale: 1) + + waitUntil { done in + player.seek(at(seekTo)) { finished in + expect(finished).to(beTrue()) + done() + } + } + + expectNothingPublishedNext(from: isSeekingPublisher(for: player), during: .seconds(2)) { + player.skipForward() + } + } + + func testSkipNearEndCompletion() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.time()).to(equal(.zero)) + let seekTo = Stream.onDemand.duration - CMTime(value: 1, timescale: 1) + + waitUntil { done in + player.seek(at(seekTo)) { finished in + expect(finished).to(beTrue()) + done() + } + } + + waitUntil { done in + player.skipForward { finished in + expect(finished).to(beTrue()) + expect(player.time()).to(equal(seekTo, by: beClose(within: 0.5))) + done() + } + } + } +} diff --git a/Tests/PlayerTests/Skips/SkipToDefaultChecksTests.swift b/Tests/PlayerTests/Skips/SkipToDefaultChecksTests.swift new file mode 100644 index 00000000..f2314685 --- /dev/null +++ b/Tests/PlayerTests/Skips/SkipToDefaultChecksTests.swift @@ -0,0 +1,55 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble +import PillarboxStreams + +final class SkipToDefaultChecksTests: TestCase { + func testCannotSkipWhenEmpty() { + let player = Player() + expect(player.canSkipToDefault()).to(beFalse()) + } + + func testCannotSkipForUnknown() { + let player = Player(item: .simple(url: Stream.unavailable.url)) + expect(player.streamType).toEventually(equal(.unknown)) + expect(player.canSkipToDefault()).to(beFalse()) + } + + func testCanSkipForOnDemand() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + expect(player.canSkipToDefault()).to(beTrue()) + } + + func testCannotSkipForDvrInLiveConditions() { + let player = Player(item: .simple(url: Stream.dvr.url)) + expect(player.streamType).toEventually(equal(.dvr)) + expect(player.canSkipToDefault()).to(beFalse()) + } + + func testCanSkipForDvrInPastConditions() { + let player = Player(item: .simple(url: Stream.dvr.url)) + expect(player.streamType).toEventually(equal(.dvr)) + + waitUntil { done in + player.seek(at(CMTime(value: 1, timescale: 1))) { _ in + done() + } + } + + expect(player.canSkipToDefault()).to(beTrue()) + } + + func testCanSkipForLive() { + let player = Player(item: .simple(url: Stream.live.url)) + expect(player.streamType).toEventually(equal(.live)) + expect(player.canSkipToDefault()).to(beTrue()) + } +} diff --git a/Tests/PlayerTests/Skips/SkipToDefaultTests.swift b/Tests/PlayerTests/Skips/SkipToDefaultTests.swift new file mode 100644 index 00000000..225a9c2a --- /dev/null +++ b/Tests/PlayerTests/Skips/SkipToDefaultTests.swift @@ -0,0 +1,91 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble +import PillarboxStreams + +final class SkipToDefaultTests: TestCase { + func testSkipWhenEmpty() { + let player = Player() + waitUntil { done in + player.skipToDefault { finished in + expect(finished).to(beTrue()) + expect(player.time()).to(equal(.invalid)) + done() + } + } + } + + func testSkipForUnknown() { + let player = Player(item: .simple(url: Stream.unavailable.url)) + expect(player.streamType).toEventually(equal(.unknown)) + waitUntil { done in + player.skipToDefault { finished in + expect(finished).to(beTrue()) + expect(player.time()).to(equal(.zero)) + done() + } + } + } + + func testSkipForOnDemand() { + let player = Player(item: .simple(url: Stream.onDemand.url)) + expect(player.streamType).toEventually(equal(.onDemand)) + waitUntil { done in + player.skipToDefault { finished in + expect(finished).to(beTrue()) + expect(player.time()).to(equal(.zero)) + done() + } + } + } + + func testSkipForLive() { + let player = Player(item: .simple(url: Stream.live.url)) + expect(player.streamType).toEventually(equal(.live)) + waitUntil { done in + player.skipToDefault { finished in + expect(finished).to(beTrue()) + done() + } + } + } + + func testSkipForDvrInLiveConditions() { + let item = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.dvr)) + waitUntil { done in + player.skipToDefault { finished in + expect(finished).to(beTrue()) + done() + } + } + } + + func testSkipForDvrInPastConditions() { + let item = PlayerItem.simple(url: Stream.dvr.url) + let player = Player(item: item) + expect(player.streamType).toEventually(equal(.dvr)) + + waitUntil { done in + player.seek(at(CMTime(value: 1, timescale: 1))) { finished in + expect(finished).to(beTrue()) + done() + } + } + + waitUntil { done in + player.skipToDefault { finished in + expect(finished).to(beTrue()) + done() + } + } + } +} diff --git a/Tests/PlayerTests/Tools/AVMediaSelectionOptionMock.swift b/Tests/PlayerTests/Tools/AVMediaSelectionOptionMock.swift new file mode 100644 index 00000000..9fd618cb --- /dev/null +++ b/Tests/PlayerTests/Tools/AVMediaSelectionOptionMock.swift @@ -0,0 +1,32 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import AVFoundation + +class AVMediaSelectionOptionMock: AVMediaSelectionOption { + override var displayName: String { + _displayName + } + + override var locale: Locale? { + _locale + } + + private let _displayName: String + private let _locale: Locale + private let _characteristics: [AVMediaCharacteristic] + + init(displayName: String, languageCode: String = "", characteristics: [AVMediaCharacteristic] = []) { + _displayName = displayName + _locale = Locale(identifier: languageCode) + _characteristics = characteristics + super.init() + } + + override func hasMediaCharacteristic(_ mediaCharacteristic: AVMediaCharacteristic) -> Bool { + _characteristics.contains(mediaCharacteristic) + } +} diff --git a/Tests/PlayerTests/Tools/ContentKeySessionDelegateMock.swift b/Tests/PlayerTests/Tools/ContentKeySessionDelegateMock.swift new file mode 100644 index 00000000..9609cc0f --- /dev/null +++ b/Tests/PlayerTests/Tools/ContentKeySessionDelegateMock.swift @@ -0,0 +1,11 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import AVFoundation + +final class ContentKeySessionDelegateMock: NSObject, AVContentKeySessionDelegate { + func contentKeySession(_ session: AVContentKeySession, didProvide keyRequest: AVContentKeyRequest) {} +} diff --git a/Tests/PlayerTests/Tools/LanguageIdentifiable.swift b/Tests/PlayerTests/Tools/LanguageIdentifiable.swift new file mode 100644 index 00000000..3e3210a6 --- /dev/null +++ b/Tests/PlayerTests/Tools/LanguageIdentifiable.swift @@ -0,0 +1,29 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import AVFoundation +import PillarboxPlayer + +protocol LanguageIdentifiable { + var languageIdentifier: String? { get } +} + +extension MediaSelectionOption: LanguageIdentifiable { + var languageIdentifier: String? { + switch self { + case let .on(option): + return option.languageIdentifier + default: + return nil + } + } +} + +extension AVMediaSelectionOption: LanguageIdentifiable { + var languageIdentifier: String? { + locale?.identifier + } +} diff --git a/Tests/PlayerTests/Tools/Matchers.swift b/Tests/PlayerTests/Tools/Matchers.swift new file mode 100644 index 00000000..af46caf0 --- /dev/null +++ b/Tests/PlayerTests/Tools/Matchers.swift @@ -0,0 +1,19 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import Nimble + +/// A Nimble matcher that checks language identifiers. +func haveLanguageIdentifier(_ identifier: String) -> Matcher where T: LanguageIdentifiable { + let message = "have language identifier \(identifier)" + return .define { actualExpression in + let actualIdentifier = try actualExpression.evaluate()?.languageIdentifier + return MatcherResult( + bool: actualIdentifier == identifier, + message: .expectedCustomValueTo(message, actual: actualIdentifier ?? "nil") + ) + } +} diff --git a/Tests/PlayerTests/Tools/MediaAccessibilityDisplayType.swift b/Tests/PlayerTests/Tools/MediaAccessibilityDisplayType.swift new file mode 100644 index 00000000..e7ea68bf --- /dev/null +++ b/Tests/PlayerTests/Tools/MediaAccessibilityDisplayType.swift @@ -0,0 +1,25 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import MediaAccessibility + +enum MediaAccessibilityDisplayType { + case automatic + case forcedOnly + case alwaysOn(languageCode: String) + + func apply() { + switch self { + case .automatic: + MACaptionAppearanceSetDisplayType(.user, .automatic) + case .forcedOnly: + MACaptionAppearanceSetDisplayType(.user, .forcedOnly) + case let .alwaysOn(languageCode: languageCode): + MACaptionAppearanceSetDisplayType(.user, .alwaysOn) + MACaptionAppearanceAddSelectedLanguage(.user, languageCode as CFString) + } + } +} diff --git a/Tests/PlayerTests/Tools/PlayerItem.swift b/Tests/PlayerTests/Tools/PlayerItem.swift new file mode 100644 index 00000000..9d743b31 --- /dev/null +++ b/Tests/PlayerTests/Tools/PlayerItem.swift @@ -0,0 +1,79 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Combine +import Foundation +import PillarboxStreams + +enum MediaMock: String { + case media1 + case media2 +} + +extension PlayerItem { + static func mock( + url: URL, + loadedAfter delay: TimeInterval, + trackerAdapters: [TrackerAdapter] = [] + ) -> Self { + let publisher = Just(Asset.simple(url: url)) + .delayIfNeeded(for: .seconds(delay), scheduler: DispatchQueue.main) + return .init(publisher: publisher, trackerAdapters: trackerAdapters) + } + + static func mock( + url: URL, + loadedAfter delay: TimeInterval, + withMetadata: AssetMetadataMock, + trackerAdapters: [TrackerAdapter] = [] + ) -> Self { + let publisher = Just(Asset.simple(url: url, metadata: withMetadata)) + .delayIfNeeded(for: .seconds(delay), scheduler: DispatchQueue.main) + return .init(publisher: publisher, trackerAdapters: trackerAdapters) + } + + static func mock( + url: URL, + withMetadataUpdateAfter delay: TimeInterval, + trackerAdapters: [TrackerAdapter] = [] + ) -> Self { + let publisher = Just(Asset.simple( + url: url, + metadata: AssetMetadataMock(title: "title1", subtitle: "subtitle1") + )) + .delayIfNeeded(for: .seconds(delay), scheduler: DispatchQueue.main) + .prepend(Asset.simple( + url: url, + metadata: AssetMetadataMock(title: "title0", subtitle: "subtitle0") + )) + return .init(publisher: publisher, trackerAdapters: trackerAdapters) + } + + static func webServiceMock(media: MediaMock, trackerAdapters: [TrackerAdapter] = []) -> Self { + let url = URL(string: "http://localhost:8123/json/\(media).json")! + return webServiceMock(url: url, trackerAdapters: trackerAdapters) + } + + static func failing( + loadedAfter delay: TimeInterval, + trackerAdapters: [TrackerAdapter] = [] + ) -> Self { + let url = URL(string: "http://localhost:8123/missing.json")! + return webServiceMock(url: url, trackerAdapters: trackerAdapters) + } + + private static func webServiceMock(url: URL, trackerAdapters: [TrackerAdapter]) -> Self { + let publisher = URLSession(configuration: .default).dataTaskPublisher(for: url) + .map(\.data) + .decode(type: AssetMetadataMock.self, decoder: JSONDecoder()) + .map { metadata in + Asset.simple(url: Stream.onDemand.url, metadata: metadata) + } + return .init(publisher: publisher, trackerAdapters: trackerAdapters) + } +} diff --git a/Tests/PlayerTests/Tools/PlayerItemTrackerMock.swift b/Tests/PlayerTests/Tools/PlayerItemTrackerMock.swift new file mode 100644 index 00000000..ea5461ac --- /dev/null +++ b/Tests/PlayerTests/Tools/PlayerItemTrackerMock.swift @@ -0,0 +1,69 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Combine + +final class PlayerItemTrackerMock: PlayerItemTracker { + typealias StatePublisher = PassthroughSubject + + enum State: Equatable { + case initialized + case enabled + case metricEvents + case disabled + case deinitialized + } + + struct Configuration { + let statePublisher: StatePublisher + let sessionIdentifier: String? + + init(statePublisher: StatePublisher = .init(), sessionIdentifier: String? = nil) { + self.statePublisher = statePublisher + self.sessionIdentifier = sessionIdentifier + } + } + + private let configuration: Configuration + + var sessionIdentifier: String? { + configuration.sessionIdentifier + } + + init(configuration: Configuration) { + self.configuration = configuration + configuration.statePublisher.send(.initialized) + } + + func updateMetadata(to metadata: Void) {} + + func updateProperties(to properties: PlayerProperties) {} + + func updateMetricEvents(to events: [MetricEvent]) { + configuration.statePublisher.send(.metricEvents) + } + + func enable(for player: AVPlayer) { + configuration.statePublisher.send(.enabled) + } + + func disable(with properties: PlayerProperties) { + configuration.statePublisher.send(.disabled) + } + + deinit { + configuration.statePublisher.send(.deinitialized) + } +} + +extension PlayerItemTrackerMock { + static func adapter(statePublisher: StatePublisher, behavior: TrackingBehavior = .optional) -> TrackerAdapter { + adapter(configuration: Configuration(statePublisher: statePublisher), behavior: behavior) + } +} diff --git a/Tests/PlayerTests/Tools/ResourceLoaderDelegateMock.swift b/Tests/PlayerTests/Tools/ResourceLoaderDelegateMock.swift new file mode 100644 index 00000000..6991f650 --- /dev/null +++ b/Tests/PlayerTests/Tools/ResourceLoaderDelegateMock.swift @@ -0,0 +1,24 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import AVFoundation + +final class ResourceLoaderDelegateMock: NSObject, AVAssetResourceLoaderDelegate { + func resourceLoader( + _ resourceLoader: AVAssetResourceLoader, + shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest + ) -> Bool { + true + } + + func resourceLoader( + _ resourceLoader: AVAssetResourceLoader, + shouldWaitForRenewalOfRequestedResource renewalRequest: AVAssetResourceRenewalRequest + ) -> Bool { + renewalRequest.finishLoading() + return true + } +} diff --git a/Tests/PlayerTests/Tools/Similarity.swift b/Tests/PlayerTests/Tools/Similarity.swift new file mode 100644 index 00000000..8b96596e --- /dev/null +++ b/Tests/PlayerTests/Tools/Similarity.swift @@ -0,0 +1,78 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import PillarboxCircumspect +import UIKit + +extension ImageSource: Similar { + public static func ~~ (lhs: ImageSource, rhs: ImageSource) -> Bool { + switch (lhs.kind, rhs.kind) { + case (.none, .none): + return true + case let ( + .url( + standardResolution: lhsStandardResolutionUrl, + lowResolution: lhsLowResolutionUrl + ), + .url( + standardResolution: rhsStandardResolutionUrl, + lowResolution: rhsLowResolutionUrl + ) + ): + return lhsStandardResolutionUrl == rhsStandardResolutionUrl && lhsLowResolutionUrl == rhsLowResolutionUrl + case let (.image(lhsImage), .image(rhsImage)): + return lhsImage.pngData() == rhsImage.pngData() + default: + return false + } + } +} + +extension Resource: Similar { + public static func ~~ (lhs: PillarboxPlayer.Resource, rhs: PillarboxPlayer.Resource) -> Bool { + switch (lhs, rhs) { + case let (.simple(url: lhsUrl), .simple(url: rhsUrl)), + let (.custom(url: lhsUrl, delegate: _), .custom(url: rhsUrl, delegate: _)), + let (.encrypted(url: lhsUrl, delegate: _), .encrypted(url: rhsUrl, delegate: _)): + return lhsUrl == rhsUrl + default: + return false + } + } +} + +extension NowPlaying.Info: Similar { + public static func ~~ (lhs: Self, rhs: Self) -> Bool { + // swiftlint:disable:next legacy_objc_type + NSDictionary(dictionary: lhs).isEqual(to: rhs) + } +} + +extension MetricEvent: Similar { + public static func ~~ (lhs: MetricEvent, rhs: MetricEvent) -> Bool { + switch (lhs.kind, rhs.kind) { + case (.metadata, .metadata), (.asset, .asset), (.failure, .failure), (.warning, .warning): + return true + default: + return false + } + } +} + +func beClose(within tolerance: TimeInterval) -> ((CMTime, CMTime) -> Bool) { + CMTime.close(within: tolerance) +} + +func beClose(within tolerance: TimeInterval) -> ((CMTime?, CMTime?) -> Bool) { + CMTime.close(within: tolerance) +} + +func beClose(within tolerance: TimeInterval) -> ((CMTimeRange, CMTimeRange) -> Bool) { + CMTimeRange.close(within: tolerance) +} diff --git a/Tests/PlayerTests/Tools/TestCase.swift b/Tests/PlayerTests/Tools/TestCase.swift new file mode 100644 index 00000000..91a0702f --- /dev/null +++ b/Tests/PlayerTests/Tools/TestCase.swift @@ -0,0 +1,22 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import Nimble +import XCTest + +/// A simple test suite with more tolerant Nimble settings. Beware that `toAlways` and `toNever` expectations appearing +/// in tests will use the same value by default and should likely always provide an explicit `until` parameter. +class TestCase: XCTestCase { + override class func setUp() { + PollingDefaults.timeout = .seconds(20) + PollingDefaults.pollInterval = .milliseconds(100) + } + + override class func tearDown() { + PollingDefaults.timeout = .seconds(1) + PollingDefaults.pollInterval = .milliseconds(10) + } +} diff --git a/Tests/PlayerTests/Tools/Tools.swift b/Tests/PlayerTests/Tools/Tools.swift new file mode 100644 index 00000000..338e6804 --- /dev/null +++ b/Tests/PlayerTests/Tools/Tools.swift @@ -0,0 +1,13 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import Foundation + +struct StructError: LocalizedError { + var errorDescription: String? { + "Struct error description" + } +} diff --git a/Tests/PlayerTests/Tools/TrackerUpdateMock.swift b/Tests/PlayerTests/Tools/TrackerUpdateMock.swift new file mode 100644 index 00000000..12ec1754 --- /dev/null +++ b/Tests/PlayerTests/Tools/TrackerUpdateMock.swift @@ -0,0 +1,55 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import Combine + +final class TrackerUpdateMock: PlayerItemTracker where Metadata: Equatable { + typealias StatePublisher = PassthroughSubject + + enum State: Equatable { + case enabled + case disabled + case updatedMetadata(Metadata) + case updatedProperties + } + + struct Configuration { + let statePublisher: StatePublisher + } + + private let configuration: Configuration + + init(configuration: Configuration) { + self.configuration = configuration + } + + func enable(for player: AVPlayer) { + configuration.statePublisher.send(.enabled) + } + + func updateMetadata(to metadata: Metadata) { + configuration.statePublisher.send(.updatedMetadata(metadata)) + } + + func updateProperties(to properties: PlayerProperties) { + configuration.statePublisher.send(.updatedProperties) + } + + func updateMetricEvents(to events: [MetricEvent]) {} + + func disable(with properties: PlayerProperties) { + configuration.statePublisher.send(.disabled) + } +} + +extension TrackerUpdateMock { + static func adapter(statePublisher: StatePublisher, mapper: @escaping (M) -> Metadata) -> TrackerAdapter { + adapter(configuration: Configuration(statePublisher: statePublisher), mapper: mapper) + } +} diff --git a/Tests/PlayerTests/Tracking/PlayerItemTrackerLifeCycleTests.swift b/Tests/PlayerTests/Tracking/PlayerItemTrackerLifeCycleTests.swift new file mode 100644 index 00000000..1a3939b0 --- /dev/null +++ b/Tests/PlayerTests/Tracking/PlayerItemTrackerLifeCycleTests.swift @@ -0,0 +1,98 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import PillarboxCircumspect +import PillarboxStreams + +final class PlayerItemTrackerLifeCycleTests: TestCase { + func testWithShortLivedPlayer() { + let publisher = PlayerItemTrackerMock.StatePublisher() + expectAtLeastEqualPublished(values: [.initialized, .deinitialized], from: publisher) { + _ = PlayerItem.simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [PlayerItemTrackerMock.adapter(statePublisher: publisher)] + ) + } + } + + func testItemPlayback() { + let player = Player() + let publisher = PlayerItemTrackerMock.StatePublisher() + expectEqualPublished(values: [.initialized, .enabled, .metricEvents, .metricEvents], from: publisher, during: .seconds(2)) { + player.append(.simple( + url: Stream.onDemand.url, + trackerAdapters: [PlayerItemTrackerMock.adapter(statePublisher: publisher)] + )) + player.play() + } + } + + func testItemEntirePlayback() { + let player = Player() + let publisher = PlayerItemTrackerMock.StatePublisher() + expectAtLeastEqualPublished(values: [.initialized, .enabled, .metricEvents, .metricEvents, .disabled], from: publisher) { + player.append(.simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [PlayerItemTrackerMock.adapter(statePublisher: publisher)] + )) + player.play() + } + } + + func testDisableDuringDeinitPlayer() { + var player: Player? = Player() + let publisher = PlayerItemTrackerMock.StatePublisher() + expectAtLeastEqualPublished(values: [.initialized, .enabled, .disabled], from: publisher) { + player?.append(.simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [PlayerItemTrackerMock.adapter(statePublisher: publisher)] + )) + player = nil + } + } + + func testNetworkLoadedItemEntirePlayback() { + let player = Player() + let publisher = PlayerItemTrackerMock.StatePublisher() + expectAtLeastEqualPublished(values: [.initialized, .enabled, .metricEvents, .metricEvents, .disabled], from: publisher) { + player.append(.mock( + url: Stream.shortOnDemand.url, + loadedAfter: 1, + trackerAdapters: [PlayerItemTrackerMock.adapter(statePublisher: publisher)] + )) + player.play() + } + } + + func testFailedItem() { + let player = Player() + let publisher = PlayerItemTrackerMock.StatePublisher() + expectEqualPublished(values: [.initialized, .enabled, .metricEvents, .metricEvents], from: publisher, during: .milliseconds(500)) { + player.append(.simple( + url: Stream.unavailable.url, + trackerAdapters: [PlayerItemTrackerMock.adapter(statePublisher: publisher)] + )) + player.play() + } + } + + func testMoveCurrentItem() { + let publisher = PlayerItemTrackerMock.StatePublisher() + let player = Player() + expectAtLeastEqualPublished(values: [.initialized, .enabled, .metricEvents, .metricEvents], from: publisher) { + player.append(.simple( + url: Stream.onDemand.url, + trackerAdapters: [PlayerItemTrackerMock.adapter(statePublisher: publisher)] + )) + player.play() + } + expectNothingPublished(from: publisher, during: .seconds(1)) { + player.prepend(.simple(url: Stream.onDemand.url)) + } + } +} diff --git a/Tests/PlayerTests/Tracking/PlayerItemTrackerMetricPublisherTests.swift b/Tests/PlayerTests/Tracking/PlayerItemTrackerMetricPublisherTests.swift new file mode 100644 index 00000000..6d9895fa --- /dev/null +++ b/Tests/PlayerTests/Tracking/PlayerItemTrackerMetricPublisherTests.swift @@ -0,0 +1,54 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import PillarboxCircumspect +import PillarboxStreams + +final class PlayerItemTrackerMetricPublisherTests: TestCase { + func testEmptyPlayer() { + let player = Player() + expectSimilarPublished(values: [[]], from: player.metricEventsPublisher, during: .milliseconds(500)) + } + + func testItemPlayback() { + let player = Player(item: .simple(url: Stream.shortOnDemand.url)) + expectAtLeastSimilarPublished(values: [ + [], + [.anyMetadata], + [.anyMetadata, .anyAsset], + [] + ], from: player.metricEventsPublisher) { + player.play() + } + } + + func testError() { + let player = Player(item: .simple(url: Stream.unavailable.url)) + expectAtLeastSimilarPublished(values: [ + [], + [.anyMetadata], + [.anyMetadata, .anyFailure] + ], from: player.metricEventsPublisher) + } + + func testPlaylist() { + let player = Player(items: [.simple(url: Stream.shortOnDemand.url), .simple(url: Stream.mediumOnDemand.url)]) + expectSimilarPublished( + values: [ + [], + [.anyMetadata], + [.anyMetadata, .anyAsset], + [.anyMetadata, .anyAsset] + ], + from: player.metricEventsPublisher, + during: .seconds(2) + ) { + player.play() + } + } +} diff --git a/Tests/PlayerTests/Tracking/PlayerItemTrackerSessionTests.swift b/Tests/PlayerTests/Tracking/PlayerItemTrackerSessionTests.swift new file mode 100644 index 00000000..1e02605f --- /dev/null +++ b/Tests/PlayerTests/Tracking/PlayerItemTrackerSessionTests.swift @@ -0,0 +1,31 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class PlayerItemTrackerSessionTests: TestCase { + func testEmpty() { + let player = Player() + expect(player.currentSessionIdentifiers(trackedBy: PlayerItemTrackerMock.self)).to(beEmpty()) + } + + func testSessions() { + let player = Player( + item: .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + PlayerItemTrackerMock.adapter(configuration: .init(sessionIdentifier: "A")), + PlayerItemTrackerMock.adapter(configuration: .init()), + PlayerItemTrackerMock.adapter(configuration: .init(sessionIdentifier: "B")) + ] + ) + ) + expect(player.currentSessionIdentifiers(trackedBy: PlayerItemTrackerMock.self)).to(equal(["A", "B"])) + } +} diff --git a/Tests/PlayerTests/Tracking/PlayerItemTrackerUpdateTests.swift b/Tests/PlayerTests/Tracking/PlayerItemTrackerUpdateTests.swift new file mode 100644 index 00000000..5bb52734 --- /dev/null +++ b/Tests/PlayerTests/Tracking/PlayerItemTrackerUpdateTests.swift @@ -0,0 +1,58 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import PillarboxCircumspect +import PillarboxStreams + +final class PlayerItemTrackerUpdateTests: TestCase { + func testMetadata() { + let player = Player() + let publisher = TrackerUpdateMock.StatePublisher() + let item = PlayerItem.simple( + url: Stream.shortOnDemand.url, + metadata: AssetMetadataMock(title: "title"), + trackerAdapters: [ + TrackerUpdateMock.adapter(statePublisher: publisher) { $0.title } + ] + ) + expectAtLeastEqualPublished( + values: [ + .updatedMetadata("title"), + .enabled, + .updatedProperties, + .disabled + ], + from: publisher.removeDuplicates() + ) { + player.append(item) + player.play() + } + } + + func testMetadataUpdate() { + let player = Player() + let publisher = TrackerUpdateMock.StatePublisher() + let item = PlayerItem.mock(url: Stream.shortOnDemand.url, withMetadataUpdateAfter: 1, trackerAdapters: [ + TrackerUpdateMock.adapter(statePublisher: publisher) { $0.title } + ]) + expectAtLeastEqualPublished( + values: [ + .updatedMetadata("title0"), + .enabled, + .updatedProperties, + .updatedMetadata("title1"), + .updatedProperties, + .disabled + ], + from: publisher.removeDuplicates() + ) { + player.append(item) + player.play() + } + } +} diff --git a/Tests/PlayerTests/Tracking/PlayerTrackingTests.swift b/Tests/PlayerTests/Tracking/PlayerTrackingTests.swift new file mode 100644 index 00000000..cb036711 --- /dev/null +++ b/Tests/PlayerTests/Tracking/PlayerTrackingTests.swift @@ -0,0 +1,133 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import PillarboxCircumspect +import PillarboxStreams + +final class PlayerTrackingTests: TestCase { + func testTrackingDisabled() { + let player = Player() + player.isTrackingEnabled = false + let publisher = PlayerItemTrackerMock.StatePublisher() + + expectEqualPublished(values: [.initialized], from: publisher, during: .milliseconds(500)) { + player.append( + .simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [ + PlayerItemTrackerMock.adapter(statePublisher: publisher) + ] + ) + ) + player.play() + } + } + + func testTrackingEnabledDuringPlayback() { + let player = Player() + player.isTrackingEnabled = false + + let publisher = PlayerItemTrackerMock.StatePublisher() + + expectEqualPublished(values: [.initialized], from: publisher, during: .seconds(1)) { + player.append( + .simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [ + PlayerItemTrackerMock.adapter(statePublisher: publisher) + ] + ) + ) + } + + expectAtLeastEqualPublished( + values: [.enabled, .metricEvents, .disabled], + from: publisher + ) { + player.isTrackingEnabled = true + player.play() + } + } + + func testTrackingDisabledDuringPlayback() { + let player = Player() + player.isTrackingEnabled = true + + let publisher = PlayerItemTrackerMock.StatePublisher() + + expectEqualPublished(values: [.initialized, .enabled, .metricEvents, .metricEvents], from: publisher, during: .seconds(1)) { + player.append( + .simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [ + PlayerItemTrackerMock.adapter(statePublisher: publisher) + ] + ) + ) + } + + expectAtLeastEqualPublished( + values: [.disabled], + from: publisher + ) { + player.isTrackingEnabled = false + player.play() + } + } + + func testTrackingEnabledTwice() { + let publisher = PlayerItemTrackerMock.StatePublisher() + + let player = Player(item: .simple(url: Stream.shortOnDemand.url, trackerAdapters: [PlayerItemTrackerMock.adapter(statePublisher: publisher)])) + player.isTrackingEnabled = true + + expectEqualPublished(values: [.metricEvents, .metricEvents], from: publisher, during: .seconds(1)) { + player.isTrackingEnabled = true + } + } + + func testMandatoryTracker() { + let player = Player() + player.isTrackingEnabled = false + + let publisher = PlayerItemTrackerMock.StatePublisher() + + expectEqualPublished(values: [.initialized, .enabled, .metricEvents, .metricEvents], from: publisher, during: .seconds(1)) { + player.append( + .simple( + url: Stream.shortOnDemand.url, + trackerAdapters: [ + PlayerItemTrackerMock.adapter(statePublisher: publisher, behavior: .mandatory) + ] + ) + ) + } + } + + func testEnablingTrackingMustNotEmitMetricEventsAgainForMandatoryTracker() { + let player = Player() + player.isTrackingEnabled = false + + let publisher = PlayerItemTrackerMock.StatePublisher() + + expectEqualPublished(values: [.initialized, .enabled, .metricEvents, .metricEvents], from: publisher, during: .seconds(1)) { + player.append( + .simple( + url: Stream.onDemand.url, + trackerAdapters: [ + PlayerItemTrackerMock.adapter(statePublisher: publisher, behavior: .mandatory) + ] + ) + ) + } + + expectNothingPublished(from: publisher, during: .seconds(1)) { + player.isTrackingEnabled = true + } + } +} diff --git a/Tests/PlayerTests/Types/CMTimeRangeTests.swift b/Tests/PlayerTests/Types/CMTimeRangeTests.swift new file mode 100644 index 00000000..361b0f94 --- /dev/null +++ b/Tests/PlayerTests/Types/CMTimeRangeTests.swift @@ -0,0 +1,48 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble + +final class CMTimeRangeTests: TestCase { + func testEmpty() { + expect(CMTimeRange.flatten([])).to(beEmpty()) + } + + func testNoOverlap() { + let timeRanges: [CMTimeRange] = [ + .init(start: .init(value: 1, timescale: 1), end: .init(value: 10, timescale: 1)), + .init(start: .init(value: 20, timescale: 1), end: .init(value: 30, timescale: 1)) + ] + expect(CMTimeRange.flatten(timeRanges)).to(equal(timeRanges)) + } + + func testOverlap() { + let timeRanges: [CMTimeRange] = [ + .init(start: .init(value: 1, timescale: 1), end: .init(value: 10, timescale: 1)), + .init(start: .init(value: 5, timescale: 1), end: .init(value: 30, timescale: 1)) + ] + expect(CMTimeRange.flatten(timeRanges)).to(equal( + [ + .init(start: .init(value: 1, timescale: 1), end: .init(value: 30, timescale: 1)) + ] + )) + } + + func testContained() { + let timeRanges: [CMTimeRange] = [ + .init(start: .init(value: 1, timescale: 1), end: .init(value: 10, timescale: 1)), + .init(start: .init(value: 2, timescale: 1), end: .init(value: 8, timescale: 1)) + ] + expect(CMTimeRange.flatten(timeRanges)).to(equal( + [ + .init(start: .init(value: 1, timescale: 1), end: .init(value: 10, timescale: 1)) + ] + )) + } +} diff --git a/Tests/PlayerTests/Types/CMTimeTests.swift b/Tests/PlayerTests/Types/CMTimeTests.swift new file mode 100644 index 00000000..4f4e8956 --- /dev/null +++ b/Tests/PlayerTests/Types/CMTimeTests.swift @@ -0,0 +1,103 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble + +final class CMTimeTests: TestCase { + func testClampedWithNonEmptyRange() { + let range = CMTimeRange(start: CMTime(value: 1, timescale: 1), end: CMTime(value: 10, timescale: 1)) + expect(CMTime.zero.clamped(to: range)).to(equal(CMTime(value: 1, timescale: 1))) + expect(CMTime.invalid.clamped(to: range)).to(equal(.invalid)) + expect(CMTime(value: 1, timescale: 1).clamped(to: range)).to(equal(CMTime(value: 1, timescale: 1))) + expect(CMTime(value: 5, timescale: 1).clamped(to: range)).to(equal(CMTime(value: 5, timescale: 1))) + expect(CMTime(value: 10, timescale: 1).clamped(to: range)).to(equal(CMTime(value: 10, timescale: 1))) + expect(CMTime(value: 20, timescale: 1).clamped(to: range)).to(equal(CMTime(value: 10, timescale: 1))) + } + + func testClampedWithNonEmptyRangeAndOffset() { + let range = CMTimeRange(start: CMTime(value: 1, timescale: 1), end: CMTime(value: 10, timescale: 1)) + let offset = CMTime(value: 1, timescale: 10) + expect(CMTime.zero.clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) + expect(CMTime.invalid.clamped(to: range, offset: offset)).to(equal(.invalid)) + expect(CMTime(value: 1, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) + expect(CMTime(value: 5, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 5, timescale: 1))) + expect(CMTime(value: 10, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 99, timescale: 10))) + expect(CMTime(value: 20, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 99, timescale: 10))) + } + + func testClampedWithNonEmptyRangeAndLargeOffset() { + let range = CMTimeRange(start: CMTime(value: 1, timescale: 1), end: CMTime(value: 10, timescale: 1)) + let offset = CMTime(value: 100, timescale: 1) + expect(CMTime.zero.clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) + expect(CMTime.invalid.clamped(to: range, offset: offset)).to(equal(.invalid)) + expect(CMTime(value: 1, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) + expect(CMTime(value: 5, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) + expect(CMTime(value: 10, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) + expect(CMTime(value: 20, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) + } + + func testClampedWithEmptyRange() { + let range = CMTimeRange(start: CMTime(value: 1, timescale: 1), end: CMTime(value: 1, timescale: 1)) + expect(CMTime.zero.clamped(to: range)).to(equal(CMTime(value: 1, timescale: 1))) + expect(CMTime.invalid.clamped(to: range)).to(equal(.invalid)) + expect(CMTime(value: 1, timescale: 1).clamped(to: range)).to(equal(CMTime(value: 1, timescale: 1))) + expect(CMTime(value: 5, timescale: 1).clamped(to: range)).to(equal(CMTime(value: 1, timescale: 1))) + } + + func testClampedWithEmptyRangeAndOffset() { + let range = CMTimeRange(start: CMTime(value: 1, timescale: 1), end: CMTime(value: 1, timescale: 1)) + let offset = CMTime(value: 1, timescale: 10) + expect(CMTime.zero.clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) + expect(CMTime.invalid.clamped(to: range, offset: offset)).to(equal(.invalid)) + expect(CMTime(value: 1, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) + expect(CMTime(value: 5, timescale: 1).clamped(to: range, offset: offset)).to(equal(CMTime(value: 1, timescale: 1))) + } + + func testClampedWithInvalidRange() { + let range = CMTimeRange.invalid + expect(CMTime.zero.clamped(to: range)).to(equal(.invalid)) + expect(CMTime.invalid.clamped(to: range)).to(equal(.invalid)) + expect(CMTime(value: 1, timescale: 1).clamped(to: range)).to(equal(.invalid)) + } + + func testClampedWithInvalidRangeAndOffset() { + let range = CMTimeRange.invalid + let offset = CMTime(value: 1, timescale: 10) + expect(CMTime.zero.clamped(to: range, offset: offset)).to(equal(.invalid)) + expect(CMTime.invalid.clamped(to: range, offset: offset)).to(equal(.invalid)) + expect(CMTime(value: 1, timescale: 1).clamped(to: range, offset: offset)).to(equal(.invalid)) + } + + func testAfterWithoutTimeRange() { + let time = CMTime(value: 5, timescale: 1) + expect(time.after(timeRanges: [])).to(beNil()) + } + + func testAfterWithMatchingTimeRange() { + let time = CMTime(value: 5, timescale: 1) + expect(time.after(timeRanges: [ + .init(start: CMTime(value: 2, timescale: 1), end: CMTime(value: 6, timescale: 1)) + ])).to(equal(CMTime(value: 6, timescale: 1))) + } + + func testAfterWithoutMatchingTimeRange() { + let time = CMTime(value: 5, timescale: 1) + expect(time.after(timeRanges: [ + .init(start: CMTime(value: 12, timescale: 1), end: CMTime(value: 16, timescale: 1)) + ])).to(beNil()) + } + + func testAfterWithMatchingTimeRanges() { + let time = CMTime(value: 5, timescale: 1) + expect(time.after(timeRanges: [ + .init(start: CMTime(value: 2, timescale: 1), end: CMTime(value: 6, timescale: 1)), + .init(start: CMTime(value: 4, timescale: 1), end: CMTime(value: 10, timescale: 1)) + ])).to(equal(CMTime(value: 10, timescale: 1))) + } +} diff --git a/Tests/PlayerTests/Types/ErrorsTests.swift b/Tests/PlayerTests/Types/ErrorsTests.swift new file mode 100644 index 00000000..ebfad89e --- /dev/null +++ b/Tests/PlayerTests/Types/ErrorsTests.swift @@ -0,0 +1,49 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import XCTest + +private enum EnumError: LocalizedError { + case someError + + var errorDescription: String? { + "Enum error description" + } + + var failureReason: String? { + "Enum failure reason" + } + + var recoverySuggestion: String? { + "Enum recovery suggestion" + } + + var helpAnchor: String? { + "Enum help anchor" + } +} + +final class ErrorsTests: XCTestCase { + func testNSErrorFromNSError() { + let error = NSError(domain: "domain", code: 1012, userInfo: [ + NSLocalizedDescriptionKey: "Error description", + NSLocalizedFailureReasonErrorKey: "Failure reason", + NSUnderlyingErrorKey: NSError(domain: "inner.domain", code: 2024) + ]) + expect(NSError.error(from: error)).to(equal(error)) + } + + func testNSErrorFromSwiftError() { + let error = NSError.error(from: EnumError.someError)! + expect(error.localizedDescription).to(equal("Enum error description")) + expect(error.localizedFailureReason).to(equal("Enum failure reason")) + expect(error.localizedRecoverySuggestion).to(equal("Enum recovery suggestion")) + expect(error.helpAnchor).to(equal("Enum help anchor")) + } +} diff --git a/Tests/PlayerTests/Types/ImageSourceTests.swift b/Tests/PlayerTests/Types/ImageSourceTests.swift new file mode 100644 index 00000000..df8b4c39 --- /dev/null +++ b/Tests/PlayerTests/Types/ImageSourceTests.swift @@ -0,0 +1,76 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import PillarboxCircumspect +import UIKit + +final class ImageSourceTests: TestCase { + func testNone() { + expectEqualPublished( + values: [.none], + from: ImageSource.none.imageSourcePublisher(), + during: .milliseconds(100) + ) + } + + func testImage() { + let image = UIImage(systemName: "circle")! + expectEqualPublished( + values: [.image(image)], + from: ImageSource.image(image).imageSourcePublisher(), + during: .milliseconds(100) + ) + } + + func testNonLoadedImageForValidUrl() { + let url = Bundle.module.url(forResource: "pixel", withExtension: "jpg")! + let source = ImageSource.url(standardResolution: url) + expectSimilarPublished( + values: [.url(standardResolution: url)], + from: source.imageSourcePublisher(), + during: .milliseconds(100) + ) + } + + func testLoadedImageForValidUrl() { + let url = Bundle.module.url(forResource: "pixel", withExtension: "jpg")! + let image = UIImage(contentsOfFile: url.path())! + let source = ImageSource.url(standardResolution: url) + expectSimilarPublished( + values: [.url(standardResolution: url), .image(image)], + from: source.imageSourcePublisher(), + during: .milliseconds(100) + ) { + _ = source.image + } + } + + func testInvalidImageFormat() { + let url = Bundle.module.url(forResource: "invalid", withExtension: "jpg")! + let source = ImageSource.url(standardResolution: url) + expectSimilarPublished( + values: [.url(standardResolution: url), .none], + from: source.imageSourcePublisher(), + during: .milliseconds(100) + ) { + _ = source.image + } + } + + func testFailingUrl() { + let url = URL(string: "https://localhost:8123/missing.jpg")! + let source = ImageSource.url(standardResolution: url) + expectSimilarPublished( + values: [.url(standardResolution: url), .none], + from: source.imageSourcePublisher(), + during: .seconds(1) + ) { + _ = source.image + } + } +} diff --git a/Tests/PlayerTests/Types/ItemErrorTests.swift b/Tests/PlayerTests/Types/ItemErrorTests.swift new file mode 100644 index 00000000..865cbddf --- /dev/null +++ b/Tests/PlayerTests/Types/ItemErrorTests.swift @@ -0,0 +1,43 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble + +final class ItemErrorTests: TestCase { + func testNoInnerComment() { + expect(ItemError.innerComment(from: "The internet connection appears to be offline")) + .to(equal("The internet connection appears to be offline")) + } + + func testInnerComment() { + expect(ItemError.innerComment( + from: "The operation couldn’t be completed. (CoreBusiness.DataError error 1 - This content is not available anymore.)" + )).to(equal("This content is not available anymore.")) + + expect(ItemError.innerComment( + from: "The operation couldn’t be completed. (CoreBusiness.DataError error 1 - Not found)" + )).to(equal("Not found")) + + expect(ItemError.innerComment( + from: "The operation couldn't be completed. (CoreMediaErrorDomain error -16839 - Unable to get playlist before long download timer.)" + )).to(equal("Unable to get playlist before long download timer.")) + + expect(ItemError.innerComment( + from: "L’opération n’a pas pu s’achever. (CoreBusiness.DataError erreur 1 - Ce contenu n'est plus disponible.)" + )).to(equal("Ce contenu n'est plus disponible.")) + } + + func testNestedInnerComments() { + expect(ItemError.innerComment( + from: """ + The operation couldn’t be completed. (CoreMediaErrorDomain error -12660 - The operation couldn’t be completed. \ + (CoreMediaErrorDomain error -12660 - HTTP 403: Forbidden)) + """ + )).to(equal("HTTP 403: Forbidden")) + } +} diff --git a/Tests/PlayerTests/Types/PlaybackSpeedTests.swift b/Tests/PlayerTests/Types/PlaybackSpeedTests.swift new file mode 100644 index 00000000..38d94c40 --- /dev/null +++ b/Tests/PlayerTests/Types/PlaybackSpeedTests.swift @@ -0,0 +1,35 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble + +final class PlaybackSpeedTests: TestCase { + func testNoValueClampingToIndefiniteRange() { + let speed = PlaybackSpeed(value: 2, range: nil) + expect(speed.value).to(equal(2)) + expect(speed.range).to(beNil()) + } + + func testValueClampingToDefiniteRange() { + let speed = PlaybackSpeed(value: 2, range: 1...1) + expect(speed.value).to(equal(1)) + expect(speed.range).to(equal(1...1)) + } + + func testEffectivePropertiesWhenIndefinite() { + let speed = PlaybackSpeed.indefinite + expect(speed.effectiveValue).to(equal(1)) + expect(speed.effectiveRange).to(equal(1...1)) + } + + func testEffectivePropertiesWhenDefinite() { + let speed = PlaybackSpeed(value: 2, range: 0...2) + expect(speed.effectiveValue).to(equal(2)) + expect(speed.effectiveRange).to(equal(0...2)) + } +} diff --git a/Tests/PlayerTests/Types/PlaybackStateTests.swift b/Tests/PlayerTests/Types/PlaybackStateTests.swift new file mode 100644 index 00000000..0d3ece45 --- /dev/null +++ b/Tests/PlayerTests/Types/PlaybackStateTests.swift @@ -0,0 +1,20 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble + +final class PlaybackStateTests: TestCase { + func testAllCases() { + expect(PlaybackState(itemStatus: .unknown, rate: 0)).to(equal(.idle)) + expect(PlaybackState(itemStatus: .unknown, rate: 1)).to(equal(.idle)) + expect(PlaybackState(itemStatus: .readyToPlay, rate: 0)).to(equal(.paused)) + expect(PlaybackState(itemStatus: .readyToPlay, rate: 1)).to(equal(.playing)) + expect(PlaybackState(itemStatus: .ended, rate: 0)).to(equal(.ended)) + expect(PlaybackState(itemStatus: .ended, rate: 1)).to(equal(.ended)) + } +} diff --git a/Tests/PlayerTests/Types/PlayerConfigurationTests.swift b/Tests/PlayerTests/Types/PlayerConfigurationTests.swift new file mode 100644 index 00000000..a6d39906 --- /dev/null +++ b/Tests/PlayerTests/Types/PlayerConfigurationTests.swift @@ -0,0 +1,42 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble + +final class PlayerConfigurationTests: TestCase { + func testDefaultValues() { + let configuration = PlayerConfiguration() + expect(configuration.allowsExternalPlayback).to(beTrue()) + expect(configuration.usesExternalPlaybackWhileMirroring).to(beFalse()) + expect(configuration.preventsDisplaySleepDuringVideoPlayback).to(beTrue()) + expect(configuration.navigationMode).to(equal(.smart(interval: 3))) + expect(configuration.backwardSkipInterval).to(equal(10)) + expect(configuration.forwardSkipInterval).to(equal(10)) + expect(configuration.preloadedItems).to(equal(2)) + expect(configuration.allowsConstrainedNetworkAccess).to(beTrue()) + } + + func testCustomValues() { + let configuration = PlayerConfiguration( + allowsExternalPlayback: false, + usesExternalPlaybackWhileMirroring: true, + preventsDisplaySleepDuringVideoPlayback: false, + navigationMode: .immediate, + backwardSkipInterval: 42, + forwardSkipInterval: 47, + allowsConstrainedNetworkAccess: false + ) + expect(configuration.allowsExternalPlayback).to(beFalse()) + expect(configuration.usesExternalPlaybackWhileMirroring).to(beTrue()) + expect(configuration.preventsDisplaySleepDuringVideoPlayback).to(beFalse()) + expect(configuration.navigationMode).to(equal(.immediate)) + expect(configuration.backwardSkipInterval).to(equal(42)) + expect(configuration.forwardSkipInterval).to(equal(47)) + expect(configuration.allowsConstrainedNetworkAccess).to(beFalse()) + } +} diff --git a/Tests/PlayerTests/Types/PlayerLimitsTests.swift b/Tests/PlayerTests/Types/PlayerLimitsTests.swift new file mode 100644 index 00000000..d2daa438 --- /dev/null +++ b/Tests/PlayerTests/Types/PlayerLimitsTests.swift @@ -0,0 +1,79 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import PillarboxStreams + +final class PlayerLimitsTests: TestCase { + private static let limits = PlayerLimits( + preferredPeakBitRate: 100, + preferredPeakBitRateForExpensiveNetworks: 200, + preferredMaximumResolution: .init(width: 100, height: 200), + preferredMaximumResolutionForExpensiveNetworks: .init(width: 300, height: 400) + ) + + func testDefaultValues() { + let limits = PlayerLimits() + expect(limits.preferredPeakBitRate).to(equal(0)) + expect(limits.preferredPeakBitRateForExpensiveNetworks).to(equal(0)) + expect(limits.preferredMaximumResolution).to(equal(.zero)) + expect(limits.preferredMaximumResolutionForExpensiveNetworks).to(equal(.zero)) + } + + func testCustomValues() { + let limits = PlayerLimits( + preferredPeakBitRate: 100, + preferredPeakBitRateForExpensiveNetworks: 200, + preferredMaximumResolution: .init(width: 100, height: 200), + preferredMaximumResolutionForExpensiveNetworks: .init(width: 300, height: 400) + ) + expect(limits.preferredPeakBitRate).to(equal(100)) + expect(limits.preferredPeakBitRateForExpensiveNetworks).to(equal(200)) + expect(limits.preferredMaximumResolution).to(equal(.init(width: 100, height: 200))) + expect(limits.preferredMaximumResolutionForExpensiveNetworks).to(equal(.init(width: 300, height: 400))) + } + + func testAppliedDefaultValues() { + let player = Player(items: [ + .simple(url: Stream.onDemand.url), + .simple(url: Stream.mediumOnDemand.url) + ]) + player.queuePlayer.items().forEach { item in + expect(item.preferredPeakBitRate).to(equal(0)) + expect(item.preferredPeakBitRateForExpensiveNetworks).to(equal(0)) + expect(item.preferredMaximumResolution).to(equal(.zero)) + expect(item.preferredMaximumResolutionForExpensiveNetworks).to(equal(.zero)) + } + } + + func testAppliedInitialValues() { + let player = Player(items: [ + .simple(url: Stream.onDemand.url), + .simple(url: Stream.mediumOnDemand.url) + ]) + player.limits = Self.limits + player.queuePlayer.items().forEach { item in + expect(item.preferredPeakBitRate).to(equal(100)) + expect(item.preferredPeakBitRateForExpensiveNetworks).to(equal(200)) + expect(item.preferredMaximumResolution).to(equal(.init(width: 100, height: 200))) + expect(item.preferredMaximumResolutionForExpensiveNetworks).to(equal(.init(width: 300, height: 400))) + } + } + + func testLoadedItem() { + let player = Player(item: .mock(url: Stream.onDemand.url, loadedAfter: 0.1)) + player.limits = Self.limits + expect(player.playbackState).toEventually(equal(.paused)) + player.queuePlayer.items().forEach { item in + expect(item.preferredPeakBitRate).to(equal(100)) + expect(item.preferredPeakBitRateForExpensiveNetworks).to(equal(200)) + expect(item.preferredMaximumResolution).to(equal(.init(width: 100, height: 200))) + expect(item.preferredMaximumResolutionForExpensiveNetworks).to(equal(.init(width: 300, height: 400))) + } + } +} diff --git a/Tests/PlayerTests/Types/PlayerMetadataTests.swift b/Tests/PlayerTests/Types/PlayerMetadataTests.swift new file mode 100644 index 00000000..70402784 --- /dev/null +++ b/Tests/PlayerTests/Types/PlayerMetadataTests.swift @@ -0,0 +1,66 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import AVFoundation +import MediaPlayer +import Nimble + +final class PlayerMetadataTests: TestCase { + private static func value(for identifier: AVMetadataIdentifier, in items: [AVMetadataItem]) async throws -> Any? { + guard let item = AVMetadataItem.metadataItems(from: items, filteredByIdentifier: identifier).first else { return nil } + return try await item.load(.value) + } + + func testNowPlayingInfo() { + let metadata = PlayerMetadata( + title: "title", + subtitle: "subtitle", + imageSource: .image(.init(systemName: "circle")!) + ) + let nowPlayingInfo = metadata.nowPlayingInfo + expect(nowPlayingInfo[MPMediaItemPropertyTitle] as? String).to(equal("title")) + expect(nowPlayingInfo[MPMediaItemPropertyArtist] as? String).to(equal("subtitle")) + expect(nowPlayingInfo[MPMediaItemPropertyArtwork]).notTo(beNil()) + } + + func testExternalMetadata() async { + let metadata = PlayerMetadata( + identifier: "identifier", + title: "title", + subtitle: "subtitle", + description: "description", + imageSource: .image(.init(systemName: "circle")!), + episodeInformation: .long(season: 2, episode: 3) + ) + let externalMetadata = metadata.externalMetadata + await expect { + try await Self.value(for: .commonIdentifierAssetIdentifier, in: externalMetadata) as? String + }.to(equal("identifier")) + await expect { + try await Self.value(for: .commonIdentifierTitle, in: externalMetadata) as? String + }.to(equal("title")) + await expect { + try await Self.value(for: .iTunesMetadataTrackSubTitle, in: externalMetadata) as? String + }.to(equal("subtitle")) + await expect { + try await Self.value(for: .commonIdentifierDescription, in: externalMetadata) as? String + }.to(equal("description")) + await expect { + try await Self.value(for: .quickTimeUserDataCreationDate, in: externalMetadata) as? String + }.to(equal("S2, E3")) + + await expect { + try await Self.value(for: .commonIdentifierArtwork, in: externalMetadata) + } +#if os(tvOS) + .notTo(beNil()) +#else + .to(beNil()) +#endif + } +} diff --git a/Tests/PlayerTests/Types/PositionTests.swift b/Tests/PlayerTests/Types/PositionTests.swift new file mode 100644 index 00000000..f4216f2d --- /dev/null +++ b/Tests/PlayerTests/Types/PositionTests.swift @@ -0,0 +1,47 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble + +final class PositionTests: TestCase { + func testPositionTo() { + let position = to(CMTime(value: 1, timescale: 1), toleranceBefore: CMTime(value: 2, timescale: 1), toleranceAfter: CMTime(value: 3, timescale: 1)) + expect(position.time).to(equal(CMTime(value: 1, timescale: 1))) + expect(position.toleranceBefore).to(equal(CMTime(value: 2, timescale: 1))) + expect(position.toleranceAfter).to(equal(CMTime(value: 3, timescale: 1))) + } + + func testPositionAt() { + let position = at(CMTime(value: 1, timescale: 1)) + expect(position.time).to(equal(CMTime(value: 1, timescale: 1))) + expect(position.toleranceBefore).to(equal(.zero)) + expect(position.toleranceAfter).to(equal(.zero)) + } + + func testPositionNear() { + let position = near(CMTime(value: 1, timescale: 1)) + expect(position.time).to(equal(CMTime(value: 1, timescale: 1))) + expect(position.toleranceBefore).to(equal(.positiveInfinity)) + expect(position.toleranceAfter).to(equal(.positiveInfinity)) + } + + func testPositionBefore() { + let position = before(CMTime(value: 1, timescale: 1)) + expect(position.time).to(equal(CMTime(value: 1, timescale: 1))) + expect(position.toleranceBefore).to(equal(.positiveInfinity)) + expect(position.toleranceAfter).to(equal(.zero)) + } + + func testPositionAfter() { + let position = after(CMTime(value: 1, timescale: 1)) + expect(position.time).to(equal(CMTime(value: 1, timescale: 1))) + expect(position.toleranceBefore).to(equal(.zero)) + expect(position.toleranceAfter).to(equal(.positiveInfinity)) + } +} diff --git a/Tests/PlayerTests/Types/StreamTypeTests.swift b/Tests/PlayerTests/Types/StreamTypeTests.swift new file mode 100644 index 00000000..4e7ef6ac --- /dev/null +++ b/Tests/PlayerTests/Types/StreamTypeTests.swift @@ -0,0 +1,23 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble + +final class StreamTypeTests: TestCase { + func testAllCases() { + expect(StreamType(for: .zero, duration: .invalid)).to(equal(.unknown)) + expect(StreamType(for: .zero, duration: .indefinite)).to(equal(.live)) + expect(StreamType(for: .finite, duration: .indefinite)).to(equal(.dvr)) + expect(StreamType(for: .zero, duration: .zero)).to(equal(.onDemand)) + } +} + +private extension CMTimeRange { + static let finite = Self(start: .zero, duration: .init(value: 1, timescale: 1)) +} diff --git a/Tests/PlayerTests/Types/TimePropertiesTests.swift b/Tests/PlayerTests/Types/TimePropertiesTests.swift new file mode 100644 index 00000000..d8f92d49 --- /dev/null +++ b/Tests/PlayerTests/Types/TimePropertiesTests.swift @@ -0,0 +1,69 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import CoreMedia +import Nimble + +final class TimePropertiesTests: TestCase { + func testWithoutTimeRange() { + expect(TimeProperties.timeRange(from: [])).to(beNil()) + } + + func testTimeRange() { + expect(TimeProperties.timeRange(from: [NSValue(timeRange: .finite)])).to(equal(.finite)) + } + + func testTimeRanges() { + expect(TimeProperties.timeRange(from: [ + NSValue(timeRange: .init(start: .init(value: 1, timescale: 1), duration: .init(value: 3, timescale: 1))), + NSValue(timeRange: .init(start: .init(value: 10, timescale: 1), duration: .init(value: 5, timescale: 1))) + ])).to(equal( + .init(start: .init(value: 1, timescale: 1), duration: .init(value: 14, timescale: 1)) + )) + } + + func testInvalidTimeRange() { + expect(TimeProperties.timeRange(from: [NSValue(timeRange: .invalid)])).to(equal(.invalid)) + } + + func testSeekableTimeRangeFallback() { + expect( + TimeProperties.timeRange( + loadedTimeRanges: [NSValue(timeRange: .finite)], + seekableTimeRanges: [] + ) + ) + .to(equal(.zero)) + } + + func testBufferEmptyLoadedTimeRanges() { + expect( + TimeProperties( + loadedTimeRanges: [], + seekableTimeRanges: [NSValue(timeRange: .finite)], + isPlaybackLikelyToKeepUp: true + ).buffer + ) + .to(equal(0)) + } + + func testBuffer() { + expect( + TimeProperties( + loadedTimeRanges: [NSValue(timeRange: .finite)], + seekableTimeRanges: [NSValue(timeRange: .finite)], + isPlaybackLikelyToKeepUp: true + ).buffer + ) + .to(equal(1)) + } +} + +private extension CMTimeRange { + static let finite = Self(start: .zero, duration: .init(value: 1, timescale: 1)) +} diff --git a/Tests/PlayerTests/UserInterface/VisibilityTrackerTests.swift b/Tests/PlayerTests/UserInterface/VisibilityTrackerTests.swift new file mode 100644 index 00000000..a02d80ee --- /dev/null +++ b/Tests/PlayerTests/UserInterface/VisibilityTrackerTests.swift @@ -0,0 +1,184 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +@testable import PillarboxPlayer + +import Nimble +import ObjectiveC +import PillarboxCircumspect +import PillarboxStreams + +#if os(iOS) +final class VisibilityTrackerTests: TestCase { + func testInitiallyVisible() { + let visibilityTracker = VisibilityTracker() + expect(visibilityTracker.isUserInterfaceHidden).to(beFalse()) + } + + func testInitiallyHidden() { + let visibilityTracker = VisibilityTracker(isUserInterfaceHidden: true) + expect(visibilityTracker.isUserInterfaceHidden).to(beTrue()) + } + + func testNoToggleWithoutPlayer() { + let visibilityTracker = VisibilityTracker() + visibilityTracker.toggle() + expect(visibilityTracker.isUserInterfaceHidden).to(beFalse()) + } + + func testToggle() { + let visibilityTracker = VisibilityTracker() + visibilityTracker.player = Player() + visibilityTracker.toggle() + expect(visibilityTracker.isUserInterfaceHidden).to(beTrue()) + } + + func testInitiallyVisibleIfPaused() { + let visibilityTracker = VisibilityTracker(delay: 0.5, isUserInterfaceHidden: true) + visibilityTracker.player = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) + expect(visibilityTracker.isUserInterfaceHidden).toEventually(beFalse()) + } + + func testVisibleWhenPaused() { + let visibilityTracker = VisibilityTracker(delay: 0.5, isUserInterfaceHidden: true) + let player = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) + visibilityTracker.player = player + player.play() + expect(player.playbackState).toEventually(equal(.playing)) + expect(visibilityTracker.isUserInterfaceHidden).to(beTrue()) + player.pause() + expect(visibilityTracker.isUserInterfaceHidden).toEventually(beFalse()) + } + + func testNoAutoHideWhileIdle() { + let visibilityTracker = VisibilityTracker(delay: 0.5) + visibilityTracker.player = Player() + expect(visibilityTracker.isUserInterfaceHidden).toNever(beTrue(), until: .seconds(1)) + } + + func testAutoHideWhilePlaying() { + let visibilityTracker = VisibilityTracker(delay: 0.5) + let player = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) + player.play() + visibilityTracker.player = player + expect(visibilityTracker.isUserInterfaceHidden).toEventually(beTrue()) + } + + func testNoAutoHideWhilePaused() { + let visibilityTracker = VisibilityTracker(delay: 0.5) + visibilityTracker.player = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) + expect(visibilityTracker.isUserInterfaceHidden).toNever(beTrue(), until: .seconds(1)) + } + + func testNoAutoHideWhileEnded() { + let visibilityTracker = VisibilityTracker(delay: Stream.shortOnDemand.duration.seconds + 0.5) + let player = Player(item: PlayerItem.simple(url: Stream.shortOnDemand.url)) + player.play() + visibilityTracker.player = player + expect(visibilityTracker.isUserInterfaceHidden).toNever(beTrue(), until: .seconds(1)) + } + + func testNoAutoHideWhileFailed() { + let visibilityTracker = VisibilityTracker(delay: 0.5) + visibilityTracker.player = Player(item: PlayerItem.simple(url: Stream.unavailable.url)) + expect(visibilityTracker.isUserInterfaceHidden).toNever(beTrue(), until: .seconds(1)) + } + + func testNoAutoHideWithEmptyPlayer() { + let visibilityTracker = VisibilityTracker(delay: 0.5) + visibilityTracker.player = Player() + expect(visibilityTracker.isUserInterfaceHidden).toNever(beTrue(), until: .seconds(1)) + } + + func testNoAutoHideWithoutPlayer() { + let visibilityTracker = VisibilityTracker(delay: 0.5) + expect(visibilityTracker.isUserInterfaceHidden).toNever(beTrue(), until: .seconds(1)) + } + + func testResetAutoHide() { + let visibilityTracker = VisibilityTracker(delay: 0.3) + let player = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) + visibilityTracker.player = player + player.play() + expect(player.playbackState).toEventually(equal(.playing)) + expect(visibilityTracker.isUserInterfaceHidden).toAlways(beFalse(), until: .milliseconds(200)) + visibilityTracker.reset() + expect(visibilityTracker.isUserInterfaceHidden).toAlways(beFalse(), until: .milliseconds(200)) + } + + func testResetDoesNotShowControls() { + let visibilityTracker = VisibilityTracker(isUserInterfaceHidden: true) + visibilityTracker.reset() + expect(visibilityTracker.isUserInterfaceHidden).to(beTrue()) + } + + func testAutoHideAfterUnhide() { + let visibilityTracker = VisibilityTracker(delay: 0.5, isUserInterfaceHidden: true) + let player = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) + player.play() + visibilityTracker.player = player + visibilityTracker.toggle() + expect(visibilityTracker.isUserInterfaceHidden).toEventually(beTrue()) + } + + func testInvalidDelay() { + guard nimbleThrowAssertionsAvailable() else { return } + expect(VisibilityTracker(delay: -5)).to(throwAssertion()) + } + + func testPlayerChangeDoesNotHideUserInterface() { + let visibilityTracker = VisibilityTracker() + visibilityTracker.player = Player() + expect(visibilityTracker.isUserInterfaceHidden).to(beFalse()) + } + + func testPlayerChangeDoesNotShowUserInterface() { + let visibilityTracker = VisibilityTracker(isUserInterfaceHidden: true) + visibilityTracker.player = Player() + expect(visibilityTracker.isUserInterfaceHidden).to(beTrue()) + } + + func testPlayerChangeResetsAutoHide() { + let player1 = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) + player1.play() + expect(player1.playbackState).toEventually(equal(.playing)) + + let player2 = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) + player2.play() + expect(player2.playbackState).toEventually(equal(.playing)) + + let visibilityTracker = VisibilityTracker(delay: 0.5) + visibilityTracker.player = player1 + expect(visibilityTracker.isUserInterfaceHidden).toAlways(beFalse(), until: .milliseconds(400)) + + visibilityTracker.player = player2 + expect(visibilityTracker.isUserInterfaceHidden).toAlways(beFalse(), until: .milliseconds(400)) + } + + func testDeallocation() { + var visibilityTracker: VisibilityTracker? = VisibilityTracker() + weak var weakVisibilityTracker = visibilityTracker + autoreleasepool { + visibilityTracker = nil + } + expect(weakVisibilityTracker).to(beNil()) + } + + func testDeallocationWhilePlaying() { + var visibilityTracker: VisibilityTracker? = VisibilityTracker() + let player = Player(item: PlayerItem.simple(url: Stream.onDemand.url)) + player.play() + visibilityTracker?.player = player + expect(player.playbackState).toEventually(equal(.playing)) + + weak var weakVisibilityTracker = visibilityTracker + autoreleasepool { + visibilityTracker = nil + } + expect(weakVisibilityTracker).to(beNil()) + } +} +#endif From fc3d347193df5c84253d5598faf1876d3de91aed Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:01:12 +0100 Subject: [PATCH 60/68] Try ubuntu-latest for some jobs --- .github/workflows/pull-request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e072f046..3f150ecb 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -6,7 +6,7 @@ on: [push] jobs: check-quality: name: "🔎 Check quality" - runs-on: [macos-latest] + runs-on: [ubuntu-latest] steps: - name: Checkout code uses: actions/checkout@v4 @@ -16,7 +16,7 @@ jobs: build-documentation: name: "📚 Build documentation" - runs-on: [macos-latest] + runs-on: [ubuntu-latest] steps: - name: Checkout code uses: actions/checkout@v4 From 88660863807734f13dc212b2951025377e61adaf Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 14:00:55 +0100 Subject: [PATCH 61/68] Revert "Split configurations" This reverts commit 296e4d5697a92dcf5c28ceb6af7db0661be84cda. --- .github/workflows/pull-request.yml | 4 +--- Makefile | 20 ++++++-------------- fastlane/Fastfile | 28 +++++++--------------------- 3 files changed, 14 insertions(+), 38 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 3f150ecb..93dfa9cf 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -43,7 +43,6 @@ jobs: strategy: matrix: platform: [ios, tvos] - configuration: [nightly, release] steps: - name: Checkout code uses: actions/checkout@v4 @@ -62,5 +61,4 @@ jobs: ${{ secrets.APPLE_ACCOUNT_INFO_B64 }} - name: Archive the demo - run: | - make archive-demo-${{ matrix.configuration }}-${{ matrix.platform }} + run: make archive-demo-${{ matrix.platform }} diff --git a/Makefile b/Makefile index b86713ce..ad12fa6f 100644 --- a/Makefile +++ b/Makefile @@ -20,21 +20,13 @@ install-bundler: fastlane: install-pkgx install-bundler @pkgx bundle exec fastlane -.PHONY: archive-demo-nightly-ios -archive-demo-nightly-ios: install-pkgx install-bundler - @pkgx bundle exec fastlane archive_demo_nightly_ios +.PHONY: archive-demo-ios +archive-demo-ios: install-pkgx install-bundler + @pkgx bundle exec fastlane archive_demo_ios -.PHONY: archive-demo-nightly-tvos -archive-demo-nightly-tvos: install-pkgx install-bundler - @pkgx bundle exec fastlane archive_demo_nightly_tvos - -.PHONY: archive-demo-release-ios -archive-demo-release-ios: install-pkgx install-bundler - @pkgx bundle exec fastlane archive_demo_release_ios - -.PHONY: archive-demo-release-tvos -archive-demo-release-tvos: install-pkgx install-bundler - @pkgx bundle exec fastlane archive_demo_release_tvos +.PHONY: archive-demo-tvos +archive-demo-tvos: install-pkgx install-bundler + @pkgx bundle exec fastlane archive_demo_tvos .PHONY: deliver-demo-nightly-ios deliver-demo-nightly-ios: install-pkgx install-bundler diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d6e9d1b4..34fe358a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -164,13 +164,9 @@ rescue StandardError => e UI.important('TestFlight external delivery was skipped because a build is already in review') end -def archive_demo_nightly(platform_id) +def archive_demo(platform_id) ensure_configuration_availability build_and_sign_app(platform_id, :nightly) -end - -def archive_demo_release(platform_id) - ensure_configuration_availability build_and_sign_app(platform_id, :release) end @@ -227,24 +223,14 @@ platform :ios do reset_git_repo(skip_clean: true) end - desc 'Archive the iOS nightly demo app' - lane :archive_demo_nightly_ios do - archive_demo_nightly(:ios) - end - - desc 'Archive the iOS release demo app' - lane :archive_demo_release_ios do - archive_demo_release(:ios) - end - - desc 'Archive the tvOS nightly demo app' - lane :archive_demo_nightly_tvos do - archive_demo_nightly(:tvos) + desc 'Archive the iOS demo app' + lane :archive_demo_ios do + archive_demo(:ios) end - desc 'Archive the tvOS release demo app' - lane :archive_demo_release_tvos do - archive_demo_release(:tvos) + desc 'Archive the tvOS demo app' + lane :archive_demo_tvos do + archive_demo(:tvos) end desc 'Deliver an iOS demo app nightly build' From c3994ff6cb8fa6f58c10a473a5a75692ac733ffd Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 14:04:28 +0100 Subject: [PATCH 62/68] Revert "Try ubuntu-latest for some jobs" This reverts commit cee1d6266ab69e4d12ea7d948609c3216cfeaa0c. --- .github/workflows/pull-request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 93dfa9cf..c4b1f979 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -6,7 +6,7 @@ on: [push] jobs: check-quality: name: "🔎 Check quality" - runs-on: [ubuntu-latest] + runs-on: [macos-latest] steps: - name: Checkout code uses: actions/checkout@v4 @@ -16,7 +16,7 @@ jobs: build-documentation: name: "📚 Build documentation" - runs-on: [ubuntu-latest] + runs-on: [macos-latest] steps: - name: Checkout code uses: actions/checkout@v4 From c7003a7ec4c6f631f82a12f7ac95d4dc2608d156 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:45:30 +0100 Subject: [PATCH 63/68] Use secrets for ENV variables --- .github/workflows/nightlies.yaml | 14 ++++++++++---- .github/workflows/pull-request.yml | 23 ++++++++++++++--------- Package.swift | 6 +++--- Scripts/add-apple-certificate.sh | 12 ++++++------ Scripts/configure-environment.sh | 6 ++---- fastlane/Fastfile | 2 ++ 6 files changed, 37 insertions(+), 26 deletions(-) diff --git a/.github/workflows/nightlies.yaml b/.github/workflows/nightlies.yaml index dee50816..92a111c8 100644 --- a/.github/workflows/nightlies.yaml +++ b/.github/workflows/nightlies.yaml @@ -1,12 +1,14 @@ --- name: Nightlies -#on: [push] +on: + push: + branches: [main] jobs: deliver-demo-nightlies: name: "🌙 Nightlies" - runs-on: [macos-latest] + runs-on: macos-latest strategy: matrix: platform: [ios, tvos] @@ -24,8 +26,12 @@ jobs: - name: Configure environment run: | Scripts/configure-environment.sh \ - ${{ secrets.APP_STORE_CONNECT_API_KEY_B64 }} \ - ${{ secrets.APPLE_ACCOUNT_INFO_B64 }} + ${{ secrets.APP_STORE_CONNECT_API_KEY }} + env: + TEAM_ID: ${{ secrets.TEAM_ID }} + KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} + KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ISSUER_ID }} + TESTFLIGHT_GROUPS: ${{ vars.TESTFLIGHT_GROUPS }} - name: Archive the demo run: | diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index c4b1f979..7c249857 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,12 +1,12 @@ --- name: Pull Request -on: [push] +on: pull_request jobs: check-quality: name: "🔎 Check quality" - runs-on: [macos-latest] + runs-on: macos-latest steps: - name: Checkout code uses: actions/checkout@v4 @@ -16,7 +16,7 @@ jobs: build-documentation: name: "📚 Build documentation" - runs-on: [macos-latest] + runs-on: macos-latest steps: - name: Checkout code uses: actions/checkout@v4 @@ -26,7 +26,7 @@ jobs: tests: name: "🧪 Tests" - runs-on: [macos-latest] + runs-on: macos-latest strategy: matrix: platform: [ios, tvos] @@ -39,7 +39,7 @@ jobs: archive-demos: name: "📦 Archives" - runs-on: [macos-latest] + runs-on: macos-latest strategy: matrix: platform: [ios, tvos] @@ -52,13 +52,18 @@ jobs: Scripts/add-apple-certificate.sh \ $RUNNER_TEMP \ ${{ secrets.KEYCHAIN_PASSWORD }} \ - ${{ secrets.SRGSSR_APPLE_DEV_CERTIFICATE_B64 }} + ${{ secrets.APPLE_DEV_CERTIFICATE }} \ + ${{ secrets.APPLE_DEV_CERTIFICATE_PASSWORD }} - name: Configure environment run: | Scripts/configure-environment.sh \ - ${{ secrets.APP_STORE_CONNECT_API_KEY_B64 }} \ - ${{ secrets.APPLE_ACCOUNT_INFO_B64 }} - + ${{ secrets.APP_STORE_CONNECT_API_KEY }} + env: + TEAM_ID: ${{ secrets.TEAM_ID }} + KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} + KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ISSUER_ID }} + TESTFLIGHT_GROUPS: ${{ vars.TESTFLIGHT_GROUPS }} + - name: Archive the demo run: make archive-demo-${{ matrix.platform }} diff --git a/Package.swift b/Package.swift index e5d5cc89..97bd5999 100644 --- a/Package.swift +++ b/Package.swift @@ -36,7 +36,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/comScore/Comscore-Swift-Package-Manager.git", .upToNextMinor(from: "6.14.0")), - .package(url: "https://github.com/SRGSSR/commanders-act-apple.git", .upToNextMinor(from: "5.4.0")), + .package(url: "https://github.com/CommandersAct/iOSV5.git", .upToNextMinor(from: "5.4.0")), .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/krzysztofzablocki/Difference.git", exact: "1.0.1"), .package(url: "https://github.com/Quick/Nimble.git", .upToNextMajor(from: "13.0.0")) @@ -47,8 +47,8 @@ let package = Package( dependencies: [ .target(name: "PillarboxPlayer"), .product(name: "ComScore", package: "Comscore-Swift-Package-Manager"), - .product(name: "TCCore", package: "commanders-act-apple"), - .product(name: "TCServerSide", package: "commanders-act-apple") + .product(name: "TCCore", package: "iOSV5"), + .product(name: "TCServerSide", package: "iOSV5") ], path: "Sources/Analytics", resources: [ diff --git a/Scripts/add-apple-certificate.sh b/Scripts/add-apple-certificate.sh index 835d8e34..bada240a 100755 --- a/Scripts/add-apple-certificate.sh +++ b/Scripts/add-apple-certificate.sh @@ -2,20 +2,20 @@ root_dir="$1" keychain_password="$2" -apple_certificate_b64="$3" +apple_certificate_base64="$3" +apple_certificate_password="$4" -if [[ -z $root_dir || -z $keychain_password || -z $apple_certificate_b64 ]] + +if [[ -z $root_dir || -z $keychain_password || -z $apple_certificate_base64 || -z $apple_certificate_password ]] then - echo "[!] Usage: $0 " + echo "[!] Usage: $0 " exit 1 fi keychain_path="$root_dir/app-signing.keychain-db" - -apple_certificate_password="6YXTQTG8JJ" apple_certificate="$root_dir/certificate.p12" -echo -n "$apple_certificate_b64" | base64 --decode -o "$apple_certificate" +echo -n "$apple_certificate_base64" | base64 --decode -o "$apple_certificate" # Create a temporary keychain (https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners) security create-keychain -p "$keychain_password" "$keychain_path" diff --git a/Scripts/configure-environment.sh b/Scripts/configure-environment.sh index de87f2a2..11f14161 100755 --- a/Scripts/configure-environment.sh +++ b/Scripts/configure-environment.sh @@ -1,14 +1,12 @@ #!/bin/bash apple_api_key_b64="$1" -apple_account_info_b64="$2" -if [[ -z $apple_api_key_b64 || -z $apple_account_info_b64 ]] +if [[ -z $apple_api_key_b64 ]] then - echo "[!] Usage: $0 " + echo "[!] Usage: $0 " exit 1 fi mkdir -p Configuration -echo "$apple_account_info_b64" | base64 --decode > Configuration/.env echo "$apple_api_key_b64" | base64 --decode > Configuration/AppStoreConnect_API_Key.p8 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 34fe358a..fbbe0a84 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -177,6 +177,8 @@ def deliver_demo_nightly(platform_id) build_and_sign_app(platform_id, :nightly) reset_git_repo(skip_clean: true) login_to_app_store_connect + # upload_app_to_testflight + # distribute_app_to_testers(platform_id, :nightly, build_number) end def deliver_demo_release(platform_id) From cf7d9102e0e868cabb2a4614c9c6d1c8e6680f4f Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:45:30 +0100 Subject: [PATCH 64/68] Use secrets for ENV variables --- .github/workflows/nightlies.yaml | 8 ++++---- .github/workflows/pull-request.yml | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/nightlies.yaml b/.github/workflows/nightlies.yaml index 92a111c8..98591ba1 100644 --- a/.github/workflows/nightlies.yaml +++ b/.github/workflows/nightlies.yaml @@ -27,12 +27,12 @@ jobs: run: | Scripts/configure-environment.sh \ ${{ secrets.APP_STORE_CONNECT_API_KEY }} + + - name: Archive the demo + run: | + make deliver-demo-nightly-${{ matrix.platform }} env: TEAM_ID: ${{ secrets.TEAM_ID }} KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ISSUER_ID }} TESTFLIGHT_GROUPS: ${{ vars.TESTFLIGHT_GROUPS }} - - - name: Archive the demo - run: | - make deliver-demo-nightly-${{ matrix.platform }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 7c249857..06fe0d9b 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -59,11 +59,11 @@ jobs: run: | Scripts/configure-environment.sh \ ${{ secrets.APP_STORE_CONNECT_API_KEY }} + + - name: Archive the demo + run: make archive-demo-${{ matrix.platform }} env: TEAM_ID: ${{ secrets.TEAM_ID }} KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ISSUER_ID }} TESTFLIGHT_GROUPS: ${{ vars.TESTFLIGHT_GROUPS }} - - - name: Archive the demo - run: make archive-demo-${{ matrix.platform }} From 1c3484c400f8c2098680e425f71c83c0b1ff26ac Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 16:34:57 +0100 Subject: [PATCH 65/68] Fix quality --- .github/workflows/nightlies.yaml | 2 +- .github/workflows/pull-request.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nightlies.yaml b/.github/workflows/nightlies.yaml index 98591ba1..f9dcc248 100644 --- a/.github/workflows/nightlies.yaml +++ b/.github/workflows/nightlies.yaml @@ -1,7 +1,7 @@ --- name: Nightlies -on: +on: # yamllint disable-line rule:truthy push: branches: [main] diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 06fe0d9b..c0e72790 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,7 +1,7 @@ --- name: Pull Request -on: pull_request +on: pull_request # yamllint disable-line rule:truthy jobs: check-quality: @@ -59,7 +59,7 @@ jobs: run: | Scripts/configure-environment.sh \ ${{ secrets.APP_STORE_CONNECT_API_KEY }} - + - name: Archive the demo run: make archive-demo-${{ matrix.platform }} env: From 1f73086ce8a08e2b7ed5f39d0a2a313f610ddfd7 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Wed, 27 Nov 2024 16:35:30 +0100 Subject: [PATCH 66/68] Remove useless .env file --- .env | 1 - 1 file changed, 1 deletion(-) delete mode 120000 .env diff --git a/.env b/.env deleted file mode 120000 index f54419c1..00000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -Configuration/.env \ No newline at end of file From df0068b8ab8d4f1f79ad9d87557adb2e008f43c3 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Thu, 28 Nov 2024 13:47:57 +0100 Subject: [PATCH 67/68] Remove Gemfile.lock --- Gemfile.lock | 231 --------------------------------------------------- Makefile | 2 +- 2 files changed, 1 insertion(+), 232 deletions(-) delete mode 100644 Gemfile.lock diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index d027463b..00000000 --- a/Gemfile.lock +++ /dev/null @@ -1,231 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.7) - base64 - nkf - rexml - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) - artifactory (3.0.17) - atomos (0.1.3) - aws-eventstream (1.3.0) - aws-partitions (1.1013.0) - aws-sdk-core (3.213.0) - aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.992.0) - aws-sigv4 (~> 1.9) - jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.96.0) - aws-sdk-core (~> 3, >= 3.210.0) - aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.173.0) - aws-sdk-core (~> 3, >= 3.210.0) - aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.5) - aws-sigv4 (1.10.1) - aws-eventstream (~> 1, >= 1.0.2) - babosa (1.0.4) - badge (0.13.0) - fastimage (>= 1.6) - fastlane (>= 2.0) - mini_magick (>= 4.9.4, < 5.0.0) - base64 (0.2.0) - claide (1.1.0) - colored (1.2) - colored2 (3.1.2) - commander (4.6.0) - highline (~> 2.0.0) - declarative (0.0.20) - digest-crc (0.6.5) - rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20240107) - dotenv (2.8.1) - emoji_regex (3.2.3) - excon (0.112.0) - faraday (1.10.4) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-cookie_jar (0.0.7) - faraday (>= 0.8.0) - http-cookie (~> 1.0.0) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.2) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) - faraday_middleware (1.2.1) - faraday (~> 1.0) - fastimage (2.3.1) - fastlane (2.225.0) - CFPropertyList (>= 2.3, < 4.0.0) - addressable (>= 2.8, < 3.0.0) - artifactory (~> 3.0) - aws-sdk-s3 (~> 1.0) - babosa (>= 1.0.3, < 2.0.0) - bundler (>= 1.12.0, < 3.0.0) - colored (~> 1.2) - commander (~> 4.6) - dotenv (>= 2.1.1, < 3.0.0) - emoji_regex (>= 0.1, < 4.0) - excon (>= 0.71.0, < 1.0.0) - faraday (~> 1.0) - faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 1.0) - fastimage (>= 2.1.0, < 3.0.0) - fastlane-sirp (>= 1.0.0) - gh_inspector (>= 1.1.2, < 2.0.0) - google-apis-androidpublisher_v3 (~> 0.3) - google-apis-playcustomapp_v1 (~> 0.1) - google-cloud-env (>= 1.6.0, < 2.0.0) - google-cloud-storage (~> 1.31) - highline (~> 2.0) - http-cookie (~> 1.0.5) - json (< 3.0.0) - jwt (>= 2.1.0, < 3) - mini_magick (>= 4.9.4, < 5.0.0) - multipart-post (>= 2.0.0, < 3.0.0) - naturally (~> 2.2) - optparse (>= 0.1.1, < 1.0.0) - plist (>= 3.1.0, < 4.0.0) - rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.5) - simctl (~> 1.6.3) - terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (~> 3) - tty-screen (>= 0.6.3, < 1.0.0) - tty-spinner (>= 0.8.0, < 1.0.0) - word_wrap (~> 1.0.0) - xcodeproj (>= 1.13.0, < 2.0.0) - xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) - fastlane-plugin-badge (1.5.0) - badge (~> 0.13.0) - fastlane-plugin-xcconfig (2.1.0) - fastlane-sirp (1.0.0) - sysrandom (~> 1.0) - gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.54.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.3) - addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) - mini_mime (~> 1.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.a) - rexml - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-playcustomapp_v1 (0.13.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.31.0) - google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.1) - google-cloud-env (>= 1.0, < 3.a) - google-cloud-errors (~> 1.0) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.4.0) - google-cloud-storage (1.47.0) - addressable (~> 2.8) - digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.31.0) - google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) - mini_mime (~> 1.0) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) - jwt (>= 1.4, < 3.0) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - signet (>= 0.16, < 2.a) - highline (2.0.3) - http-cookie (1.0.7) - domain_name (~> 0.5) - httpclient (2.8.3) - jmespath (1.6.2) - json (2.8.2) - jwt (2.9.3) - base64 - mini_magick (4.13.2) - mini_mime (1.1.5) - multi_json (1.15.0) - multipart-post (2.4.1) - nanaimo (0.4.0) - naturally (2.2.1) - nkf (0.2.0) - optparse (0.6.0) - os (1.1.4) - plist (3.7.1) - public_suffix (6.0.1) - rake (13.2.1) - representable (3.2.0) - declarative (< 0.1.0) - trailblazer-option (>= 0.1.1, < 0.2.0) - uber (< 0.2.0) - retriable (3.1.2) - rexml (3.3.9) - rouge (2.0.7) - ruby2_keywords (0.0.5) - rubyzip (2.3.2) - security (0.1.5) - signet (0.19.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) - multi_json (~> 1.10) - simctl (1.6.10) - CFPropertyList - naturally - sysrandom (1.0.5) - terminal-notifier (2.0.0) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - trailblazer-option (0.1.2) - tty-cursor (0.7.1) - tty-screen (0.8.2) - tty-spinner (0.9.3) - tty-cursor (~> 0.7) - uber (0.1.0) - unicode-display_width (2.6.0) - word_wrap (1.0.0) - xcodeproj (1.27.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.4.0) - rexml (>= 3.3.6, < 4.0) - xcpretty (0.3.0) - rouge (~> 2.0.7) - xcpretty-travis-formatter (1.0.1) - xcpretty (~> 0.2, >= 0.0.7) - -PLATFORMS - arm64-darwin-23 - ruby - -DEPENDENCIES - fastlane - fastlane-plugin-badge - fastlane-plugin-xcconfig - -BUNDLED WITH - 2.5.23 diff --git a/Makefile b/Makefile index ad12fa6f..b89e1bb2 100644 --- a/Makefile +++ b/Makefile @@ -144,7 +144,7 @@ help: @echo "The following targets are available:" @echo "" @echo " all Default target" - @echo " install-pkgx Install required tools" + @echo " install-pkgx Install required tools" @echo "" @echo " fastlane Run fastlane" @echo "" From e58a0dcaa60989914d08a86d6714a944c51abd81 Mon Sep 17 00:00:00 2001 From: Walid Kayhal <3347810+waliid@users.noreply.github.com> Date: Thu, 28 Nov 2024 13:50:15 +0100 Subject: [PATCH 68/68] Update Gemfile.lock --- Gemfile.lock | 236 +++++++++++++++++++++++++++++++++++++++++++++++++++ Makefile | 1 - 2 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 Gemfile.lock diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..a1ebc7d6 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,236 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.3.0) + aws-partitions (1.1014.0) + aws-sdk-core (3.214.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.96.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.174.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.10.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + badge (0.13.0) + fastimage (>= 1.6) + fastlane (>= 2.0) + mini_magick (>= 4.9.4, < 5.0.0) + base64 (0.2.0) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.112.0) + faraday (1.10.4) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.3.1) + fastlane (2.225.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored (~> 1.2) + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (>= 0.1.1, < 1.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-plugin-badge (1.5.0) + badge (~> 0.13.0) + fastlane-plugin-xcconfig (2.1.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.7.1) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.7) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.6.2) + json (2.8.2) + jwt (2.9.3) + base64 + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.15.0) + multipart-post (2.4.1) + nanaimo (0.4.0) + naturally (2.2.1) + nkf (0.2.0) + optparse (0.6.0) + os (1.1.4) + plist (3.7.1) + public_suffix (6.0.1) + rake (13.2.1) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.3.9) + rouge (2.0.7) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.5) + signet (0.19.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + sysrandom (1.0.5) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + arm64-darwin-21 + arm64-darwin-22 + arm64-darwin-23 + arm64-darwin-24 + ruby + x86_64-darwin-21 + x86_64-darwin-22 + +DEPENDENCIES + fastlane + fastlane-plugin-badge + fastlane-plugin-xcconfig + +BUNDLED WITH + 2.5.23 diff --git a/Makefile b/Makefile index b89e1bb2..3429dd37 100644 --- a/Makefile +++ b/Makefile @@ -144,7 +144,6 @@ help: @echo "The following targets are available:" @echo "" @echo " all Default target" - @echo " install-pkgx Install required tools" @echo "" @echo " fastlane Run fastlane" @echo ""