HTML5 audio and video controls (Ionic 5)

I am having a couple issues using the HTML <audio> and <video> tags on iOS devices.

For one, the audio controls only include the play/pause button and volume (and – while playing – the Airplay control). No seek control or duration.

Secondly, the controls occasionally become invisible, though they continue to work. This one also applies to video files.

Below is a screenshot depicting the issue. It may be worth pointing out that both audio and video are presented in a segment, and sometimes switching segments back and forth will fix the issue.

Here is the code for this segment…

    <div *ngSwitchCase="'audio'" class="segment-content">
      <div class="padded-list">
        <ion-list-header mode="md"> Audio </ion-list-header>
        <ion-list *ngIf="media && media.audio.length">
          <ion-item *ngFor="let file of media.audio; let i = index">
            <ion-label>
              <h3
                class="ion-text-wrap"
                [innerHTML]="helpers.smarten(file.title)"
              ></h3>
              <p
                [innerHTML]="helpers.smarten(file.description)"
                class="ion-text-wrap"
              ></p>
              <audio controls preload="auto">
                <source type="audio/mpeg" [src]="file.url" />
                [unable to load audio]
              </audio>
            </ion-label>
          </ion-item>
        </ion-list>
      </div>
    </div>

I’m using Capacitor and trying to minimize the use of compatible Cordova plugins, and I’d also like to avoid rolling my own media controls (which I’ve done in previous versions of the app).

Any advice would be appreciated.

UPDATE: This helps reduce the occurrence of disappearing controls, but doesn’t work 100% of the time

For what it’s worth, I was able to prevent the disappearing controls, by adding a a function to the ionChange event…

<ion-segment (ionChange)="segmentChange()">

I wrapped each audio and video element in a container div with a hidden attribute and set controls and preload to false…

<!-- e.g. --> 
<div class="audio-container" hidden>
  <audio controls="false" preload="none">
    <source type="audio/mpeg" [src]="file.url" />
    [unable to load audio]
  </audio>
</div>

Then I set a short timeout to remove the hidden attribute, change the controls and preload attributes, and reload the inner HTML content, which means the player and controls get rendered on demand each time.

segmentChange() {
   if (this.media_segment == "audio" || this.media_segment == "video") {
     setTimeout(() => {
       let elems = <NodeList>(
         document.querySelectorAll("." + this.media_segment + "-container")
       );
       for (var i = 0; i < elems.length; i++) {
         let elem = <HTMLAnchorElement>elems[i];
         let inner = elem.innerHTML;
         elem.innerHTML = inner;
         elem.children[0].setAttribute("controls", "true");
         elem.children[0].setAttribute("preload", "metadata");
         elem.removeAttribute("hidden");
       }
     }, 300);
   }
 }

Still, if anyone can figure out how to force the duration and seek controls to show up, please share.

Hello ebellempire:

Did you resolve this?

I wanted to use audio html 5 in one proyect but firt test and the controls are disabled as you mentioned.

Regards…

Hi @boel21,

I ended up writing my own audio player.

Below are some details, which may or may not be helpful, given that my data has a specific structure that probably differs from yours. But if you can abstract it a little, it should make sense.

Here’s how I marked up the page template.:

<ion-list *ngIf="media && media.audio.length">
  <ion-item *ngFor="let file of media.audio; let i = index">
    <ion-label>
      <h3
        class="ion-text-wrap"
        [innerHTML]="file.title"
      ></h3>
      <p
        [innerHTML]="file.description"
        class="ion-text-wrap"
      ></p>

      <!-- player -->
      <ion-grid class="audio-container">
        <ion-row class="ion-align-items-center">
          <ion-col size="auto">
            <ion-button
              aria-label="play"
              *ngIf="nowPlayingAudioIndex !== i || audioIsPaused"
              [color]="nowPlayingAudioIndex == i ? 'primary' : 'medium'"
              (click)="playPlauseAudio(i, file.url)"
            >
              <ion-icon
                ios="play-outline"
                md="play-sharp"
                slot="icon-only"
              ></ion-icon>
            </ion-button>

            <ion-button
              aria-label="pause"
              *ngIf="nowPlayingAudioIndex == i && !audioIsPaused"
              [color]="nowPlayingAudioIndex == i ? 'primary' : 'medium'"
              (click)="playPlauseAudio(i, file.url)"
            >
              <ion-icon
                ios="pause-outline"
                md="pause-sharp"
                slot="icon-only"
              ></ion-icon>
            </ion-button>
          </ion-col>

          <ion-col>
            <ion-range
              #range
              min="0"
              [max]="nowPlayingAudioIndex == i ? audioDuration : 100"
              [value]="nowPlayingAudioIndex == i ? audioProgress : 0"
              [color]="nowPlayingAudioIndex == i ? 'primary' : 'medium'"
              pin="false"
              mode="md"
              debounce="0"
              (touchstart)="pauseWhileSeeking(i)"
              (mousedown)="pauseWhileSeeking(i)"
              (touchend)="seek(i,$event)"
              (mouseup)="seek(i, $event)"
            ></ion-range>
          </ion-col>
          <ion-col size="auto">
            <ion-text
              class="timestamp"
              [color]="nowPlayingAudioIndex == i ? 'dark' : 'medium'"
              >{{nowPlayingAudioIndex == i && audioPlayer?
              getAudioProgress(audioPlayer.currentTime) :
              '00:00'}}</ion-text
            >
          </ion-col>
        </ion-row>
      </ion-grid>
      <!-- end player -->
    </ion-label>
  </ion-item>
</ion-list>

Add some minimal CSS as needed:

// Custom Audio Player
ion-grid.audio-container {
  margin: 1em 0 0 0;
  padding: 0;
}
ion-grid.audio-container ion-button {
  margin: 0;
}
ion-grid.audio-container ion-col {
  padding: 0;
  margin: 0;
}
ion-grid.audio-container ion-text.timestamp {
  font-size: 14px;
  display: block;
  font-family: monospace;
}
.ios {
  ion-grid.audio-container ion-button {
    margin: 0 0 3px 0;
  }
}

And here’s how it functions:

// ...
@ViewChild("range", { static: false }) range: IonRange;

// ...
media: any;

// ...
audioPlayer = null;
audioDuration = null;
audioProgress = null;
audioTimer = null;
audioIsPaused = false;
nowPlayingAudioIndex = null;

// ...

playPlauseAudio(index: number, file: string) {
  if (index === this.nowPlayingAudioIndex) {
    // update current player
    if (this.audioIsPaused) {
      // resume
      this.audioIsPaused = false;
      if (this.audioPlayer) this.audioPlayer.play();
    } else {
      // pause
      this.audioIsPaused = true;
      if (this.audioPlayer) this.audioPlayer.pause();
    }
  } else {
    if (this.audioPlayer) {
      // stop any existing audio
      this.destroyAudio();
    }
    // create and play new track
    this.nowPlayingAudioIndex = index;
    this.audioIsPaused = false;
    this.audioPlayer = new Audio(file);
    this.audioPlayer.play().then(() => {
      this.audioDuration = this.audioPlayer.duration;
      this.audioTimer = setInterval(() => {
        this.audioProgress = this.audioPlayer.currentTime;
        this.audioPlayer.onended = () => {
          this.destroyAudio();
        };
      }, 100);
    });
  }
}

seek(index: number, event: any) {
  if (index == this.nowPlayingAudioIndex) {
    let seekTo = event.target.value;
    this.audioPlayer.currentTime = seekTo;
    if (this.audioIsPaused == true) {
      this.audioPlayer.play();
      this.audioIsPaused = false;
    }
  }
}

pauseWhileSeeking(index: number) {
  if (index == this.nowPlayingAudioIndex) {
    this.audioIsPaused = true;
    this.audioPlayer.pause();
  }
}

getAudioProgress(seconds: number) {
  if (seconds >= 0) {
    return new Date(seconds * 1000).toISOString().substr(14, 5);
  }
}

destroyAudio() {
  if (this.audioPlayer && !this.audioIsPaused) this.audioPlayer.pause();
  if (this.audioTimer) clearInterval(this.audioTimer);
  if (this.range) this.range.value = 0;
  this.audioPlayer = null;
  this.audioDuration = null;
  this.audioProgress = null;
  this.audioTimer = null;
  this.audioIsPaused = false;
  this.nowPlayingAudioIndex = null;
}
2 Likes

hello ebellempire, thanks for the reply. Very helpfull for me than im studing and very nice from you for sharing and help.

Regards