Android
Интеграция в приложение для смартфонов и планшетов. Общая схема описана в разделе WebView: общая схема, здесь — готовый код для Android.
Что понадобится
- WebView с включённым JavaScript.
- Видеоплеер — в примере Media3 ExoPlayer.
- Адрес страницы SDK с платформой
android:https://cdn.adsdk.ru/android/v3/.
Как устроен экран
xml
<!-- res/layout/activity_ad.xml -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
<androidx.media3.ui.PlayerView
android:id="@+id/adPlayer"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:use_controller="false" />
<!-- Сверху: принимает нажатия на кнопки SDK -->
<WebView
android:id="@+id/adWebView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>Полный пример
Настройте WebView и откройте страницу SDK
kotlin
class AdActivity : AppCompatActivity() {
private lateinit var webView: WebView
private lateinit var player: ExoPlayer
private val main = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_ad)
player = ExoPlayer.Builder(this).build()
findViewById<PlayerView>(R.id.adPlayer).player = player
player.addListener(playerListener)
webView = findViewById(R.id.adWebView)
webView.setBackgroundColor(Color.TRANSPARENT)
webView.settings.javaScriptEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.addJavascriptInterface(AdEvents(), "Android")
webView.loadUrl(adPageUrl(vastUrl = intent.getStringExtra(EXTRA_VAST_URL)!!))
}
private fun adPageUrl(vastUrl: String): String =
Uri.parse("https://cdn.adsdk.ru/android/v3/").buildUpon()
.appendQueryParameter("url", vastUrl)
.appendQueryParameter("lang", "ru")
.appendQueryParameter("controls", """["ad-skip-btn","ad-click-btn","ad-more-menu"]""")
.build()
.toString()
/** Выполняет метод SDK. evaluateJavascript можно вызывать только из главного потока. */
private fun sdk(call: String) = main.post {
webView.evaluateJavascript("window.myAdController.$call", null)
}
}Получайте события
SDK вызывает методы объекта Android. Объявите те, что нужны, — остальные SDK пропустит.
Потоки
Методы @JavascriptInterface вызываются в фоновом потоке WebView. Всё, что трогает плеер или интерфейс, переносите в главный поток — в примере это main.post { }.
kotlin
private inner class AdEvents {
private var isVpaid = false
// Приходит перед AdLoaded: тип креатива говорит, кто будет его проигрывать
@JavascriptInterface
fun AdCreativeLoaded(
srcUrl: String, type: String, bannerId: String, isNoBanner: Boolean,
isPod: Boolean, podLength: Int, isLast: Boolean, erid: String, durationMsec: Int,
) {
isVpaid = type == "application/javascript"
}
@JavascriptInterface
fun AdLoaded(mediaFileUrl: String) = main.post {
if (isVpaid) {
sdk("playAd()") // VPAID играет внутри WebView
} else {
player.setMediaItem(MediaItem.fromUri(mediaFileUrl))
player.prepare()
player.playWhenReady = true
}
}
@JavascriptInterface
fun AdSkipped(adNumber: Int) = main.post { player.stop() }
@JavascriptInterface
fun AdClick(url: String, adNumber: Int) = main.post {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
@JavascriptInterface
fun AdError(code: String, message: String, type: String,
isNoBanner: Boolean, isLast: Boolean, adNumber: Int) = main.post { close() }
@JavascriptInterface
fun AdDestroyed(isLast: Boolean) = main.post {
player.stop()
if (isLast) close()
}
}Сообщайте SDK о воспроизведении
kotlin
private val progress = object : Runnable {
override fun run() {
if (player.duration > 0) {
sdk("timeupdateAd(${player.currentPosition / 1000.0}, ${player.duration / 1000.0})")
}
main.postDelayed(this, PROGRESS_INTERVAL_MS)
}
}
private val playerListener = object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
if (isPlaying) {
sdk("playAd()") // и первый старт, и продолжение после паузы
main.post(progress)
} else {
main.removeCallbacks(progress)
if (!player.playWhenReady) sdk("pauseAd()") // пауза пользователя, не буферизация
}
}
override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_ENDED) {
val durationSec = player.duration / 1000.0
sdk("timeupdateAd($durationSec, $durationSec)") // SDK пришлёт AdComplete
}
}
}
private companion object {
const val PROGRESS_INTERVAL_MS = 250L
const val EXTRA_VAST_URL = "vast_url"
}Завершите показ
kotlin
private fun close() {
main.removeCallbacks(progress)
finish()
}
override fun onPause() {
super.onPause()
player.pause() // listener отправит pauseAd()
}
override fun onDestroy() {
webView.removeJavascriptInterface("Android")
webView.destroy()
player.release()
super.onDestroy()
}Сигнатуры событий
WebView ищет метод интерфейса по имени и числу аргументов. Метод с другим числом аргументов не будет вызван. Полный список — в разделе События → Android.
| Событие | Метод |
|---|---|
| Реклама загружена | AdCreativeLoaded(9 аргументов), затем AdLoaded(mediaFileUrl) |
| Начало показа | AdStarted(adNumber) |
| Квартили | AdFirstQuartile(adNumber), AdMidpoint(adNumber), AdThirdQuartile(adNumber) |
| Можно пропустить | AdSkippableStateChanged(status) |
| Ролик досмотрен | AdComplete(adNumber) |
| Показ закончен | AdDestroyed(isLast) |
| Ошибка | AdError(code, message, type, isNoBanner, isLast, adNumber) |
Частые ошибки
| Симптом | Причина |
|---|---|
| Кнопки SDK видны, ролика нет | Фон WebView не прозрачный или WebView лежит под плеером. |
Нет AdStarted | Не вызван playAd() после старта плеера. |
Нет квартилей и AdComplete | Не вызывается timeupdateAd(). |
| Событие не приходит | Число аргументов метода не совпадает с сигнатурой. |
| Приложение падает в обработчике события | Плеер тронут не из главного потока. |
Больше — в разделе Решение проблем.