1. Requirements analysis and development plan

1.1 Requirements overview

Recently, the product presented us with the requirement of “playing audio courses in small programs”. There are four main points:

  • Course management: Enter the playing page of a course and get the audio list, but do not play it temporarily.
  • Audio management: Click any audio to play on the play page. Can automatically play the next track. Such as this



  • Progress control: Supports dragging to modify progress/up/down/pause/play, as shown below.



  • Global play: The background audio is displayed at the top of the wechat chat list page when the user temporarily leaves the mini program.
It looks something like this.



1.2 Development analysis

Well, the question is, how do you implement these requirements?
I was lost in thought …………

The first “course management” is easy, just maintain an array globally.

The second “audio management” seemed like a hassle, and at first I thought of the Audio control provided by the applet.

But I immediately dismissed the idea for two main reasons:

  • The audio control provided by wechat has the default style as shown in the following figure, which is inconsistent with the requirements of the design draft.

  • After a personal test in the small program Demo provided by wechat official, if I use audio control, the audio will disappear when I exit the current page, which cannot meet the “global play” required by PM.
Therefore, I decided to use backgroundAudioManager provided by wechat.

1.2.1 backgroundAudioManager profile

According to the official documentation, backgroundAudioManager is:

Globally unique background audio manager
Some of its important attributes and important methods are listed below:

Properties:

  • Duration: Current audio length, which can be used to initialize the value of the play control.
  • CurrentTime: Current playback position, which can be used to update the playback control’s progress value
  • Paused: false indicates play and true indicates stop or pause
  • SRC: audio source. Note that SRC is automatically played
  • Title: Audio title (the audio title “Why children get sick easily in autumn and winter” just displayed at the top of wechat chat list page is set here)
Methods:

  • Play/Pause/Stop/seek: Performs common audio play control. Seek is a method to jump to a specific play progress
  • OnPlay/onPause/onStop/onEnded: Responds to a specific event, where onStop is an active stop and onEnded is an automatic end (this can be used for “continuous play”)
  • OnTimeUpdate: Background audio playback progress update event that can be combined with the previous currentTime property to update the value of the control.
  • OnWaiting/onCanplay: Audio usually doesn’t play immediately, so these two methods can give the user a hint when the audio loads.
Check out its official documentation for more information.

1.2.2 Play control

The third “play control” is not too difficult, play/pause/up and down capital with a small picture.

But the difficulty is to play the simulation of the progress bar. As mentioned earlier, the style of audio control does not meet the requirements.

So I decided to use slider to simulate, should also be able to handle.

Fourth, as mentioned above, use backgroundAudioManager to implement “global play”.

1.2.3 Development scheme determination

Okay, so that’s the end of the requirements analysis, we’re going to develop this requirement, we need three objects,

  • Course management object, responsible for maintaining course information and course audio list, not responsible for playing



  • The audio management object, namely backgroundAudioManager, is responsible for managing the playback of audio. Only the changeAudio method has the permission to modify audio



  • Play control.



With these objects, course management/audio management/progress control/global playback can be done.

Having said that, however, there are always problems with actually implementing requirements.



2. Function realization

There are too many requirements for me to list them all, but here are a few that require skill

2.1 Slider control simulation progress

As mentioned earlier, the control looks something like this



So use slider to simulate, but the simulation is not easy.

Huh? Why do you think? I’ll tell you all about it.

2.1.1 Requirement 1: The control automatically updates with audio playback

PM requirements are: control with audio playback, automatically update progress, lvalue with progress, rvalue is the total length of the audio.

However, the slider of the small program does not support the display of left and right values, we had to simulate their own.

<! -- Audio progress control --> <view class="course-control-process"> // Lvalue display currentProcess <text class="current-process">{{currentProcess}}</text> // Progress bar <slider bindchange="hanleSliderChange"// Respond to the drag event bindTouchStart ="handleSliderMoveStart"
      bindtouchend="handleSliderMoveEnd"
      min="0"
      max="{{sliderMax}}"
      activeColor="#8f7df0"
      value="{{sliderValue}}"/> // totalProcess <text class="total-process">{{totalProcess}}</text>
</view> Copy the code
CurrentProcess is lvalue, totalProcess is rvalue, sliderMax is the maximum value of the control, and sliderValue is the value of the current control.

So, how do you update these numbers? As mentioned earlier, backgroundAudioManager has an onTimeUpdate method in which to update the progress value.

// the formatAudioProcess function is used to format the time 00:15onTimeUpdate() {/ / omit some judgment code self. Page. SetData ({currentProcess: formatAudioProcess (globalBgAudioManager. CurrentTime), sliderValue: Math.floor(globalBgAudioManager.currentTime) }); },Copy the code
Note that when entering a playpage of the same course, the original page will probably be destroyed (for example, if you execute navigateTo), so you need to update the original data value, such as currentProcess, when initializing the playpage. This is going to come from the current backgroundAudioManager.

## Check if the course is the same, if so, update the progress

if(id ! == globalCourseAudioListManager.getCurrentCourseInfo().id)## Update method
  updateControlsInOldAudio() {/ / get the current audio const currentAudio = globalCourseAudioListManager. GetCurrentAudio (); / / update the progress and control content enclosing 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 }); },Copy the code

2.1.2 Requirement 2: Drag the progress bar to automatically go to a specific location

Notice that the slider control has bindchange=”hanleSliderChange”, so we can get the value and update the audio

hanleSliderChange(e) { const position = e.detail.value; this.seekCurrentAudio(position); }, // Drag the progress bar seekCurrentAudio(position) {// Update the progress bar const page = this; // Audio control jump // here is a weird bug: The seek in suspended state cannot change currentTime, need to play after 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`); }}); },Copy the code
It looks a little weird, doesn’t it? BackgroundAudioManager’s seek method does not have a success callback.

seek(options) { wx.seekBackgroundAudio(options); // The success callback can be configured}Copy the code
However, the onTimeUpdate event triggers the slider to update and the manually dragging event triggers the slider to update. If both functions need to change the slider, who should change the slider?

However, you can check if you are sliding by monitoring the TouchStart and TouchEnd events. If slider is sliding, disable onTimeUpdate from modifying slider updates.

So, I’m going to set a variable to indicate whether I’m sliding

handleSliderMoveStart() {
    this.setData({
      isMovingSlider: true
    });
  },
  handleSliderMoveEnd() {
    this.setData({
      isMovingSlider: false
    });
  }, Copy the code
Stop updating the progress bar during the slide

onTimeUpdate() {// Do not update the progress bar control while movingif(! self.page.data.isMovingSlider) { self.page.setData({ currentProcess: formatAudioProcess(globalBgAudioManager.currentTime), sliderValue: Math.floor(globalBgAudioManager.currentTime) }); } // other ellipses},Copy the code

2.2 backgroundAudioManager Requirements

Before we move on to the next requirements presentation, I wonder if you have any questions:

Where did I set the onTimeupdate method?
OK, let me introduce you.

First, global fetch

this.backgroundAudioManager = wx.getBackgroundAudioManager(); Copy the code
Second, introduce backgroundAudioManager in play/index.js

let globalBgAudioManager = app.backgroundAudioManager; Copy the code
When appropriate, such as I am onLoad, extend the globalBgAudioManager object. This way I put the specific functionality into the specific page, different page for the backgroundAudioManager can have different implementation.

this.initBgAudioListManager(); Copy the code
Now let’s see what this extension does.

initBgAudioListManager() {// This refers to the function itself at the time of execution, so we need to save this corresponding to Page. const page = this; const self = globalBgAudioManager; Const options = {// options will be covered later}; / / decorateBgAudioListManager function, modify globalBgAudioManager object directly, So as to realize the expansion of method globalBgAudioManager = decorateBgAudioListManager (globalBgAudioManager, options);Copy the code
Options options options options options options options options options options

In fact, options are all methods that backgroundAudioManager already has. For details, please refer to the documentation. I just rewrote it

2.2.1 Requirement 3: Bypass onCanPlay and remind users that audio is loading

As we all know, audio takes a while to load before it can play, so the applet’s global player object, the backgroundAudioManager, provides onWaiting and onCanplay, which seem to be built for interactive audio loading.

But don’t know why, onCanplay no! Method! Touch! Hair!!!! Raised this issue with the community and no one bird me ah… Heartache.

Come on, let him do it. I’ll go around my wall…

First, in options, overwrite onWaiting: the user is prompted to load, isWaiting is marked (” Look! Audio in Waiting!” )

const options = {
    onWaiting() {
        wx.showLoading({
          title: 'Audio loading... '
        });
        globalBgAudioManager.isWaiting = true; }},Copy the code


Then, when the timeline is updated (this is equivalent to starting to play), the Loading window is closed. Also override onTimeUpdate in options.

onTimeUpdate() {
    if (self.isWaiting) {
      self.isWaiting = false;
      setTimeout(() => { wx.hideLoading(); }, 300); // 300ms is set to avoid the bad experience caused by the quick Loading of some audio files.Copy the code

2.2.2 Requirement 4: Click an audio to play it

The trouble with this requirement is that you need to check what audio you’re clicking on, so let’s say you’re playing audio A, and you click on audio A again, you don’t have to replay it.

As well as iOS version of the small program and Ali cloud server seems to have a bit of a holiday, will see below.

Inside Pages/Play /index, click events are responded to first

## pages/play/indexOutlineOperation (e) {/ / get audio address const courseAudio = e.c. with our fabrication: urrentTarget. Dataset. The outline | | {}; const targetAudioId = courseAudio.audio_id; // omit a series of validity checks. this.playTargetAudio(targetAudioId); },Copy the code
Then perform related operations, although the globalCourseAudioListManager mentioned earlier, but for a while, more about this, do it directly see comments

## pages/play/index@param {*Number} targetAudioId * - Check if the same audio is hit * - Check if the audio is played completely * - If the audio is not played completely, or if the audio is not hit the same audio, * - playTargetAudio(targetAudioId) {const currentAudio = globalCourseAudioListManager.getCurrentAudio(); // There is no need to respond if you click the original audio without stoppingif(targetAudioId === currentAudio.audio_id && !! globalBgAudioManager.currentTime) {return false;
    } else{this.getaudiosrc (targetAudioId).then(() => {// If not suspended, then suspendedif(! globalBgAudioManager.paused) { globalBgAudioManager.pause(); } / / global switching current audio index (at this point hasn't played) globalCourseAudioListManager. ChangeCurrentAudioById (targetAudioId); // Update the current control state, such as the title and length of the new audio. this.updateControlsInNewAudio(); . / / replacement and play background music globalBgAudioManager changeAudio (); }); }},Copy the code
Ok, finally to the changeAudio function, which is also part of the options mentioned earlier.

## changeAudio is an options property that is extended into the backgroundAudioManager// Modify the current audiochangeAudio() {/ / access and const {audio_id url, 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 ||'Lilac Doctor'; // iOS uses content_type_signare_url const SRC = isIOS()? content_type_signare_url : url;if(! src) { showToast({ title:'Audio lost, cannot play',
        icon: 'warn',
        duration: 2000
      });
    } else{ self.src = src; }}Copy the code
Why does iOS use content_type_signare_URL? (This is a field that we return on the back end)

Because the iOS applet will add content-Type: octet-stream to the audio file request by default, and our audio file URL has Signatrue signature parameter, ali Cloud server seems to add content-type to the signature by default… So I ran into error 403.

There are two solutions:

  • Ask my colleagues in charge of the CDN server at the back end to request resources and cache them before I request the audio SRC address.
  • Change the audio address to public.

2.3 Requirements related to courseAudioListManager

As mentioned earlier, I need to maintain a global course information and audio list management object, and then I can manipulate the audio list.

## init in app.js
this.courseAudioListManager = createCourseAudioListManager();

## referenced in pages/play/index.js
const globalCourseAudioListManager = app.courseAudioListManager; Copy the code
There’s really not much to say about this object, it’s pretty simple.

Another example is “click on an audio and play it automatically”, which has a step like this.

/ / global switching current audio index (at this point hasn't played) globalCourseAudioListManager. ChangeCurrentAudioById (targetAudioId);Copy the code
Is to change the audio index by ID, and here’s what it does.

changeCurrentAudioById(audioId = -1) {
    this.currentIndex = this.audioList.findIndex(audio => audio.audio_id === audioId);
}, Copy the code
For other methods, please refer to the brain map in section 1.2.3 “Development Plan Determination”.

However, it has an addAudioSrc that takes care of replay failures.



2.3.1 Solve the replay failure by reloading SRC

The play() method will not be replayed when the audio is “stopped” rather than “paused”, nor will the seek method be used to jump the audio.

For example, when I finish listening to an audio clip and want to listen to it again, regular play doesn’t work… How to do? Go around it, of course

When you hit the play button,

  • First through a series of checks, the following playTargetAudio is triggered
handleStartPlayClickIf () {/ / above omit, globalBgAudioManager. CurrentTime is zerofalseYou think you are clicking on an audio that has already played}else if(! globalBgAudioManager.currentTime) { this.playTargetAudio(currentAudio.audio_id); }else// omit}Copy the code
  • Execute getAudioSrc/changeCurrentAudioById/changeAudio in sequence inside playTargetAudio
This.getaudiosrc (targetAudioId).then(() => {// omit // Globally toggle the currently playing audio index globalCourseAudioListManager.changeCurrentAudioById(targetAudioId); / / government / / replacement and play background music globalBgAudioManager. ChangeAudio (); }); }Copy the code
  • Inside the getAudioSrc, the main action is to update the new SRC
globalCourseAudioListManager.addAudioSrc(res.items[0]); Copy the code
And then what does addAudioSrc do

## Now inside the courseAudioListManagerAddAudioSrc (audioSrcObject) {this.audiolist = this.audiolist.map (audio => {// Forces the audio object with a specific ID to be updated // The new SRC is hidden inside audioSrcObjectif (Number(audio.audio_id) === Number(audioSrcObject.id)) {
          return Object.assign(audio, audioSrcObject, { id: audio.id });
        } else {
          returnaudio; }}); },Copy the code
SRC has now been updated. The SRC address of the audio is time-stamped, which avoids caching. When backgroundAudioManager sets SRC, it is reloaded

This way, of course, there is no caching, and the interaction is sacrificed by flashing “audio loading” every time a replay is played.

If you have a good way to achieve cache, welcome to exchange ha.



3. Other experiences

  • If the code is too long, don’t use the ternary operators; it’s hard to read.
  • Errors may occur in audio playback and need to be caught with onError.
  • Finally, welcome to leave a message ~!