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

1. Предисловие

Адаптивное переключение потоков — один из методов многопотокового переключения. ExoPlayer, как мастер MediaCodec, не только имеет возможность реализовать комбинированное переключение разных потоков через MergingMediaSource, но и имеет возможность переключения на основе MGEG-DASH. HLS и протоколы сглаживания потока. Адаптивное переключение потоков. Конечно, условия команды должны полностью учитываться при выборе каждого варианта проекта.

Основные различия заключаются в следующем:

  • Метод MergingMediaSource больше подходит для ситуаций, когда численность команды ограничена, а поддержка фоновых сервисов ограничена. Нет необходимости больше думать о передаче и кодировании ресурсов. Достаточно обычного развертывания CDN, что более экономически эффективно. Адаптивная потоковая передача предъявляет относительно профессиональные требования, а также определенные требования к развертыванию серверов, нарезке ресурсов и их кодированию.
  • MergingMediaSource Метод может реализовать объединение потоков разных кодировок. К некоторым частям адаптивного потока, например HLS, предъявляются более строгие требования. Основное требование состоит в том, чтобы кодирование ts-фрагментов было максимально согласованным. добиться максимально возможного повторного использования MediaCodec. Конечно, в методе MergingMediaSource, если формат каждого потока не сильно отличается, видеодекодер может полностью передавать PPS, SPS или флеш. buffer Для реализации использования MediaCodec аудиодекодер может также реализовать мультиплексирование MediaCodec путем ввода определенных характеристик байтов.
  • В ExoPlayer MergingMediaSource данные одного типа (тип видео, тип аудио, тип субтитров и т. д.) из-за отсутствия необходимых параметров битрейта, Треки с похожим форматом не могут быть объединены в группу, поэтому используемый FixTrackSelection, естественно, не поддерживает автоматическое переключение нескольких потоков для одного и того же типа ресурсов. Адаптивное завершение потока может реализовать группировку формата и, наконец, создать AdaptiveTrackSelection. Динамически управляйте отдельными потоками.

2. Базовые знания

Содержание предисловия все еще немного абстрактно для новых разработчиков ExoPlayer. Давайте разберемся с ключевыми классами ExoPlayer, чтобы облегчить понимание содержания этой статьи.

  • Renderer: отвечает за определение возможности поддержки формата декодером.、Регистрация декодера、Уничтожение декодера、Мультиплексирование декодера、Выборочное чтение данных、рендеринг или вывод данных、пропущенные кадры、Такие функции, как пропуск кадров и синхронизация аудио и видео. ExoPlayer поддерживает дизассемблирование Рендерер, комбинация, выкл. и вкл.,Также поддерживает доступ к пользовательскому декодеру.,Например, SimpleDecoder используется для реализации декодирования и рендеринга FFMPEG видео и аудио.
  • Медиа-часы MediaClock: отвечают за синхронизацию аудио и видео, управление ходом воспроизведения и т. д. В ExoPlayer China есть два тактовых сигнала: один — независимый тактовый генератор StandaloneMediaClock, а другой — тактовый сигнал режима Audio Master, реализованный через аудиорендерер.
  • Декапсулятор экстрактора: отвечает за извлечение информации Moov из каждого потока медиаресурса.、Таблица выборки、Format、Данные выборки (например, СПС, ППС, различные рамы (данные) разбираются,В то же время некоторые данные,Удобно для выбора трека и формата, а также переключения потоков. ExoPlayer имеет большое количество встроенных декапсуляторов.,Также поддерживаются индивидуальные экстракторы для достижения конкретных целей. конечно,Анализ формата адаптивного потока обычно выполняется через MediaSource.,С помощью Extractor необходимо проанализировать только видеоконтейнер.
  • Декодер-декодер: отвечает за декодирование образцов данных.,Среди них MediaCodec имеет возможность использовать как жесткие, так и программные решения.,И поддерживает рендеринг.
  • TrackSelector: основной класс многопоточного переключения, отвечающий за группировку TrackSelection посредством определения формата и объединение TrackGroup и Renderer, TrackGroup и TrackSelection.
  • DataSource данныеисточник:Ответственный за предоставлениеданные。
  • Источник мультимедиа MediaSource: в ExoPlayer, благодаря абстракции MediaSource от DataSource, ExoPlayer является более гибким и удобным в управлении многоканальными потоками.
  • SeekPoint: в ExoPlayer SeekPoint часто является позицией, в которой вот-вот начнется кадр IDR.
  • FixTrackSelection: фиксированный селектор потока.,TrackSelection остается фиксированным во время воспроизведения,Обычный протокол по умолчанию TrackSelection,Но и по этой причине,Это делает его пригодным для многопоточного переключения в режиме MergingMediaSource.
  • AdptiveTrackSelection: адаптивный селектор потока, который может динамически выбирать фрагменты на основе пропускной способности. Конечно, некоторые стратегии можно использовать, чтобы позволить пользователям переключаться самостоятельно, аналогично переключению потока кода в bilibili.
  • TrackGroup: формат отслеживания группы ресурсов одного типа.
  • MapTrackInfo: этот класс на самом деле представляет собой класс коллекции информации, связанной с Renderer и TrackGroup.,В основном сохраняет информацию о возможностях рендерера и информацию о группе треков.,В некоторой степени вы можете увидеть формат данных и глобальную информацию Renderer.
  • Принцип группировки выбора: TrackSelector группирует ресурсы с помощью SELECTION_ELIGIBILITY_FIXED и SELECTION_ELIGIBILITY_ADAPTIVE. Конечно, предполагается, что различные форматы совместимы друг с другом. Для конкретной логики совместимости обратитесь к классам VideoTrackInfo и AudioTrackInfo.
  • Пропускная способность: важный инструмент для определения скорости сети в ExoPlayer. Результаты обнаружения используются в AdaptiveTrackSelection для выбора фрагмента.
  • DefaultMediaSourceFactory используется для реализации фабрики маршрутизации, которая преобразует DataSource в MediaSource. Он определяет тип MediaSource, созданный с помощью mineType и суффикса uri.

3. Анализ адаптивного переключения потоков

3.1 Принципиальная схема

Автоматическое переключение на медиапоток, совместимый с текущим битрейтом на разных скоростях сети. Условия соответствия обычно задаются заранее в файле манифеста адаптивного потока. Убедитесь, что битрейт текущей сети превышает минимальную ширину полосы пропускания. медиапоток в протоколе манифеста. Переключиться на указанный медиапоток Track.

Из принципиальной схемы мы можем узнать следующую информацию:

  • По умолчанию адаптивное переключение потоков не требует поиска SeekPoint, а реализуется путем выбора следующего шарда.
  • По умолчанию адаптивная потоковая передача реализует переключение сегментов посредством определения скорости сети.
  • Как видно из рисунка, время воспроизведения каждого фрагмента и начальная позиция I-кадра также должны быть строго согласованы.

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

3.2 Основная логика

Основная логика в основном:

  • Анализ файла манифеста
  • Установите взаимосвязь между Renderer, TrackGroup и Selection.
  • Начать сегментированную загрузку
  • Определение скорости и пропускной способности сети и AdaptiveTrackSelection для выбора подходящих фрагментов
  • Повторное использование или перезапуск декодера
  • Полный переключатель

3.2.1 Анализ файла манифеста адаптивного потока

ExoPlayer поддерживает протоколы DASH, HLS и Smoothing-Stream. Здесь мы используем протоколы HLS и DASH для анализа процессов. В конце концов, сама Microsoft — единственная, кто использует Smoothing-Stream. Далее давайте посмотрим на файлы манифеста HLS и DASH, чтобы облегчить последующее тестирование.

3.2.1.1 Файл манифеста протокола hls

Язык кода:javascript
копировать
#EXTM3U

#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",LANGUAGE="eng",NAME="BipBop Audio 1",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",LANGUAGE="eng",NAME="BipBop Audio 2",AUTOSELECT=NO,DEFAULT=NO,URI="alternate_audio_aac/prog_index.m3u8"


#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="en",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/eng/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="en",URI="subtitles/eng_forced/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="fr",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/fra/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="fr",URI="subtitles/fra_forced/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="es",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/spa/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="es",URI="subtitles/spa_forced/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=СУБТИТРЫ,GROUP-ID="subs",NAME="Японский",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="ja",CHARACTERISTICS="public.accessibility" .transcribes-разговорный-диалог, public.accessibility.describes-music-and-sound",URI="subtitles/jpn/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=СУБТИТРЫ,GROUP-ID="subs",NAME="Японский (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="ja",URI="subtitles/jpn_forced/prog_index.m3u8"


#EXT-X-STREAM-INF:BANDWIDTH=263851,CODECS="mp4a.40.2, avc1.4d400d",RESOLUTION=416x234,AUDIO="bipbop_audio",SUBTITLES="subs"
gear1/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=28451,CODECS="avc1.4d400d",URI="gear1/iframe_index.m3u8"

#EXT-X-STREAM-INF:BANDWIDTH=577610,CODECS="mp4a.40.2, avc1.4d401e",RESOLUTION=640x360,AUDIO="bipbop_audio",SUBTITLES="subs"
gear2/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=181534,CODECS="avc1.4d401e",URI="gear2/iframe_index.m3u8"

#EXT-X-STREAM-INF:BANDWIDTH=915905,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=960x540,AUDIO="bipbop_audio",SUBTITLES="subs"
gear3/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=297056,CODECS="avc1.4d401f",URI="gear3/iframe_index.m3u8"

#EXT-X-STREAM-INF:BANDWIDTH=1030138,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1280x720,AUDIO="bipbop_audio",SUBTITLES="subs"
gear4/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=339492,CODECS="avc1.4d401f",URI="gear4/iframe_index.m3u8"

#EXT-X-STREAM-INF:BANDWIDTH=1924009,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1920x1080,AUDIO="bipbop_audio",SUBTITLES="subs"
gear5/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=669554,CODECS="avc1.4d401f",URI="gear5/iframe_index.m3u8"

#EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS="mp4a.40.2",AUDIO="bipbop_audio",SUBTITLES="subs"
gear0/prog_index.m3u8

3.2.1.2 Файл манифеста протокола DASH

Язык кода:javascript
копировать
<?xml version="1.0" encoding="UTF-8"?>
<!--Generated with https://github.com/google/shaka-packager version 97fc982-release-->
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" xmlns:cenc="urn:mpeg:cenc:2013" minBufferTime="PT2S" type="static" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" mediaPresentationDuration="PT734S">
  <Period id="0">
    <AdaptationSet id="0" contentType="audio" lang="en">
      <Representation id="0" bandwidth="131596" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
        <AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
        <BaseURL>tears_audio_eng.mp4</BaseURL>
        <SegmentBase indexRange="745-1664" timescale="44100">
          <Initialization range="0-744"/>
        </SegmentBase>
      </Representation>
    </AdaptationSet>
    <AdaptationSet id="1" contentType="video" maxWidth="1920" maxHeight="856" frameRate="12288/512" par="38:17">
      <Representation id="1" bandwidth="769255" codecs="avc1.42c01e" mimeType="video/mp4" sar="852:857" width="320" height="142">
        <BaseURL>tears_h264_baseline_240p_800.mp4</BaseURL>
        <SegmentBase indexRange="827-1602" timescale="12288">
          <Initialization range="0-826"/>
        </SegmentBase>
      </Representation>
      <Representation id="2" bandwidth="1774254" codecs="avc1.4d401f" mimeType="video/mp4" sar="2242:2249" width="854" height="380">
        <BaseURL>tears_h264_main_480p_2000.mp4</BaseURL>
        <SegmentBase indexRange="829-1604" timescale="12288">
          <Initialization range="0-828"/>
        </SegmentBase>
      </Representation>
      <Representation id="3" bandwidth="7203938" codecs="avc1.4d4028" mimeType="video/mp4" sar="855:857" width="1280" height="570">
        <BaseURL>tears_h264_main_720p_8000.mp4</BaseURL>
        <SegmentBase indexRange="830-1605" timescale="12288">
          <Initialization range="0-829"/>
        </SegmentBase>
      </Representation>
      <Representation id="4" bandwidth="18316946" codecs="avc1.64002a" mimeType="video/mp4" sar="856:857" width="1920" height="856">
        <BaseURL>tears_h264_high_1080p_20000.mp4</BaseURL>
        <SegmentBase indexRange="832-1607" timescale="12288">
          <Initialization range="0-831"/>
        </SegmentBase>
      </Representation>
    </AdaptationSet>
  </Period>
</MPD>

Мы явно можем найти много общего между двумя протоколами:

  • bandWidth: Пропускная способность сети — это скорость загрузки, но в файле манифеста обычно выражается минимальная скорость загрузки, которая поддерживает поток данных.
  • mimeType: тип ресурса
  • кодеки: тип кодировки ресурса
  • ширина: ширина видео
  • height: видеовысокий

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

При анализе файла манифеста, если используется протокол HLS, ExoPlayer внутренне использует класс HlsPlaylistParser в качестве инструмента анализа файла манифеста. Если это DASH, для анализа манифеста используется DashManifestParser, и т. д. Smoothing-stream использует SsmanifestParser для анализа. файл манифеста.

Процесс анализа в основном выглядит следующим образом.

  • Используйте DefaultMediaSourceFactory, чтобы создать соответствующий адаптивный поток MediaSource, например HlsMediaSource, DashMediaSource и SsMediaSource.
  • Адаптивный поток MediaSource создает инструмент синтаксического анализа Parser. Конечно, HlsMediaSource является особенным. Он не содержит Parser напрямую, а планирует его работу через временную шкалу DefaultHlsPlaylistTracker.
  • Create Loader и ParsingLoadable аналогичен Runnable и принадлежит задаче загрузки. В ParsingLoadable его можно загружать и анализировать одновременно.
  • Сгруппируйте каждый набор форматов треков и используйте TrackGroup для сохранения информации о группировке.

3.2.2 Модуль рендеринга, группа треков и выбор

В ExoPlayer за эту работу в основном отвечает DefaultTrackSelector. Основная логика следующая.

Язык кода:javascript
копировать
  @Override
  protected final Pair<@NullableType RendererConfiguration[], @NullableType ExoTrackSelection[]>
      selectTracks(
          MappedTrackInfo mappedTrackInfo,
          @Capabilities int[][][] rendererFormatSupports,
          @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupport,
          MediaPeriodId mediaPeriodId,
          Timeline timeline)
          throws ExoPlaybackException {
    Parameters parameters;
    synchronized (lock) {
      parameters = this.parameters;
      if (parameters.constrainAudioChannelCountToDeviceCapabilities
          && Util.SDK_INT >= 32
          && spatializer != null) {
        // Initialize the spatializer now so we can get a reference to the playback looper with
        // Looper.myLooper().
        spatializer.ensureInitialized(this, checkStateNotNull(Looper.myLooper()));
      }
    }
    int rendererCount = mappedTrackInfo.getRendererCount();

    // Создание рендереров и TrackGroup При сопоставлении обратите внимание, что MappedTrackInfo, о котором мы упоминали ранее, сохраняет информацию, связанную с Renderer и TrackGroup.
    // Здесь мы также попробуем разные TrackGroup Формат объединяется и группируется в определения на основе совместимости.
    ExoTrackSelection.@NullableType Definition[] definitions =
        selectAllTracks(
            mappedTrackInfo,
            rendererFormatSupports,
            rendererMixedMimeTypeAdaptationSupport,
            parameters);

    //Для отфильтрованных определений Вторичная фильтрация
    applyTrackSelectionOverrides(mappedTrackInfo, parameters, definitions);
    applyLegacyRendererOverrides(mappedTrackInfo, parameters, definitions);


    //Тройная фильтрация, отмену отображения нельзя использовать в Renderer
    // Disable renderers if needed.
    for (int i = 0; i < rendererCount; i++) {
      @C.TrackType int rendererType = mappedTrackInfo.getRendererType(i);
      if (parameters.getRendererDisabled(i)
          || parameters.disabledTrackTypes.contains(rendererType)) {
        definitions[i] = null;
      }
    }

  //Устанавливаем связь между TrackGroup и Selection и передаем BandwidthMeter, чтобы получить данные политики скорости сети.
    @NullableType
    ExoTrackSelection[] rendererTrackSelections =
        trackSelectionFactory.createTrackSelections(
            definitions, getBandwidthMeter(), mediaPeriodId, timeline);

    // Initialize the renderer configurations to the default configuration for all renderers with
    // selections, and null otherwise.
//Создаем конфигурацию инициализации рендерера
    @NullableType
    RendererConfiguration[] rendererConfigurations = new RendererConfiguration[rendererCount];
    for (int i = 0; i < rendererCount; i++) {
      @C.TrackType int rendererType = mappedTrackInfo.getRendererType(i);
      boolean forceRendererDisabled =
          parameters.getRendererDisabled(i) || parameters.disabledTrackTypes.contains(rendererType);
      boolean rendererEnabled =
          !forceRendererDisabled
              && (mappedTrackInfo.getRendererType(i) == C.TRACK_TYPE_NONE
                  || rendererTrackSelections[i] != null);
      rendererConfigurations[i] = rendererEnabled ? RendererConfiguration.DEFAULT : null;
    }

    // Configure audio and video renderers to use tunneling if appropriate.
    if (parameters.tunnelingEnabled) {
     //Это туннельный рендеринг. Судя по всему, конкретный процесс заключается в том, что MediaCodec не отвечает за декодирование. Некодированные данные могут выводиться на уровень драйвера и обрабатываться им. Например, аудиоданные ac3 напрямую выводятся в AudioTrack. Информации по этому поводу слишком мало.
      maybeConfigureRenderersForTunneling(
          mappedTrackInfo, rendererFormatSupports, rendererConfigurations, rendererTrackSelections);
    }

    return Pair.create(rendererConfigurations, rendererTrackSelections);
  }

Эта часть логики относительно сложна, но нам com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection.Factory#createTrackSelections необходимо провести необходимый анализ.

Заводская адаптация Выбор используется по умолчанию в ExoPlayer. Конкретная логика следующая.

Язык кода:javascript
копировать
   @Override
    public final @NullableType ExoTrackSelection[] createTrackSelections(
        @NullableType Definition[] definitions,
        BandwidthMeter bandwidthMeter,
        MediaPeriodId mediaPeriodId,
        Timeline timeline) {
      ImmutableList<ImmutableList<AdaptationCheckpoint>> adaptationCheckpoints =
          getAdaptationCheckpoints(definitions);
      ExoTrackSelection[] selections = new ExoTrackSelection[definitions.length];
      for (int i = 0; i < definitions.length; i++) {
        @Nullable Definition definition = definitions[i];
        if (definition == null || definition.tracks.length == 0) {
          continue;
        }
       //Проходим по всем определениям и сравниваем треки в группе Создайте фиксированныйTrackSelection, если количество равно 1, и создайте AdaptiveTrackSelection, если их несколько.
        selections[i] =
            definition.tracks.length == 1
                ? new FixedTrackSelection(
                    definition.group,
                    /* track= */ definition.tracks[0],
                    /* type= */ definition.type)
                : createAdaptiveTrackSelection(
                    definition.group,
                    definition.tracks,
                    definition.type,
                    bandwidthMeter,
                    adaptationCheckpoints.get(i));
      }
      return selections;
    }

На этом этапе сопоставление между Renderer, TrackGroup и выделением завершено.

Итак, вот вопрос. Если вы используете MergingMediaSource для объединения нескольких потоков и изменения параметров, можете ли вы также реализовать AdaptiveTrackSelection и протестировать адаптивные возможности? Ответ — нет, потому что MergingMediaSource объединяет полные ресурсы и не вызывает методы, связанные с TrackSelection, во время использования. Конечно, ExoPlayer не реализует динамическое разделение ресурсов.

3.2.3 Загрузка осколков

Когда DASH, HLS и Smoothing-Stream загружают сегменты, каждый сегмент использует свой собственный реализованный класс ChunkSource. Однако при наличии нескольких сегментов ExoPlayer использует ChunkSampleStream и HlsSampleStreamWrapper для управления очередью сегментов. Основной метод — это реализация в continueLoading. то же самое. Здесь в качестве ссылки используется HlsSampleStreamWrapper.

Язык кода:javascript
копировать
 public boolean continueLoading(long positionUs) {
    if (loadingFinished || loader.isLoading() || loader.hasFatalError()) {
      return false;
    }

    boolean pendingReset = isPendingReset();
    //Получаем ресурсы
    List<BaseMediaChunk> chunkQueue; 
   //Получаем время и позицию последнего загруженного ресурса
    long loadPositionUs;
    if (pendingReset) {
      chunkQueue = Collections.emptyList();
      loadPositionUs = pendingResetPositionUs;
    } else {
      chunkQueue = readOnlyMediaChunks;
      loadPositionUs = getLastMediaChunk().endTimeUs;
    }
//Получаем следующий фрагмент
    chunkSource.getNextChunk(positionUs, loadPositionUs, chunkQueue, nextChunkHolder);
    boolean endOfStream = nextChunkHolder.endOfStream;
    @Nullable Chunk loadable = nextChunkHolder.chunk;
    nextChunkHolder.clear();

    if (endOfStream) {
    // Если ресурсов для загрузки нет, отметьте конец загрузки
      pendingResetPositionUs = C.TIME_UNSET;
      loadingFinished = true;
      return true;
    }

    if (loadable == null) {
      return false;
    }
    //Следующая логика является прямой оценкой статуса загрузки
    loadingChunk = loadable;
    if (isMediaChunk(loadable)) {
      BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable;
      if (pendingReset) {
        // Only set the queue start times if we're not seeking to a chunk boundary. If we are
        // seeking to a chunk boundary then we want the queue to pass through all of the samples in
        // the chunk. Doing this ensures we'll always output the keyframe at the start of the chunk,
        // even if its timestamp is slightly earlier than the advertised chunk start time.
        if (mediaChunk.startTimeUs != pendingResetPositionUs) {
          primarySampleQueue.setStartTimeUs(pendingResetPositionUs);
          for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
            embeddedSampleQueue.setStartTimeUs(pendingResetPositionUs);
          }
        }
        pendingResetPositionUs = C.TIME_UNSET;
      }
      mediaChunk.init(chunkOutput);
      mediaChunks.add(mediaChunk);
    } else if (loadable instanceof InitializationChunk) {
      ((InitializationChunk) loadable).init(chunkOutput);
    }
    long elapsedRealtimeMs =
        loader.startLoading(
            loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type));
    mediaSourceEventDispatcher.loadStarted(
        new LoadEventInfo(loadable.loadTaskId, loadable.dataSpec, elapsedRealtimeMs),
        loadable.type,
        primaryTrackType,
        loadable.trackFormat,
        loadable.trackSelectionReason,
        loadable.trackSelectionData,
        loadable.startTimeUs,
        loadable.endTimeUs);
    return true;
  }

getNextChunk — это основной метод continueLoading, продолжайте изучать код.

Язык кода:javascript
копировать
  public final void getNextChunk(
      long playbackPositionUs,
      long loadPositionUs,
      List<? extends MediaChunk> queue,
      ChunkHolder out) {
    if (fatalError != null) {
      return;
    }

    StreamElement streamElement = manifest.streamElements[streamElementIndex];
    if (streamElement.chunkCount == 0) {
      // There aren't any chunks for us to load.
      //Нет осколков для загрузки
      out.endOfStream = !manifest.isLive;
      return;
    }

    int chunkIndex;  //Получаем индекс последнего загруженного фрагмента
    if (queue.isEmpty()) {
      chunkIndex = streamElement.getChunkIndex(loadPositionUs);
    } else {
      chunkIndex =
          (int) (queue.get(queue.size() - 1).getNextChunkIndex() - currentManifestChunkOffset);
      if (chunkIndex < 0) {
      //Этого фрагмента не существует, что указывает на то, что фрагмент загрузки неполный
        // This is before the first chunk in the current manifest.
        fatalError = new BehindLiveWindowException();
        return;
      }
    }

    if (chunkIndex >= streamElement.chunkCount) {
      // This is beyond the last chunk in the current manifest.
      //Этот индекс недопустим, прекратите загрузку напрямую
      out.endOfStream = !manifest.isLive;
      return;
    }

    long bufferedDurationUs = loadPositionUs - playbackPositionUs;
    long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs);

    MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()];
    for (int i = 0; i < chunkIterators.length; i++) {
      int trackIndex = trackSelection.getIndexInTrackGroup(i);
      chunkIterators[i] = new StreamElementIterator(streamElement, trackIndex, chunkIndex);
    }

    //Самая важная часть заключается в том, что следующий фрагмент будет отфильтрован в зависимости от скорости сети.
    trackSelection.updateSelectedTrack(
        playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, chunkIterators);

    long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex);
    long chunkEndTimeUs = chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex);
    long chunkSeekTimeUs = queue.isEmpty() ? loadPositionUs : C.TIME_UNSET;
    int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset;

    //Вот ключевой момент, получаем следующий фрагмент, который нужно загрузить.
    int trackSelectionIndex = trackSelection.getSelectedIndex();

   //Получаем декапсулятор фрагмента
    ChunkExtractor chunkExtractor = chunkExtractors[trackSelectionIndex];

    int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex);
    Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex);
    //Привязываем информацию о фрагменте для загрузки
    out.chunk =
        newMediaChunk(
            trackSelection.getSelectedFormat(),
            dataSource,
            uri,
            currentAbsoluteChunkIndex,
            chunkStartTimeUs,
            chunkEndTimeUs,
            chunkSeekTimeUs,
            trackSelection.getSelectionReason(),
            trackSelection.getSelectionData(),
            chunkExtractor);
  }

3.2.4 Определение скорости сети и выбор фрагмента AdaptiveTrackSelection

DefaultBandWidthMeter по умолчанию используется для определения скорости сети. Конкретный принцип заключается в мониторинге трафика загрузки данных в течение определенного периода времени и расчете средней скорости сети. AdaptiveTrack Selection#updateSelectedTrack будет использовать данные измерения выбора для пересчета данных, какую очередь использовать.

Язык кода:javascript
копировать
int newSelectedIndex = determineIdealSelectedIndex(nowMs, chunkDurationUs);

Основная логика заключается в сравнении битрейта.

Язык кода:javascript
копировать
 @Override
  public void updateSelectedTrack(
      long playbackPositionUs,
      long bufferedDurationUs,
      long availableDurationUs,
      List<? extends MediaChunk> queue,
      MediaChunkIterator[] mediaChunkIterators) {
    long nowMs = clock.elapsedRealtime();
    long chunkDurationUs = getNextChunkDurationUs(mediaChunkIterators, queue);

    // Make initial selection
    if (reason == C.SELECTION_REASON_UNKNOWN) {
      reason = C.SELECTION_REASON_INITIAL;
      //После инициализации или если он еще не выбран, сразу выберите его напрямую, не проходя процесс совместимости.
      selectedIndex = determineIdealSelectedIndex(nowMs, chunkDurationUs);
      return;
    }

    int previousSelectedIndex = selectedIndex;
    @C.SelectionReason int previousReason = reason;
    int formatIndexOfPreviousChunk =
        queue.isEmpty() ? C.INDEX_UNSET : indexOf(Iterables.getLast(queue).trackFormat);
    if (formatIndexOfPreviousChunk != C.INDEX_UNSET) {
      previousSelectedIndex = formatIndexOfPreviousChunk;
      previousReason = Iterables.getLast(queue).trackSelectionReason;
    }
   //Выбираем формат, соответствующий битрейту
    int newSelectedIndex = determineIdealSelectedIndex(nowMs, chunkDurationUs);
    //Оцениваем, нет ли трека, на котором находится формат, в черном списке, тогда используем логику совместимости
    if (!isBlacklisted(previousSelectedIndex, nowMs)) {
      // Revert back to the previous selection if conditions are not suitable for switching.
      Format currentFormat = getFormat(previousSelectedIndex);
      Format selectedFormat = getFormat(newSelectedIndex);
      //Примечание: вход в это место является ловушкой. Если поток кода дает сбой, причина в том, что он снова сбрасывается из-за некоторых условий.
      long minDurationForQualityIncreaseUs =
          minDurationForQualityIncreaseUs(availableDurationUs, chunkDurationUs);
      if (selectedFormat.bitrate > currentFormat.bitrate
          && bufferedDurationUs < minDurationForQualityIncreaseUs) {
        // The selected track is a higher quality, but we have insufficient buffer to safely switch
        // up. Defer switching up for now.
        //Если выбранный поток кода больше текущего, но данных буферизации недостаточно для безопасного переключения, поэтому текущий трек все равно выбран. 
        newSelectedIndex = previousSelectedIndex;
      } else if (selectedFormat.bitrate < currentFormat.bitrate
          && bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
        // The selected track is a lower quality, but we have sufficient buffer to defer switching
        // down for now.
        //Выбранный поток кода меньше текущего, но буфера данных достаточно, чтобы не пришлось переключаться.
        newSelectedIndex = previousSelectedIndex;
      }
    }
    // If we adapted, update the trigger.
//условие выбора триггера
    reason =
        newSelectedIndex == previousSelectedIndex ? previousReason : C.SELECTION_REASON_ADAPTIVE;
    selectedIndex = newSelectedIndex;
  }

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

Основной момент — определениеIdealSelectedIndex. Логика здесь заключается в том, чтобы получить текущую пропускную способность и затем сопоставить очередь выборки в Selection.

Язык кода:javascript
копировать
 private int determineIdealSelectedIndex(long nowMs, long chunkDurationUs) {
     //Получаем данные о пропускной способности
    long effectiveBitrate = getAllocatedBandwidth(chunkDurationUs);
    int lowestBitrateAllowedIndex = 0;
    for (int i = 0; i < length; i++) {
      if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) {
        Format format = getFormat(i);
       //Получаем формат, который меньше текущей скорости сети
        if (canSelectFormat(format, format.bitrate, effectiveBitrate)) {
          return i;
        } else {
          lowestBitrateAllowedIndex = i;
        }
      }
    }
    return lowestBitrateAllowedIndex;
  }

Сбор данных о пропускной способности

Язык кода:javascript
копировать
private long getTotalAllocatableBandwidth(long chunkDurationUs) {
  //Обратите внимание, что пропускная способность x0,7f здесь на самом деле меньше фактического значения, поэтому при настройке скорости сети ее необходимо разделить на 0,7f.
    long cautiousBandwidthEstimate =
        (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction);

    long timeToFirstByteEstimateUs = bandwidthMeter.getTimeToFirstByteEstimateUs();
    if (timeToFirstByteEstimateUs == C.TIME_UNSET || chunkDurationUs == C.TIME_UNSET) {
    //Здесь по умолчанию полоса пропускания делится на текущую скорость воспроизведения (двойная скорость)
      return (long) (cautiousBandwidthEstimate / playbackSpeed); 
    }
    float availableTimeToLoadUs =
        max(chunkDurationUs / playbackSpeed - timeToFirstByteEstimateUs, 0);
    return (long) (cautiousBandwidthEstimate * availableTimeToLoadUs / chunkDurationUs);
  }

3.2.5 Мультиплексирование декодера и перезапуск

Поскольку формат фрагмента каждой очереди выборки несколько отличается, декодеру может потребоваться обнаружение изменений формата. В это время может потребоваться перезапуск декодера. Конечно, перезапуск — это самый простой способ переключения между несколькими потоками. Однако это часто приводит к зависаниям или кратковременному черному экрану, что оставляет желать лучшего. ExoPlayer провел значительную оптимизацию для вызова onInputFormatChanged из-за многопоточного переключения в методе MergingMediaSource или адаптивного переключения потоков, тем самым позволяя повторно использовать декодеры.

Основная логика заключается в следующем:

Язык кода:javascript
копировать
  protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder)
      throws ExoPlaybackException {
    waitingForFirstSampleInFormat = true;
    Format newFormat = checkNotNull(formatHolder.format);
    if (newFormat.sampleMimeType == null) {
      // If the new format is invalid, it is either a media bug or it is not intended to be played.
      // See also https://github.com/google/ExoPlayer/issues/8283.

      throw createRendererException(
          new IllegalArgumentException(),
          newFormat,
          PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED);
    }
    setSourceDrmSession(formatHolder.drmSession);
    inputFormat = newFormat;

    if (bypassEnabled) {
     //Подождем, пока очередьданные очистятся, а затем инициализируем
      bypassDrainAndReinitialize = true;
      return null; // Need to drain batch buffer first.
    }

    //Если кодек выпущен или не создан, инициализируем его повторно
    if (codec == null) {
      availableCodecInfos = null;
      maybeInitCodecOrBypass();
      return null;
    }

    // We have an existing codec that we may need to reconfigure, re-initialize, or release to
    // switch to bypass. If the existing codec instance is kept then its operating rate and DRM
    // session may need to be updated.

    // Copy the current codec and codecInfo to local variables so they remain accessible if the
    // member variables are updated during the logic below.
    MediaCodecAdapter codec = this.codec;
    MediaCodecInfo codecInfo = this.codecInfo;

    Format oldFormat = codecInputFormat;
    if (drmNeedsCodecReinitialization(codecInfo, newFormat, codecDrmSession, sourceDrmSession)) {
      drainAndReinitializeCodec();
      return new DecoderReuseEvaluation(
          codecInfo.name,
          oldFormat,
          newFormat,
          REUSE_RESULT_NO,
          DISCARD_REASON_DRM_SESSION_CHANGED);
    }
    boolean drainAndUpdateCodecDrmSession = sourceDrmSession != codecDrmSession;
    Assertions.checkState(!drainAndUpdateCodecDrmSession || Util.SDK_INT >= 23);

   //Сравнивая условия повторного использования старых и новых декодеров, кодов здесь слишком много и подробно обсуждать не будем.
    DecoderReuseEvaluation evaluation = canReuseCodec(codecInfo, oldFormat, newFormat);
    @DecoderDiscardReasons int overridingDiscardReasons = 0;
    switch (evaluation.result) {
      case REUSE_RESULT_NO:
//Не использовать повторно, перезапустить напрямую
        drainAndReinitializeCodec();
        break;
      case REUSE_RESULT_YES_WITH_FLUSH:
//Можно использовать повторно, но необходимо очистить. В этом случае необходимо вызвать MediaCodec.flush.
        if (!updateCodecOperatingRate(newFormat)) {
          overridingDiscardReasons |= DISCARD_REASON_OPERATING_RATE_CHANGED;
        } else {
          codecInputFormat = newFormat;
          if (drainAndUpdateCodecDrmSession) {
            if (!drainAndUpdateCodecDrmSessionV23()) {
              overridingDiscardReasons |= DISCARD_REASON_WORKAROUND;
            }
          } else if (!drainAndFlushCodec()) {
            overridingDiscardReasons |= DISCARD_REASON_WORKAROUND;
          }
        }
        break;
      case REUSE_RESULT_YES_WITH_RECONFIGURATION:
//Его можно использовать повторно, но необходимо перерегистрировать информацию. Если это видео, необходимо перерегистрировать информацию SPS и PPS. Конкретная информация находится в .
//com.google.android.exoplayer2.Format.Builder#initializationData , если звук обычно Тип ресурса
        if (!updateCodecOperatingRate(newFormat)) {
          overridingDiscardReasons |= DISCARD_REASON_OPERATING_RATE_CHANGED;
        } else {
          codecReconfigured = true;
          codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
          codecNeedsAdaptationWorkaroundBuffer =
              codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS
                  || (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION
                      && newFormat.width == oldFormat.width
                      && newFormat.height == oldFormat.height);
          codecInputFormat = newFormat;
          if (drainAndUpdateCodecDrmSession && !drainAndUpdateCodecDrmSessionV23()) {
            overridingDiscardReasons |= DISCARD_REASON_WORKAROUND;
          }
        }
        break;
      case REUSE_RESULT_YES_WITHOUT_RECONFIGURATION:
    //Повторно используем декодер напрямую без регистрации какой-либо информации
        if (!updateCodecOperatingRate(newFormat)) {
          overridingDiscardReasons |= DISCARD_REASON_OPERATING_RATE_CHANGED;
        } else {
          codecInputFormat = newFormat;
          if (drainAndUpdateCodecDrmSession && !drainAndUpdateCodecDrmSessionV23()) {
            overridingDiscardReasons |= DISCARD_REASON_WORKAROUND;
          }
        }
        break;
      default:
        throw new IllegalStateException(); // Never happens.
    }

    if (evaluation.result != REUSE_RESULT_NO
        && (this.codec != codec || codecDrainAction == DRAIN_ACTION_REINITIALIZE)) {
      // Initial evaluation indicated reuse was possible, but codec re-initialization was triggered.
      // The reasons are indicated by overridingDiscardReasons.
      return new DecoderReuseEvaluation(
          codecInfo.name, oldFormat, newFormat, REUSE_RESULT_NO, overridingDiscardReasons);
    }

    return evaluation;
  }

3.2.6 Завершение переключения

С помощью приведенной выше необходимой логики можно добиться переключения шардов. Конечно, объем кода в каждой части слишком велик, включая часть загрузки ресурсов, которая также является основной ссылкой. Я не буду здесь продолжать анализ.

Но как проверить, что переключение действительно выполнено? Обратитесь к реализации интерфейса ниже.

Язык кода:javascript
копировать
com.google.android.exoplayer2.audio.AudioRendererEventListener#onAudioInputFormatChanged(com.google.android.exoplayer2.Format)
com.google.android.exoplayer2.video.VideoRendererEventListener#onVideoInputFormatChanged(com.google.android.exoplayer2.Format)

4. Экспериментируйте

4.1 Экспериментальная цель

Реализовать ручное переключение шардов

4.2 Экспериментальные методы

Автоматически использовать AdaptiveTrackSelection#Factory или настроить BandwidthMeter. Здесь мы выбираем последнее, поскольку изменения меньше.

4.2.1 Реализация QmBandwidthMeter

Язык кода:javascript
копировать
  private long bitrateEstimate;
  private long specificBitrate = C.TIME_UNSET;
  @Override
  public synchronized long getBitrateEstimate() {
     //Если пользователь не указывает битрейт, используйте битрейт по умолчанию. Если указано, используйте устройство пользователя.
    if(specificBitrate == C.RATE_UNSET_INT) {
       return bitrateEstimate;
    }
    return specificBitrate;
  }

4.2.2 Установить «Строитель»

Язык кода:javascript
копировать
  //Создаем класс измерения пропускной способности
  bandwidthMeter = new QmBandwidthMeter
          .Builder(getApplicationContext())
          .build();
  //класс управления буфером
  DefaultLoadControl loadControl = new DefaultLoadControl.Builder()
          .setAllocator(new DefaultAllocator(true,C.DEFAULT_BUFFER_SEGMENT_SIZE / 2))
          .setBufferDurationsMs(
              10_000, //Начинаем загрузку данных, когда буфер меньше 20 секунд, не обязательно буфер
              20_000, //Загрузка данных на срок до 20 секунд за раз
              DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
              DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
          .build();
                
      ExoPlayer.Builder playerBuilder =
          new ExoPlayer.Builder(/* context= */ this)
              .setLoadControl(loadControl)
              .setBandwidthMeter(bandwidthMeter)
              .setMediaSourceFactory(createMediaSourceFactory());
      setRenderersFactory(
          playerBuilder, intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false));
      player = playerBuilder.build();

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

4.2.3 В качестве примера возьмем переключение на следующее шардинг Hls для достижения разрешения 1920x1080. -> Переключение 640x360

Адрес ссылки: https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8

Язык кода:javascript
копировать
#EXT-X-STREAM-INF:BANDWIDTH=1924009,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1920x1080,AUDIO="bipbop_audio",SUBTITLES="subs"gear5/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=577610,CODECS="mp4a.40.2, avc1.4d401e",RESOLUTION=640x360,AUDIO="bipbop_audio",SUBTITLES="subs"gear2/prog_index.m3u8

Установите пропускную способность через 5 секунд после начала трансляции

【1】Установите пропускную способность 1924009/0,7f при запуске трансляции.

【2】Установите пропускную способность 577610/0,7f через 10 секунд после начала трансляции.

Язык кода:javascript
копировать
//При запуске трансляции
bandwidthMeter.setSpecificBitrate((long) Math.ceil(1924009/0.7f)); 
//Поиграв некоторое время
bandwidthMeter.setSpecificBitrate((long) Math.ceil(577610/0.7f));

Примечание. Это значение делится на 0,7f, как упоминалось выше, что вызвано механизмом резервирования полосы пропускания.

Метод проверки, реализуйте следующий обратный вызов

Язык кода:javascript
копировать
com.google.android.exoplayer2.audio.AudioRendererEventListener#onAudioInputFormatChanged(com.google.android.exoplayer2.Format)
com.google.android.exoplayer2.video.VideoRendererEventListener#onVideoInputFormatChanged(com.google.android.exoplayer2.Format)

4.3 Результаты экспериментов

В соответствии с ожиданиями поток сокращения кода был успешно реализован.

5. Резюме

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

Если адаптивное переключение потоков ExoPlayer должно быть преобразовано в метод, который могут выбирать пользователи, необходимо изменить некоторые параметры BandwidthMeter и AdaptiveTrackSelection. Существуют также факторы, которые необходимо учитывать: длина фрагмента, выравнивание кадра IDR фрагмента и согласованный формат кодирования (повторное использование декодера). Кроме того, ожидание переключения фрагмента может занять много времени, что также является проблемой. требует внимания.

boy illustration
Неразрушающее увеличение изображений одним щелчком мыши, чтобы сделать их более четкими артефактами искусственного интеллекта, включая руководства по установке и использованию.
boy illustration
Копикодер: этот инструмент отлично работает с Cursor, Bolt и V0! Предоставьте более качественные подсказки для разработки интерфейса (создание навигационного веб-сайта с использованием искусственного интеллекта).
boy illustration
Новый бесплатный RooCline превосходит Cline v3.1? ! Быстрее, умнее и лучше вилка Cline! (Независимое программирование AI, порог 0)
boy illustration
Разработав более 10 проектов с помощью Cursor, я собрал 10 примеров и 60 подсказок.
boy illustration
Я потратил 72 часа на изучение курсорных агентов, и вот неоспоримые факты, которыми я должен поделиться!
boy illustration
Идеальная интеграция Cursor и DeepSeek API
boy illustration
DeepSeek V3 снижает затраты на обучение больших моделей
boy illustration
Артефакт, увеличивающий количество очков: на основе улучшения характеристик препятствия малым целям Yolov8 (SEAM, MultiSEAM).
boy illustration
DeepSeek V3 раскручивался уже три дня. Сегодня я попробовал самопровозглашенную модель «ChatGPT».
boy illustration
Open Devin — инженер-программист искусственного интеллекта с открытым исходным кодом, который меньше программирует и больше создает.
boy illustration
Эксклюзивное оригинальное улучшение YOLOv8: собственная разработка SPPF | SPPF сочетается с воспринимаемой большой сверткой ядра UniRepLK, а свертка с большим ядром + без расширения улучшает восприимчивое поле
boy illustration
Популярное и подробное объяснение DeepSeek-V3: от его появления до преимуществ и сравнения с GPT-4o.
boy illustration
9 основных словесных инструкций по доработке академических работ с помощью ChatGPT, эффективных и практичных, которые стоит собрать
boy illustration
Вызовите deepseek в vscode для реализации программирования с помощью искусственного интеллекта.
boy illustration
Познакомьтесь с принципами сверточных нейронных сетей (CNN) в одной статье (суперподробно)
boy illustration
50,3 тыс. звезд! Immich: автономное решение для резервного копирования фотографий и видео, которое экономит деньги и избавляет от беспокойства.
boy illustration
Cloud Native|Практика: установка Dashbaord для K8s, графика неплохая
boy illustration
Краткий обзор статьи — использование синтетических данных при обучении больших моделей и оптимизации производительности
boy illustration
MiniPerplx: новая поисковая система искусственного интеллекта с открытым исходным кодом, спонсируемая xAI и Vercel.
boy illustration
Конструкция сервиса Synology Drive сочетает проникновение в интрасеть и синхронизацию папок заметок Obsidian в облаке.
boy illustration
Центр конфигурации————Накос
boy illustration
Начинаем с нуля при разработке в облаке Copilot: начать разработку с минимальным использованием кода стало проще
boy illustration
[Серия Docker] Docker создает мультиплатформенные образы: практика архитектуры Arm64
boy illustration
Обновление новых возможностей coze | Я использовал coze для создания апплета помощника по исправлению домашних заданий по математике
boy illustration
Советы по развертыванию Nginx: практическое создание статических веб-сайтов на облачных серверах
boy illustration
Feiniu fnos использует Docker для развертывания личного блокнота Notepad
boy illustration
Сверточная нейронная сеть VGG реализует классификацию изображений Cifar10 — практический опыт Pytorch
boy illustration
Начало работы с EdgeonePages — новым недорогим решением для хостинга веб-сайтов
boy illustration
[Зона легкого облачного игрового сервера] Управление игровыми архивами
boy illustration
Развертывание SpringCloud-проекта на базе Docker и Docker-Compose