Перенос приложения CCL Sender в Cast Application Framework (CAF)

Следующая процедура позволяет преобразовать приложение-отправитель Android из Cast SDK v2 с CCL в CAF. Все функции CCL реализованы в CAF, поэтому после миграции вам больше не понадобится использовать CCL.

Cast CAF Sender SDK использует CastContext для управления GoogleAPIClient от вашего имени. CastContext управляет жизненными циклами, ошибками и обратными вызовами за вас, что значительно упрощает разработку приложения Cast.

Введение

  • Поскольку на дизайн CAF Sender повлияла Cast Companion Library, миграция с CCL на CAF Sender включает в основном взаимно однозначное сопоставление классов и их методов.
  • CAF Sender по-прежнему распространяется как часть сервисов Google Play с использованием диспетчера Android SDK.
  • Новые пакеты ( com.google.android.gms.cast.framework.* ), добавленные в CAF Sender, с функциональностью, аналогичной CCL, отвечают за соответствие контрольному списку Google Cast Design .
  • CAF Sender предоставляет виджеты, соответствующие требованиям Cast UX; эти виджеты аналогичны тем, которые предоставляет CCL.
  • CAF Sender предоставляет асинхронные обратные вызовы, аналогичные CCL, для отслеживания состояний и получения данных. В отличие от CCL, CAF Sender не предоставляет неработающих реализаций различных методов интерфейса.

В следующих разделах мы в основном сосредоточимся на видеоориентированных приложениях, основанных на CCL VideoCastManager, но во многих случаях те же концепции применимы и к DataCastManager.

Зависимости

CCL и CAF имеют одинаковые зависимости от библиотеки поддержки AppCompat, библиотеки поддержки MediaRouter v7 и сервисов Google Play. Однако разница в том, что CAF зависит от новой платформы Cast, доступной в сервисах Google Play 9.2.0 или более поздней версии.

В файле build.gradle удалите зависимости от com.google.android.gms:play-services-cast и com.google.android.libraries.cast.companionlibrary:ccl , затем добавьте новую структуру Cast:

dependencies {
    compile 'com.android.support:appcompat-v7:23.4.0'
    compile 'com.android.support:mediarouter-v7:23.4.0'
    compile 'com.google.android.gms:play-services-cast-framework:9.4.0'
}

Вы также можете удалить метаданные службы Google Play:

<meta‐data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>

Любые службы, действия и ресурсы, являющиеся частью CAF, автоматически объединяются с манифестом и ресурсами вашего приложения.

Минимальная версия Android SDK, которую поддерживает CAF, — 9 (Gingerbread); Минимальная версия CCL для Android SDK — 10.

CCL предоставляет удобный метод BaseCastManager.checkGooglePlayServices(activity) для проверки наличия на устройстве совместимой версии сервисов Google Play. CAF не предоставляет это как часть Cast SDK. Выполните процедуру Убедитесь, что на устройствах установлен APK сервисов Google Play, чтобы убедиться, что правильный APK сервисов Google Play установлен на устройстве пользователя, поскольку обновления могут не сразу достигать всех пользователей.

Вам по-прежнему необходимо использовать вариант Theme.AppCompat для темы приложения.

Инициализация

Для CCL требовалось, чтобы VideoCastManager.initialize() вызывался в onCreate() экземпляра Applications. Эта логика должна быть удалена из кода вашего класса приложения.

В CAF для платформы Cast также требуется явный этап инициализации. Это включает в себя инициализацию синглтона CastContext с использованием соответствующего OptionsProvider для указания идентификатора приложения-получателя и любых других глобальных параметров. CastContext играет ту же роль, что и VideoCastManager CCL, предоставляя синглтон, с которым взаимодействуют клиенты. OptionsProvider похож на CCL CastConfiguration , что позволяет настраивать функции платформы Cast.

Если ваш текущий CCL CastConfiguration.Builder выглядит так:

VideoCastManager.initialize(
   getApplicationContext(),
   new CastConfiguration.Builder(context.getString(R.string.app_id))
       .enableWifiReconnection()
       .enableAutoReconnect()
       .build());

Тогда в CAF следующий CastOptionsProvider использующий CastOptions.Builder , будет аналогичен:

public class CastOptionsProvider implements OptionsProvider {

    @Override
    public CastOptions getCastOptions(Context context) {
        return new CastOptions.Builder()
                .setReceiverApplicationId(context.getString(R.string.app_id))
                .build();
    }

    @Override
    public List<SessionProvider> getAdditionalSessionProviders(
            Context context) {
        return null;
    }
}

Взгляните на наш образец приложения для полной реализации OptionsProvider.

Объявите OptionsProvider в элементе application файла AndroidManifest.xml:

<application>
...
    <meta-data
        android:name=
          "com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
        android:value="com.google.sample.cast.refplayer.CastOptionsProvider"    
    />
</application>

Лениво инициализируйте CastContext в каждом методе onCreate Activity а не в экземпляре Application ):

private CastContext mCastContext;

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.video_browser);
    setupActionBar();

    mCastContext = CastContext.getSharedInstance(this);
}

Чтобы получить доступ к синглтону CastContext используйте:

mCastContext = CastContext.getSharedInstance(this);

Обнаружение устройства

CCL VideoCastManager incrementUiCounter и decrementUiCounter должны быть удалены из методов onResume и onPause вашей Activities .

В CAF процесс обнаружения автоматически запускается и останавливается фреймворком, когда приложение выходит на передний план и уходит в фон соответственно.

Кнопка трансляции и диалоговое окно трансляции

Как и в случае с CCL, эти компоненты предоставляются библиотекой поддержки MediaRouter v7.

Кнопка Cast по-прежнему реализуется MediaRouteButton и может быть добавлена ​​в вашу активность (используя ActionBar или Toolbar ) в качестве пункта меню в вашем меню.

Объявление MediaRouteActionProvider в меню xml такое же, как и в CCL:

<item
    android:id="@+id/media_route_menu_item"
    android:title="@string/media_route_menu_title"
    app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
    app:showAsAction="always"/>

Подобно CCL, переопределите метод onCreateOptionMenu() каждого действия, но вместо использования CastManager.addMediaRouterButton используйте CAF CastButtonFactory для подключения MediaRouteButton к платформе Cast:

public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.browse, menu);
    CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                                                menu,
                                                R.id.media_route_menu_item);
    return true;
}

Управление устройством

Подобно CCL, в CAF управление устройствами в значительной степени осуществляется платформой. Приложению-отправителю не нужно обрабатывать (и не пытаться обрабатывать) подключение к устройству и запуск приложения-получателя с помощью GoogleApiClient .

Взаимодействие между отправителем и получателем теперь представлено как «сеанс». Класс SessionManager управляет жизненным циклом сеанса и автоматически запускает и останавливает сеансы в ответ на жесты пользователя: сеанс начинается, когда пользователь выбирает устройство Cast в диалоговом окне Cast, и завершается, когда пользователь нажимает кнопку «Остановить трансляцию» в диалоговом окне Cast. или когда само приложение-отправитель завершает работу.

В CCL вы должны расширить класс VideoCastConsumerImpl , чтобы отслеживать статус сеанса приведения:

private final VideoCastConsumer mCastConsumer = new VideoCastConsumerImpl() {
  public void onApplicationConnected(ApplicationMetadata appMetadata, 
                                     String sessionId,
                                     boolean wasLaunched) {}
  public void onDisconnectionReason(int reason) {}
  public void onDisconnected() {}
}

В CAF приложение-отправитель может быть уведомлено о событиях жизненного цикла сеанса путем регистрации SessionManagerListener с помощью SessionManager . Обратные вызовы SessionManagerListener определяют методы обратного вызова для всех событий жизненного цикла сеанса.

Следующие методы SessionManagerListener отображаются из интерфейса VideoCastConsumer CCL:

  • VideoCastConsumer.onApplicationConnected -> SessionManagerListener.onSessionStarted
  • VideoCastConsumer.onDisconnected -> SessionManagerListener.onSessionEnded

Объявите класс, реализующий интерфейс SessionManagerListener , и переместите логику VideoCastConsumerImpl в соответствующие методы:

private class CastSessionManagerListener implements SessionManagerListener<CastSession> {
  public void onSessionEnded(CastSession session, int error) {}
  public void onSessionStarted(CastSession session, String sessionId) {}
  public void onSessionEnding(CastSession session) {}
  ...
}

Класс CastSession представляет сеанс с устройством Cast. У класса есть методы для управления громкостью устройства и состояниями отключения звука, которые CCL делает в BaseCastManager .

Вместо использования CCL VideoCastManager для добавления потребителя:

VideoCastManager.getInstance().addVideoCastConsumer(mCastConsumer);

Теперь зарегистрируйте свой SessionManagerListener :

mCastSessionManager = 
    CastContext.getSharedInstance(this).getSessionManager();
mCastSessionManagerListener = new CastSessionManagerListener();
mCastSessionManager.addSessionManagerListener(mCastSessionManagerListener,
                  CastSession.class);

Чтобы прекратить прослушивание событий в CCL:

VideoCastManager.getInstance().removeVideoCastConsumer(mCastConsumer);

Теперь используйте SessionManager , чтобы прекратить прослушивание событий сеанса:

mCastSessionManager.removeSessionManagerListener(mCastSessionManagerListener,
                    CastSession.class);

Чтобы явно отключиться от устройства Cast, CCL использовал:

VideoCastManager.disconnectDevice(boolean stopAppOnExit, 
            boolean clearPersistedConnectionData,
            boolean setDefaultRoute)

Для CAF используйте SessionManager :

CastContext.getSharedInstance(this).getSessionManager()
                                   .endCurrentSession(true);

Чтобы определить, подключен ли отправитель к получателю, CCL предоставляет VideoCastManager.getInstance().isConnected() , но в CAF используется SessionManager :

public boolean isConnected() {
    CastSession castSession = CastContext.getSharedInstance(mAppContext)
                                  .getSessionManager()
                                  .getCurrentCastSession();
    return (castSession != null && castSession.isConnected());
}

В CAF уведомления об изменении состояния громкости/отключения звука по-прежнему доставляются с помощью методов обратного вызова в Cast.Listener ; эти слушатели зарегистрированы в CastSession . Все остальные уведомления о состоянии устройства доставляются через обратные вызовы CastStateListener ; эти слушатели зарегистрированы в CastSession . Убедитесь, что вы по-прежнему отменяете регистрацию прослушивателей, когда связанные фрагменты, действия или приложения переходят в фоновый режим.

Логика повторного подключения

CAF пытается восстановить сетевые подключения, потерянные из-за временной потери сигнала WiFi или других сетевых ошибок. Теперь это делается на уровне сеанса; сеанс может войти в состояние «приостановлено», когда соединение потеряно, и вернется в состояние «подключено», когда соединение будет восстановлено. Платформа заботится о повторном подключении к приложению-приемнику и повторном подключении любых каналов Cast как часть этого процесса.

CAF предоставляет собственную службу переподключения, поэтому вы можете удалить CCL ReconnectionService из своего манифеста:

<service android:name="com.google.android.libraries.cast.companionlibrary.cast.reconnection.ReconnectionService"/>

Вам также не нужны следующие разрешения в манифесте для логики переподключения:

<uses‐permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses‐permission android:name="android.permission.ACCESS_WIFI_STATE"/>

Служба переподключения CAF включена по умолчанию, но ее можно отключить с помощью CastOptions .

Кроме того, CAF также добавляет автоматическое возобновление сеанса, которое включено по умолчанию (и может быть отключено с помощью CastOptions ). Если приложение-отправитель переводится в фоновый режим или завершается (проведением пальцем или из-за сбоя) во время выполнения сеанса Cast, инфраструктура попытается возобновить этот сеанс, когда приложение-отправитель вернется на передний план или перезапустится; это автоматически обрабатывается SessionManager , который выдает соответствующие обратные вызовы для любых зарегистрированных экземпляров SessionManagerListener .

Регистрация пользовательского канала

CCL предоставляет два способа создания пользовательского канала сообщений получателю:

  • CastConfiguration позволяет указать несколько пространств имен, после чего CCL создаст для вас канал.
  • DataCastManager похож на VideoCastManager, но ориентирован на варианты использования, не связанные с мультимедиа.

Ни один из этих способов создания пользовательского канала не поддерживается CAF — вместо этого вы должны выполнить процедуру добавления пользовательского канала для своего приложения-отправителя.

Подобно CCL, для мультимедийных приложений нет необходимости явно регистрировать канал управления мультимедиа.

Медиа-контроль

В CAF класс RemoteMediaClient эквивалентен методам мультимедиа VideoCastManager . RemoteMediaClient.Listener эквивалентен методам VideoCastConsumer . В частности, методы onRemoteMediaPlayerMetadataUpdated и onRemoteMediaPlayerStatusUpdated класса VideoCastConsumer сопоставляются с onMetadataUpdated и onStatusUpdated класса RemoteMediaClient.Listener соответственно:

private class CastMediaClientListener implements RemoteMediaClient.Listener {

    @Override
    public void onMetadataUpdated() {
        setMetadataFromRemote();
    }

    @Override
    public void onStatusUpdated() {
        updatePlaybackState();
    }

    @Override
    public void onSendingRemoteMediaRequest() {
    }

    @Override
    public void onQueueStatusUpdated() {
    }

    @Override
    public void onPreloadStatusUpdated() {
    }
}

Нет необходимости явно инициализировать или регистрировать объект RemoteMediaClient ; платформа автоматически создаст экземпляр объекта и зарегистрирует базовый медиаканал во время начала сеанса, если подключаемое приложение-получатель поддерживает пространство имен мультимедиа.

Доступ к RemoteMediaClient можно получить как метод getRemoteMediaClient объекта CastSession .

CastSession castSession = CastContext.getSharedInstance(mAppContext)
                                     .getSessionManager()
                                     .getCurrentCastSession();
mRemoteMediaClient = castSession.getRemoteMediaClient();
mRemoteMediaClientListener = new CastMediaClientListener();

Вместо CCL:

VideoCastManager.getInstance().addVideoCastConsumer(mCastConsumer);

Теперь используйте CAF:

mRemoteMediaClient.addListener(mRemoteMediaClientListener);

В RemoteMediaClient можно зарегистрировать любое количество прослушивателей, что позволяет нескольким компонентам-отправителям совместно использовать один экземпляр RemoteMediaClient , связанный с сеансом.

VideoCastManager CCL предоставляет методы для управления воспроизведением мультимедиа:

VideoCastManager manager = VideoCastManager.getInstance();
if (manager.isRemoteMediaLoaded()) {
    manager.pause();
    mCurrentPosition = (int) manager.getCurrentMediaPosition();
}

Теперь они реализованы RemoteMediaClient в CAF:

if (mRemoteMediaClient.hasMediaSession()) {
    mRemoteMediaClient.pause();
    mCurrentPosition = 
        (int)mRemoteMediaClient.getApproximateStreamPosition();
}

В CAF все медиа-запросы, отправленные на RemoteMediaClient , возвращают RemoteMediaClient.MediaChannelResult через обратный вызов PendingResult , который можно использовать для отслеживания хода выполнения и возможного результата запроса.

И CCL, и CAF используют классы MediaInfo и MediaMetadata для представления элементов мультимедиа и для загрузки мультимедиа.

Для загрузки мультимедиа в CCL используется VideoCastManager :

VideoCastManager.getInstance().loadMedia(media, autoPlay, mCurrentPosition, customData);

В CAF RemoteMediaClient используется для загрузки носителя:

mRemoteMediaClient.load(media, autoPlay, mCurrentPosition, customData);

Чтобы получить информацию о Media и статус текущего сеанса мультимедиа на приемнике, CCL использует VideoCastManager :

MediaInfo mediaInfo = VideoCastManager.getInstance()
                                      .getRemoteMediaInformation();
int status = VideoCastManager.getInstance().getPlaybackStatus();
int idleReason = VideoCastManager.getInstance().getIdleReason();

В CAF используйте RemoteMediaClient для получения той же информации:

MediaInfo mediaInfo = mRemoteMediaClient.getMediaInfo();
int status = mRemoteMediaClient.getPlayerState();
int idleReason = mRemoteMediaClient.getIdleReason();

Вводный оверлей

Подобно CCL, CAF предоставляет настраиваемое представление IntroductoryOverlay для выделения кнопки Cast, когда она впервые отображается пользователям.

Вместо использования метода CCL VideoCastConsumer onCastAvailabilityChanged , чтобы узнать, когда отображать наложение, объявите CastStateListener , чтобы определить, когда кнопка Cast становится видимой, как только MediaRouter обнаруживает устройства Cast в локальной сети:

private IntroductoryOverlay mIntroductoryOverlay;
private MenuItem mMediaRouteMenuItem;

protected void onCreate(Bundle savedInstanceState) {
    ...
    mCastStateListener = new CastStateListener() {
        @Override
        public void onCastStateChanged(int newState) {
            if (newState != CastState.NO_DEVICES_AVAILABLE) {
                showIntroductoryOverlay();
            }
        }
    };
    mCastContext = CastContext.getSharedInstance(this);
    mCastContext.registerLifecycleCallbacksBeforeIceCreamSandwich(this, 
        savedInstanceState);
}

protected void onResume() {
    mCastContext.addCastStateListener(mCastStateListener);
    ...
}

protected void onPause() {
    mCastContext.removeCastStateListener(mCastStateListener);
    ...
}

Следите за экземпляром MediaRouteMenuItem :

public boolean onCreateOptionsMenu(Menu menu) {
   super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.browse, menu);
    mMediaRouteMenuItem = CastButtonFactory.setUpMediaRouteButton(
            getApplicationContext(), menu,
            R.id.media_route_menu_item);
    showIntroductoryOverlay();
    return true;
}

Убедитесь, что MediaRouteButton видна, чтобы можно было отобразить вводное наложение:

private void showIntroductoryOverlay() {
    if (mIntroductoryOverlay != null) {
        mIntroductoryOverlay.remove();
    }
    if ((mMediaRouteMenuItem != null) && mMediaRouteMenuItem.isVisible()) {
        new Handler().post(new Runnable() {
            @Override
            public void run() {
                mIntroductoryOverlay = new IntroductoryOverlay.Builder(
                        VideoBrowserActivity.this, mMediaRouteMenuItem)
                        .setTitleText(getString(R.string.introducing_cast))
                        .setOverlayColor(R.color.primary)
                        .setSingleTime()
                        .setOnOverlayDismissedListener(
                                new IntroductoryOverlay
                                    .OnOverlayDismissedListener() {
                                        @Override
                                        public void onOverlayDismissed() {
                                            mIntroductoryOverlay = null;
                                        }
                                })
                        .build();
                mIntroductoryOverlay.show();
            }
        });
    }
}

Взгляните на наш образец приложения, чтобы увидеть полный рабочий код для демонстрации вводного наложения.

Чтобы настроить стиль вступительного наложения, выполните процедуру « Настройка вступительного наложения ».

Мини-контроллер

Вместо MiniController CCL используйте MiniController CAF в MiniControllerFragment макета приложения действий, в которых вы хотите показать мини-контроллер:

<fragment
        android:id="@+id/cast_mini_controller"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        app:castShowImageThumbnail="true"
        android:visibility="gone"
        class="com.google.android.gms.cast.framework.media.widget.MiniControllerFragment" />

CAF не поддерживает ручную настройку, поддерживаемую MiniController CCL, а также не поддерживает функцию Autoplay .

Чтобы настроить стиль и кнопки мини-контроллера, выполните процедуру « Настройка мини-контроллера ».

Уведомление и экран блокировки

Подобно VideoCastNotificationService CCL, CAF предоставляет MediaNotificationService для управления отображением медиа-уведомлений при трансляции.

Вам нужно удалить следующее из вашего манифеста:

  • VideoIntentReceiver
  • VideoCastNotificationService

CCL поддерживает предоставление пользовательской службы уведомлений с помощью CastConfiguration.Builder ; который не поддерживается CAF.

Рассмотрим следующую инициализацию CastManager с использованием CCL:

VideoCastManager.initialize(
   getApplicationContext(),
   new CastConfiguration.Builder(
           context.getString(R.string.app_id))
       .addNotificationAction(
           CastConfiguration.NOTIFICATION_ACTION_PLAY_PAUSE,true)
       .addNotificationAction(
           CastConfiguration.NOTIFICATION_ACTION_DISCONNECT,true)
       .build());

Для эквивалентной конфигурации в CAF пакет SDK предоставляет NotificationsOptions.Builder , который помогает создавать элементы управления мультимедиа для уведомлений и экрана блокировки в приложении-отправителе. Элементы управления экраном уведомлений и блокировки можно включить с помощью CastOptions при инициализации CastContext .

public CastOptions getCastOptions(Context context) {
    NotificationOptions notificationOptions = 
        new NotificationOptions.Builder()
            .setActions(Arrays.asList(
                MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK,
                MediaIntentReceiver.ACTION_STOP_CASTING), new int[]{0, 1})
            .build();
    CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
             .setNotificationOptions(notificationOptions)
             .build();
    return new CastOptions.Builder()
             .setReceiverApplicationId(context.getString(R.string.app_id))
             .setCastMediaOptions(mediaOptions)
             .build();
}

Уведомления и элементы управления на экране блокировки всегда включены в CAF. Также обратите внимание, что кнопки воспроизведения/паузы и остановки каста предусмотрены по умолчанию. CAF автоматически отслеживает видимость действий для принятия решения о том, когда отображать медиа-уведомление, за исключением Gingerbread. (Для Gingerbread см. предыдущее примечание об использовании registerLifecycleCallbacksBeforeIceCreamSandwich() ; вызовы CCL VideoCastManager incrementUiCounter и decrementUiCounter должны быть удалены.)

Чтобы настроить кнопки, отображаемые в уведомлениях, выполните процедуру « Добавление элементов управления мультимедиа на экран уведомлений и блокировки» .

Расширенный контроллер

CCL предоставляет VideoCastControllerActivity и VideoCastControllerFragment для отображения расширенного контроллера при трансляции мультимедиа.

Вы можете удалить объявление VideoCastControllerActivity в манифесте.

В CAF нужно расширить ExpandedControllerActivity и добавить кнопку Cast .

Чтобы настроить стили и кнопки, отображаемые в расширенном контроллере, выполните процедуру Настройка расширенного контроллера .

Аудио фокус

Как и в случае с CCL, аудиофокус управляется автоматически.

Контроль громкости

Для Gingerbread требуется dispatchKeyEvent , как и для CCL. В ICS и выше как для CCL, так и для CAF регулировка громкости осуществляется автоматически.

CAF позволяет управлять громкостью трансляции с помощью жесткой кнопки громкости на телефоне в действиях ваших приложений, а также отображает визуальную полосу громкости при трансляции в поддерживаемых версиях. CAF также обрабатывает изменение громкости через жесткий том, даже если ваше приложение не находится впереди, заблокировано или даже если экран выключен.

Субтитры

В Android KitKat и более поздних версиях субтитры можно настроить в разделе «Настройки субтитров» в разделе «Настройки» > «Специальные возможности». Однако более ранние версии Android не имеют такой возможности. CCL справляется с этим, предоставляя пользовательские настройки для более ранних версий и делегируя системные настройки на KitKat и выше.

CAF не предоставляет пользовательских настроек для изменения предпочтений подписи. Вы должны удалить ссылки CaptionsPreferenceActivity в своем манифесте и XML-файле настроек.

TracksChooserDialog CCL больше не нужен, поскольку изменение дорожек скрытых субтитров обрабатывается расширенным пользовательским интерфейсом контроллера.

API скрытых субтитров в CAF похож на v2.

Журнала отладки

CAF не предоставляет параметры ведения журнала отладки.

Разное

Следующие функции CCL не поддерживаются в CAF:

  • Получение авторизации перед воспроизведением путем предоставления MediaAuthService
  • Настраиваемые сообщения пользовательского интерфейса

Примеры приложений

Взгляните на разницу для переноса примера приложения Universal Music Player для Android (uamp) с CCL на CAF.

У нас также есть учебники по кодам и примеры приложений , использующих CAF.