1. Анализ требований и план развития
1.1 Введение в требования
Недавно к продукту выдвинули требование «воспроизведение аудиокурсов в апплете», основных пунктов четыре:
- Управление курсом: перейдите на страницу воспроизведения программы, получите все списки аудио, но временно не воспроизводите.
- Управление звуком: поддержка на странице воспроизведения, щелкните любой звук для воспроизведения; следующая песня может воспроизводиться автоматически. так
- Управление прогрессом: поддержка перетаскивания для изменения прогресса/вверх/вниз/паузы/воспроизведения, как показано ниже.
- Глобальная игра: когда пользователь временно покидает мини-программу, фоновый звук отображается в верхней части страницы списка чатов WeChat.
так.
1.2 Анализ развития
Хорошо, проблема приходит, как вы реализуете эти потребности?
Я задумался..............
Первое «управление курсом» не сложно, достаточно поддерживать массив глобально.
Второе "управление звуком" кажется хлопотным, сначала подумал о мелкой предоставленной программеаудио управление.
Но потом я отказался от этой идеи по двум основным причинам:
- Элемент управления звуком, официально предоставленный WeChat, имеет стиль по умолчанию, как показано на рисунке ниже, который не соответствует требованиям проекта дизайна.
- После тестирования в официальной демонстрации экземпляра мини-программы WeChat, если я использую управление звуком, звук исчезнет, когда я выйду с текущей страницы.Нет никакого способа выполнить «глобальное воспроизведение», требуемое PM.
Поэтому я решил использоватьbackgroundAudioManager.
1.2.1 Введение в backgroundAudioManager
Согласно официальной документации, backgroundAudioManager это:
Глобально уникальный менеджер фонового звука
Некоторые из его важных свойств и важных методов перечислены ниже:
Атрибуты:
- продолжительность: текущая длина звука, которую можно использовать для инициализации значения элементов управления воспроизведением.
- currentTime: текущая позиция воспроизведения, которую можно использовать для обновления значения прогресса элемента управления воспроизведением.
- paused: false для воспроизведения, true для остановки/паузы
- src: источник аудиоданных, обратите внимание, что он будет воспроизводиться автоматически при установке src
- title: аудиозаголовок (здесь устанавливается аудиозаголовок «Почему дети легко болеют осенью и зимой», отображаемый только вверху страницы списка чатов WeChat)
метод:
- воспроизведение/пауза/остановка/поиск: может выполнять общие элементы управления воспроизведением звука, где поиск — это метод перехода к определенному ходу воспроизведения
- onPlay/onPause/onStop/onEnded: в ответ на определенное событие, где onStop является активной остановкой, а onEnded автоматически завершает воспроизведение (это можно использовать для достижения «непрерывного воспроизведения»)
- onTimeUpdate: событие обновления хода фонового воспроизведения звука, которое можно комбинировать с предыдущим свойством currentTime для обновления значения элемента управления.
- onWaiting/onCanplay: звук обычно не воспроизводится сразу, эти два метода могут выдавать пользователю некоторые подсказки при загрузке звука.
Для получения дополнительных новостей, пожалуйста, проверьте егоофициальная документация.
1.2.2 управление игрой
Третье «управление воспроизведением» не слишком сложно, просто используйте маленькие картинки для воспроизведения/паузы/вверх и вниз по столице.
Но сложность заключается в имитации полосы прогресса воспроизведения, как было сказано ранее, стиль управления звуком не соответствует требованиям.
Затем я решил использовать ползунок для имитации, и он должен уметь это делать.
Четвертый элемент, как упоминалось ранее, использует backgroundAudioManager для достижения «глобального воспроизведения».
1.2.3 Определение плана разработки
Что ж, анализ спроса почти сделан, нам нужно этот спрос развить, нам нужно три объекта,
- Объект управления курсом, отвечающий за ведение информации о курсе и списка аудиозаписей курса, не отвечающий за воспроизведение
- Объект управления звуком, backgroundAudioManager, отвечает за управление воспроизведением звука, из которых только метод changeAudio имеет разрешение на изменение звука.
- Элементы управления воспроизведением.
С помощью этих объектов можно выполнять управление курсом/управление звуком/контроль прогресса/общее воспроизведение.
Однако, сказав это, всегда есть различные проблемы в фактической реализации требований.
2. Реализация функции
Поскольку требований слишком много, я не могу перечислить их все.Вот некоторые требования, которые требуют навыков.
2.1 Ползунок имитирует прогресс
Как упоминалось ранее, элемент управления выглядит так
Таким образом, вы должны использовать ползунок для моделирования, но моделирование не простое.
ха? вы говорите, почему? Я буду говорить вам медленно.
2.1.1 Требование 1: Элементы управления автоматически обновляются при воспроизведении звука.
Требования PM таковы: элемент управления автоматически обновляет прогресс по мере воспроизведения аудио, левое значение обновляется с прогрессом, а правое значение — это общая продолжительность аудио.
Но слайдер, который поставляется с апплетом, не поддерживает отображение левых и правильных значений, мы можем только имитировать его самим собой.
<!-- 音频进度控件 -->
<view class="course-control-process">
// 左值展示,currentProcess
<text class="current-process">{{currentProcess}}</text>
// 进度条
<slider
bindchange="hanleSliderChange" // 响应拖动事件
bindtouchstart="handleSliderMoveStart"
bindtouchend="handleSliderMoveEnd"
min="0"
max="{{sliderMax}}"
activeColor="#8f7df0"
value="{{sliderValue}}"/>
// 右值展示,totalProcess
<text class="total-process">{{totalProcess}}</text>
</view>
currentProcess — левое значение, totalProcess — правое значение, sliderMax — максимальное значение элемента управления, а sliderValue — значение текущего элемента управления.
Итак, как обновить эти значения? Как упоминалось ранее, у backgroundAudioManager есть метод onTimeUpdate, где вы можете обновить значение прогресса.
// formatAudioProcess函数我就不放了,就是把时间格式化成00:15这样就行了
onTimeUpdate() {
// 省略一些判断代码
self.page.setData({
currentProcess: formatAudioProcess(globalBgAudioManager.currentTime),
sliderValue: Math.floor(globalBgAudioManager.currentTime)
});
},
Здесь стоит отметить одну вещь: при входе на страницу воспроизведения того же курса, поскольку исходная страница, скорее всего, будет уничтожена (например, вы выполняете navigationTo), необходимо обновить исходное значение данных во время инициализации, например, текущее воспроизведение Текущий процесс воспроизведения, который должен быть взят из текущего backgroundAudioManager.
## 检查是否同一个课程,如果是的话,更新进度
if (id !== globalCourseAudioListManager.getCurrentCourseInfo().id)
## 更新方法
updateControlsInOldAudio() {
// 获取当前音频
const currentAudio = globalCourseAudioListManager.getCurrentAudio();
// 更新进度和控件内容
this.setData({
currentProcess: formatAudioProcess(globalBgAudioManager.currentTime),
sliderValue: formatAudioProcess(globalBgAudioManager.currentTime),
sliderMax: Math.floor(currentAudio.duration / 1000) - 1 || 0,
totalProcess: formatAudioProcess(currentAudio.duration / 1000 || 0),
hasNextAudio: !globalCourseAudioListManager.isRightEdge() && this.data.hasBuy,
hasPrevAudio: !globalCourseAudioListManager.isLeftEdge() && this.data.hasBuy,
paused: globalBgAudioManager.paused,
currentPlayingAudioId: currentAudio.audio_id,
courseChapterTitle: currentAudio.title
});
},
2.1.2 Требование 2: перетащите индикатор выполнения, чтобы автоматически перейти к определенной позиции
Обратите внимание, что предыдущий ползунок имеет bindchange="hanleSliderChange", тогда мы можем получить значение, а затем обновить звук.
hanleSliderChange(e) {
const position = e.detail.value;
this.seekCurrentAudio(position);
},
// 拖动进度条控件
seekCurrentAudio(position) {
// 更新进度条
const page = this;
// 音频控制跳转
// 这里有一个诡异bug:seek在暂停状态下无法改变currentTime,需要先play后pause
const pauseStatusWhenSlide = globalBgAudioManager.paused;
if (pauseStatusWhenSlide) {
globalBgAudioManager.play();
}
globalBgAudioManager.seek({
position: Math.floor(position),
success: () => {
page.setData({
currentProcess: formatAudioProcess(position),
sliderValue: Math.floor(position)
});
if (pauseStatusWhenSlide) {
globalBgAudioManager.pause();
}
console.log(`The process of the audio is now in ${globalBgAudioManager.currentTime}s`);
}
});
},
Это выглядит немного странно, не так ли? Метод seek для backgroundAudioManager не имеет обратного вызова, который был изменен мной здесь.
seek(options) {
wx.seekBackgroundAudio(options); // 这样实现,就可以配置success回调了
}
Однако «событие onTimeUpdate запускает обновление ползунка» и «ручное перетаскивание запускает обновление ползунка» конфликтуют. Если обе функции должны изменить ползунок, кого вы слушаете?
Однако вы можете проверить наличие прокрутки, отслеживая события touchstart и touchend. Если вы скользите, запретите onTimeUpdate изменять обновление элемента управления ползунком.
Поэтому я сначала устанавливаю переменную, чтобы отметить, скользит ли она.
handleSliderMoveStart() {
this.setData({
isMovingSlider: true
});
},
handleSliderMoveEnd() {
this.setData({
isMovingSlider: false
});
},
Вы можете отключить обновление индикатора выполнения во время смахивания
onTimeUpdate() {
// 在move的时候,不要更新进度条控件
if (!self.page.data.isMovingSlider) {
self.page.setData({
currentProcess: formatAudioProcess(globalBgAudioManager.currentTime),
sliderValue: Math.floor(globalBgAudioManager.currentTime)
});
}
// 其他省略
},
2.2 Требования, связанные с BackgroundAudioManager
Прежде чем приступить к следующему введению требований, я хотел бы узнать, есть ли у вас какие-либо вопросы:
Где установить метод onTimeupdate?
Хорошо, позвольте мне представить это.
Во-первых, получить глобальный
this.backgroundAudioManager = wx.getBackgroundAudioManager();
Во-вторых, добавьте backgroundAudioManager в play/index.js.
let globalBgAudioManager = app.backgroundAudioManager;
Когда это уместно, например, я onLoad, расширяя объект globalBgAudioManager. —— Таким образом, я помещаю определенные функции на определенные страницы, и на разных страницах могут быть разные реализации для backgroundAudioManager.
this.initBgAudioListManager();
Далее, давайте посмотрим, что делает это расширение.
initBgAudioListManager() {
// options中的函数在执行的时候,this指向函数本身(亲测),因此这里需要保存Page对应的this。
const page = this;
const self = globalBgAudioManager;
const options = {
// options在后面会介绍
};
// decorateBgAudioListManager函数,直接修改globalBgAudioManager对象,从而实现方法的拓展
globalBgAudioManager = decorateBgAudioListManager(globalBgAudioManager, options);
Ну как внедрить сейчас сказано, а потом поговорим о спросе, то есть о том, что делается в опциях.
На самом деле параметры — это все методы, которые уже есть в backgroundAudioManager.Документация. я только что переписал
2.2.1 Требование 3. Обход onCanPlay и напоминание пользователю о загрузке аудио
Как мы все знаем, аудио должно быть загружено в течение определенного периода времени, прежде чем его можно будет воспроизвести, поэтому глобальный объект воспроизведения апплета, то есть backgroundAudioManager, предоставляет onWaiting и onCanplay, которые, кажется, естественным образом реализованы для взаимодействие аудио нагрузки.
Но не знаю почему, на Canplay None! Закон! трогать! Отправить! Я задал этот вопрос сообществу, и меня никто не беспокоил... душевная боль.
Забудь, он сильнее, чем он есть, а я вокруг своей стены. . .
Во-первых, в опциях перепишите onWaiting: сначала подскажите пользователю, что идет загрузка, и отметьте isWaiting ("Смотрите! Аудио ждет!")
const options = {
onWaiting() {
wx.showLoading({
title: '音频加载中…'
});
globalBgAudioManager.isWaiting = true;
},
}
Затем, когда обновляется временной прогресс (это эквивалентно игре), окно загрузки отключается. Также в опциях есть переписать файл OntimeUpdate.
onTimeUpdate() {
if (self.isWaiting) {
self.isWaiting = false;
setTimeout(() => {
wx.hideLoading();
}, 300);
// 设置300ms是为了避免某些音频加载过快而导致Loading效果一闪而过对用户造成糟糕的体验
}
// 以下代码省略
},
2.2.2 Требование 4. Нажмите аудио для воспроизведения
Проблема с этим требованием заключается в том, что вам нужно проверить, какой звук был нажат. Например, если вы воспроизводите звук A и снова нажимаете A, вам, конечно, не нужно его воспроизводить.
А также версия апплета для iOS и сервер Alibaba Cloud кажутся чем-то вроде фестиваля, как будет видно ниже.
Внутри pages/play/index сначала реагировать на клики
## pages/play/index
outlineOperation(e) {
// 获取音频地址
const courseAudio = e.currentTarget.dataset.outline || {};
const targetAudioId = courseAudio.audio_id;
// 中间省略一系列合法性检查。
this.playTargetAudio(targetAudioId);
},
Затем выполните операции, связанные с воспроизведением. Хотя этот globalCourseAudioListManager упоминался ранее, он будет подробно представлен позже. Просто посмотрите в комментариях, что он делает.
## pages/play/index
/**
* 点击/自动播放 目标音频
* @param {*Number} targetAudioId
* - 检查是否点击到同一个音频
* - 检查是否完全播放完毕
* - 若未播放完毕,或者点击的不是同一个音频,先暂停当前音频
* - 执行音频播放操作
*/
playTargetAudio(targetAudioId) {
const currentAudio = globalCourseAudioListManager.getCurrentAudio();
// 点击未停止的原音频的话,没必要响应
if (targetAudioId === currentAudio.audio_id && !!globalBgAudioManager.currentTime) {
return false;
} else {
this.getAudioSrc(targetAudioId).then(() => {
// 若未暂停,则先暂停
if (!globalBgAudioManager.paused) {
globalBgAudioManager.pause();
}
// 全局切换当前播放的音频index(此时还没有开始播放)
globalCourseAudioListManager.changeCurrentAudioById(targetAudioId);
// 更新当前控件状态,比如新音频的title和长度,总要更新吧。
this.updateControlsInNewAudio();
// 更换并且播放背景音乐
globalBgAudioManager.changeAudio();
});
}
},
Ну и, наконец, функция changeAudio, которая также является частью только что упомянутых опций.
## changeAudio是options的属性,被扩展进入了backgroundAudioManager
// 修改当前音频
changeAudio() {
// 获取并且
const { url, audio_id, title, content_type_signare_url } = globalCourseAudioListManager.getCurrentAudio();
const { doctor, name, image } = globalCourseAudioListManager.courseInfo;
self.title = title;
self.epname = name;
self.audioId = audio_id;
self.coverImgUrl = image;
self.singer = doctor.nickname || '丁香医生';
// iOS使用content_type_signare_url
const src = isIOS() ? content_type_signare_url : url;
if (!src) {
showToast({
title: '音频丢失,无法播放',
icon: 'warn',
duration: 2000
});
} else {
self.src = src;
}
}
Почему iOS использует здесь content_type_signare_url? (это поле, возвращаемое нашим сервером)
Потому что, когда апплет iOS инициирует запрос аудиофайла, он по умолчанию выдает тип контента: octet-stream, а URL-адрес нашего аудиофайла имеет параметр Signature, поэтому сервер Alibaba Cloud, похоже, добавляет тип контента к подписи. по умолчанию. ... поэтому я получил ошибку 403.
Есть два решения:
- Пусть коллега, отвечающий за сервер CDN на бэкенде, прежде чем я запрошу адрес audio src, один раз запросит ресурс и хорошенько поработает с кешированием.
- Измените адрес аудио на общедоступный.
2.3 Требования, связанные с CourseAudioListManager
Как упоминалось ранее, мне нужно поддерживать глобальную информацию о курсе и объект управления списком аудио, а затем я могу управлять списком аудио.
## 在app.js当中初始化
this.courseAudioListManager = createCourseAudioListManager();
## 在pages/play/index.js里面引用
const globalCourseAudioListManager = app.courseAudioListManager;
На самом деле об этом объекте особо рассказывать нечего, он относительно прост.
В качестве другого примера, вышеупомянутый «щелкните аудио и воспроизведите его автоматически», один из шагов выглядит следующим образом.
// 全局切换当前播放的音频index(此时还没有开始播放)
globalCourseAudioListManager.changeCurrentAudioById(targetAudioId);
Это изменение индекса аудио в соответствии с идентификатором, что он и делает.
changeCurrentAudioById(audioId = -1) {
this.currentIndex = this.audioList.findIndex(audio => audio.audio_id === audioId);
},
Для других конкретных методов вы можете увидеть карту мозга в предыдущем разделе 1.2.3 «Определение плана развития».
Однако у него есть addAudioSrc, который может решить проблему сбоя воспроизведения.
2.3.1 Используйте метод перезагрузки src для устранения сбоя воспроизведения
Когда воспроизведение аудио «остановлено», а не «приостановлено», повторный вызов метода play() не приведет к его повторному воспроизведению, и вызов метода seek для выполнения перехода не сработает.
Например, когда я закончил прослушивать аудиозапись и хочу прослушать ее снова, обычное воспроизведение не работает... Что мне делать? Конечно обойти
Когда вы нажимаете кнопку воспроизведения,
- Сначала через серию проверок будет запущен следующий playTargetAudio
handleStartPlayClick() {
// 以上省略,若globalBgAudioManager.currentTime为false,表示认为你在点击一个已经播放完毕的音频
} else if (!globalBgAudioManager.currentTime) {
this.playTargetAudio(currentAudio.audio_id);
} else
// 以下省略
}
- Выполнить getAudioSrc/changeCurrentAudioById/changeAudio последовательно внутри playTargetAudio
this.getAudioSrc(targetAudioId).then(() => {
// 省略
// 全局切换当前播放的音频index
globalCourseAudioListManager.changeCurrentAudioById(targetAudioId);
// 省略
// 更换并且播放背景音乐
globalBgAudioManager.changeAudio();
});
}
- Внутри getAudioSrc основная функция — обновить новый src.
globalCourseAudioListManager.addAudioSrc(res.items[0]);
Тогда давайте посмотрим, что делает addAudioSrc.
## 现在在courseAudioListManager内部
addAudioSrc(audioSrcObject) {
this.audioList = this.audioList.map(audio => {
// 强制更新特定id的audio对象
// 新的src隐藏在audioSrcObject里面
if (Number(audio.audio_id) === Number(audioSrcObject.id)) {
return Object.assign(audio, audioSrcObject, { id: audio.id });
} else {
return audio;
}
});
},
Теперь src обновлен. Кажется, что аудио src, полученное каждый раз, указывает на одно и то же аудио, но адрес src аудио имеет временную метку, что позволяет избежать кэширования.Когда backgroundAudioManager установит src, он будет перезагружен~
Конечно, таким образом не будет никакого кеширования, и будут жертвы во взаимодействии.Каждый раз при повторном прохождении будет вспышка "аудио загрузки".
Если у вас есть хороший способ добиться кэширования, добро пожаловать на обмен.
3. Некоторые другие впечатления
- Если код слишком длинный, не используйте тернарный оператор, его трудно читать.
- Воспроизведение аудио может иметь ошибки, которые необходимо отловить с помощью onError.
- Наконец, добро пожаловать, чтобы оставить сообщение~!