Como hacer un reproductor de música en Angular

 

A continuación les presentare un tutorial sobre como hacer un reproductor música parecido a Spotify.



Que debes tener instalado:

Herramientas a utilizar:


Pasos a seguir:

Crear el proyecto

En la carpeta de tu preferencia ejecuta el siguiente comando desde la terminal:

ng new music-player

A continuación el CLI de Angular les hará una serie de preguntas sobre la configuración de su proyecto, presiona y y dale al botón Enter:

? Would you like to add Angular routing? (y/N) y

Selecciona el motor de estilos CSS y presiona Enter.

? Which stylesheet format would you like to use? 
❯ CSS 
  SCSS   [ https://sass-lang.com/documentation/syntax#scss                ] 
  Sass   [ https://sass-lang.com/documentation/syntax#the-indented-syntax ] 
  Less   [ http://lesscss.org                                             ] 
  
Angular CLI comenzara a crear los componentes y a instalar los paquetes necesarios.

una vez culminado el proceso veras que se creo una carpeta llamada music-player, abrela con tu editor 
de codigo favorito, en este caso estare usando Visual Studio Code.

Al abrir el proyecto debe verse así:



El siguiente paso es agregar las librerías de estilos externas para el CSS, estaremos usando Bootstrap
y Google Fonts Icons.

Abrimos el archivo index.html y agregamos las etiquetas <link> y <script> para que se carguen en nuestro proyecto.


<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>MusicPlayer</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
</head>
<body>
  <app-root></app-root>
  <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/popper.js@1.12.9/dist/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
</body>
</html>

Ya tenemos importadas las librerias css, ahora vamos a establecer los estilos globales en el siguiente 
archivo: src/styles.css
:root {
    --bg-dark-1: #000000;
    --bg-dark-2: #272c42;
    --bg-dark-3: #1a1e31;
}

.icon-click{
    cursor: pointer;
}

.icon-click:active {
    background-color: purple;
}

Ahora vamos a crear los componentes compartidos.

en la terminal escribe el siguiente comando para crear un servicio el cual manejara la tarea de reproducción de música:

ng g s shared/services/multimedia

El servicio creado se debe ver de la siguiente manera:



Ahora lo vamos a modificar con el siguiente codigo:


import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class MultimediaService {

  public audio!: HTMLAudioElement;
  public trackInfo$: BehaviorSubject = new BehaviorSubject(undefined);
  public playerPercentage$: BehaviorSubject = new BehaviorSubject(0);

  constructor() { 
    this.audio = new Audio();
    this.trackInfo$.subscribe(responseOk => {
      this.setAudio(responseOk);
    });
    this.listenAllEvents();
  }

  private listenAllEvents(): void{
    this.audio.addEventListener('timeupdate', this.calculatePercentage, false);
  }

  private calculatePercentage = () => {
    const {duration, currentTime} = this.audio;
    let percentage = (currentTime * 100) / duration;
    this.playerPercentage$.next(percentage);
  }

  private setAudio(track: any):void{
    this.audio.src = `assets/tracks/${track.url}`;
    this.audio.play();
  }

  public togglePlayer(): void{
    (this.audio.paused) ? this.audio.play() : this.audio.pause()
  }
}

Voy a explicar rapidamente los elementos de este servicio:

public audio!: HTMLAudioElement; Esta variable y las siguientes se declaran como publicas para que puedan ser usadas en toda la aplicación, el objeto HTMLAudioElement maneja los atributos, funciones y eventos necesarios para manejar una pista de audio.

public trackInfo$: BehaviorSubject = new BehaviorSubject(undefined); esta variable de tipo observable guardara los datos de la pista a reproducir, mediante una subscripcion se puede obtener o cambiar su información en toda la aplicacion.

public playerPercentage$: BehaviorSubject = new BehaviorSubject(0); Este observable manejara el porcentaje de reproducción de la canción.

constructor(): en el constructor se ejecutan métodos e instancias que deben procesarse en la creación del componente o servicio.

listenAllEvents(): dentro de este método se "escucha" un evento que emite el objeto audio, este evento se dispara cada vez que pasa un segundo de la canción, cuando se dispara el evento se llama a la función calculatePercentage .

calculatePercentage(): dentro de este método se hace una destructuracion del objeto audio para obtener los atributos de duración y tiempo actual, una vez se calcula el porcentaje se asigna y dispara el siguiente valor del observable playerPercentage$.

setAudio(): Esta es una de las principales funciones de este servicio, se encarga de indicarle al al objeto audio donde esta la canción e iniciar su reproducción.

togglePlayer(): esta función se encarga de alternar el estado de la canción.

Ahora vamos a crear la carpeta de contendrá las pistas de audio. Dentro de la carpeta
assets crea una carpeta llamada tracks.



Puedes clonar o descargar el repositorio del proyecto y copiar las pistas de audio
de la carpeta: Github



Ahora dentro de la carpeta shared vamos a crear una carpeta llamada data dentro de la
misma debes crear un archivo llamado track-list.ts, este archivo tendra un array con
toda la información de las pistas de audio.


Puedes ver la ruta correcta del archivo en el breadcrumb que esta marcado en rojo,
a simple vista se ve la primera pista de audio, la cual contiene información valiosa
como lo es el id, nombre de la canción, url de la portada del álbum, nombre del archivo
mp3 y datos básicos del artista, el codigo de este archivo es el siguiente:


export const trackList = [{
    "_id": 1,
    "name": "Getting Over",
    "album": "One Love",
    "cover": "https://jenesaispop.com/wp-content/uploads/2009/09/guetta_onelove.jpg",
    "artist": {
        "name": "David Guetta",
        "nickname": "David Guetta",
        "nationality": "FR"
    },
    "url": `track.mp3`
},
{
    "_id": 2,
    "name": "Snow Tha Product || BZRP Music Sessions #39",
    "album": "BZRP Music Sessions",
    "cover": "https://is5-ssl.mzstatic.com/image/thumb/Features125/v4/9c/b9/d0/9cb9d017-fcf6-28c6-81d0-e9ac5b0f359e/pr_source.png/800x800cc.jpg",
    "artist": {
        "name": "Snow",
        "nickname": "Snow",
        "nationality": "US"
    },
    "duration": {
        "start": 0,
        "end": 333
    },
    "url": `track-1.mp3`
},
{
    "_id": 3,
    "name": "Calypso (Original Mix)",
    "album": "Round Table Knights",
    "cover": "https://cdns-images.dzcdn.net/images/cover/1db3f8f185e68f26feaf0b9d72ff1645/350x350.jpg",
    "artist": {
        "name": "Round Table Knights",
        "nickname": "Round Table Knights",
        "nationality": "US"
    },
    "duration": {
        "start": 0,
        "end": 333
    },
    "url": `track-2.mp3`
},
{
    "_id": 4,
    "name": "Bad Habits",
    "album": "Ed Sheeran",
    "cover": "https://www.lahiguera.net/musicalia/artistas/ed_sheeran/disco/11372/tema/25301/ed_sheeran_bad_habits-portada.jpg",
    "artist": {
        "name": "Ed Sheeran",
        "nickname": "Ed Sheeran",
        "nationality": "UK"
    },
    "duration": {
        "start": 0,
        "end": 333
    },
    "url": `track-4.mp3`
},
{
    "_id": 5,
    "name": "BEBE (Official Video)",
    "album": "Giolì & Assia",
    "cover": "https://i.scdn.co/image/ab67616d0000b27345ca41b0d2352242c7c9d4bc",
    "artist": {
        "name": "Giolì & Assia",
        "nickname": "Giolì & Assia",
        "nationality": "IT"
    },
    "duration": {
        "start": 0,
        "end": 333
    },
    "url": `track-3.mp3`
},
{
    "_id": 6,
    "name": "T.N.T. (Live At River Plate, December 2009)",
    "album": "AC/DC",
    "cover": "https://cdns-images.dzcdn.net/images/cover/ba5eaf2f3a49768164d0728b7ba64372/500x500.jpg",
    "artist": {
        "name": "AC/DC",
        "nickname": "AC/DC",
        "nationality": "US"
    },
    "duration": {
        "start": 0,
        "end": 333
    },
    "url": `track-5.mp3`
},
{
    "_id": 7,
    "name": "50 Cent - Candy Shop (feat. Olivia)",
    "album": "50 Cent",
    "cover": "https://i.scdn.co/image/ab67616d0000b27391f7222996c531b981e7bb3d",
    "artist": {
        "name": "50 Cent",
        "nickname": "50 Cent",
        "nationality": "US"
    },
    "duration": {
        "start": 0,
        "end": 333
    },
    "url": `track-6.mp3`
},
{
    "_id": 8,
    "name": "Bésame💋",
    "album": "Valentino Ft MTZ Manuel Turizo (Video Oficial)",
    "cover": "https://i1.sndcdn.com/artworks-000247627460-1hqnjr-t500x500.jpg",
    "artist": {
        "name": "Valentino",
        "nickname": "Valentino",
        "nationality": "CO"
    },
    "duration": {
        "start": 0,
        "end": 333
    },
    "url": `track-7.mp3`
}
]
Ahora vamos a crear los componentes visuales sidebar y music-player dentro de la
carpeta shared/components, vamos con el sidebar.
ng g c shared/components/sidebar
Ahora vamos a modificar el HTML de este componente ubicado en:
src/app/shared/components/sidebar/sidebar.component.html


<div class="sidebar d-flex flex-column">
    <div class="row justify-content-center mt-4" [routerLink]="'home'">
        <p class="title">Music Player</p>
        <span class="material-symbols-outlined icon">music_note</span>
    </div>
    <button class="btn btn-dark m-2" [routerLink]="'home'">Home</button>
    <button class="btn btn-dark m-2" [routerLink]="'player'">Player</button>
</div>
  
Bootstrap nos ayuda a organizar la estructura del HTML, la clase flex-column organiza
todos los elementos de manera vertical, la clase row al contrario, de manera horizontal,
material-symbols-outlined invoca a la libreria Google Fonts para mostrar el icono de la
corchea. Debajo puedes observar la directiva [routerLink] la cual se encarga de
renderizar un componente sobre la etiqueta <router-link></router-link> la cual veremos
mas adelante.

Ahora veremos el CSS asociado a este HTML,
src/app/shared/components/sidebar/sidebar.component.css el codigo es el siguiente:

.sidebar{
    height: 85vh;
    background-color: var(--bg-dark-1);
}

.title{
    font-size: 30px;
    color: var(--main-white);
    font-weight: bolder;
}

.icon{
    color: var(--main-white);
    font-size: 40px;
}
  
En la clase sidebar se asigna la altura y el color, en title la talla de la letra,
su ancho y el color, en icon solo la talla y el color.

Vamos a crear el componente music-player con el siguiente comando:
ng g c shared/components/music-player

el codigo del archivo src/app/shared/components/music-player/music-player.component.ts
es el siguiente:
import { Component } from '@angular/core';
import { MultimediaService } from '../../services/multimedia.service';

@Component({
  selector: 'app-music-player',
  templateUrl: './music-player.component.html',
  styleUrls: ['./music-player.component.css']
})
export class MusicPlayerComponent {
  icon = 'play_circle';

  constructor(public multimediaService: MultimediaService){}

  toggleState(){
    this.icon = this.icon === 'play_circle' ? 'pause_circle' : 'play_circle'; 
    this.multimediaService.togglePlayer();
  }
}
    
Se declara la variable icon para guardar un string con el tipo de icono, se inyecta
el MultimediaService en el constructor de manera publica para que todos los componentes
compartan la misma información, en la función toggleState() ocurren dos cosas, se cambia
el state del icono entre play y pause ademas de que se llama a la funcion togglePlayer()
de MultimediaService para cambiar el estado de la reproducción del archivo.


Ahora vamos a ver el HTML:

src/app/shared/components/music-player/music-player.component.html
<div class="music-player row">
    <div class="col-md-2 d-flex justify-content-center">
        <span class="material-symbols-outlined m-2 icon-click" [innerHTML]="icon" (click)="toggleState()"></span>
    </div>
    <div class="col-md-8">
        <div class="song-name" *ngIf="multimediaService.trackInfo$ | async as track">{{track.name}} - {{track.artist.name}}</div>
        <div class="progress mt-4">
            <div class="progress-bar" role="progressbar" [style.width]="(multimediaService.playerPercentage$ | async)+'%'" aria-valuemin="0" aria-valuemax="100"></div>
        </div>
    </div>
    <div class="col-md-2 d-flex justify-content-end">
        <span class="material-symbols-outlined m-2 icon-click" [routerLink]="'player'">open_in_full</span>
    </div>
</div>
    
Este componente básicamente esta ubicado en la parte inferior de la pagina web,
contiene los botonesde play/pause, datos básicos de la canción,
y un link para ver los datos completos
de la canción en otro componente, en la primera columna esta el boton de play/pause
desde aquí se llama a la función toggleState() para cambiar el estado de la canción,
el la segunda columna, un dato interesante a recalcar es el la manera en la que
Angular permite usar los datos contenidos en MultimediaService, por ejemplo trackInfo$
es un observable pero no hace falta hacer una subscripcion para obtener sus datos,
simplemente se usa el pipe async y esto automáticamente se subscribe a la información
actualizada y ademas instancia esta información sobre la variable track que es usada
para obtener el nombre de la canción y del artista, lo mismo sucede con playerPercentage$
pero se usa su información directamente para aumentar el ancho de la barra de progreso,
en la tercera columna esta el botón de expandir, aquí se cambia de vista mediante la
directiva [routerLink].

Los estilos de este componente son los siguientes:

src/app/shared/components/music-player/music-player.component.css
.music-player{
    height: 15vh;
    background-color: var(--bg-dark-2);
}

.material-symbols-outlined {
    font-size: 130px;
    color: var(--main-white);
}

.song-name{
    font-size: 40px;
    color: var(--main-white);
}
      
A continuación vamos a crear la vista home.
ng g c modules/home
Vamos a ver la lógica de este componente:

src/app/modules/home/home.component.ts
import { Component } from '@angular/core';
import { trackList } from 'src/app/shared/data/track-list';
import { MultimediaService } from 'src/app/shared/services/multimedia.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent {
  tracks:any[] = trackList;
  selectedTrack: any = undefined;

  constructor(public multimediaService: MultimediaService){
    this.multimediaService.trackInfo$.subscribe( trackInfo => this.selectedTrack = trackInfo);
  }
  
  play(track:any){
    this.multimediaService.trackInfo$.next(track);
  }
}
          
la primera variable que se ve aqui es tracks, obtiene sus datos directamente de este
archivo: src/app/shared/data/track-list.ts, aqui se guarda la lista de canciones.

selectedTrack se usa para saber la pista actual, se actualiza subscribiendose al
observable trackinfo$ de MultimediaService.

en la función play() se asigna el valor actual de la pista a reproducir.


La estructura HTML esta ubicada en esta ruta:

src/app/modules/home/home.component.html
<div class="home">
    <table class="table table-striped table-dark">
        <thead>
            <tr>
            <th scope="col">#</th>
            <th scope="col">Song Name</th>
            <th scope="col">Artist</th>
            <th scope="col"></th>
            </tr>
        </thead>
        <tbody>
            <tr *ngFor="let track of tracks" [class.selected]="track._id === selectedTrack?._id">
            <th scope="row">{{track._id}}</th>
            <td>{{track.name}}</td>
            <td>{{track.artist.name}}</td>
            <td><span class="material-symbols-outlined icon-click" (click)="play(track)">play_circle</span></td>
            </tr>
        </tbody>
    </table>
</div>
            
Aquí vemos el uso de una tabla de Bootstrap y una iteracion de items mediante un
ngFor, se usa la clase selected sobre la fila si la canción iterada es igual a la
pista seleccionada actualmente.

los estilos de este componente son los siguientes:

src/app/modules/home/home.component.css
.home{
    height: 100%;
}

.selected{
    border: 4px solid purple;
}
Aquí se le asigna una altura del 100% al componente y la clase selected pone un borde
purpura sobre la canción seleccionada.
Ahora vamos a generar la vista player.
ng g c modules/player
la lógica de este componente es la siguiente:

src/app/modules/player/player.component.ts
import { Component } from '@angular/core';
import { MultimediaService } from 'src/app/shared/services/multimedia.service';

@Component({
  selector: 'app-player',
  templateUrl: './player.component.html',
  styleUrls: ['./player.component.css']
})
export class PlayerComponent {
  constructor(public multimediaService: MultimediaService){}
} 
  
Simplemente se inyecta MultimediaService sobre el constructor.

src/app/modules/player/player.component.html
<div class="player" *ngIf="multimediaService.trackInfo$ | async as track">
    <img [src]="track.cover" class="cover">

    <table class="table table-striped table-dark">
        <tbody>
          <tr>
            <th scope="row">Song</th>
            <td>{{track.name}}</td>
          </tr>
          <tr>
            <th scope="row">Album</th>
            <td>{{track.album}}</td>
          </tr>
          <tr>
            <th scope="row">Artist</th>
            <td>{{track.artist.name}}</td>
          </tr>
          <tr>
            <th scope="row">Nationality</th>
            <td>{{track.artist.nationality}}</td>
          </tr>
        </tbody>
      </table>
</div>
  
Dentro del condicional ngIf se hace una subscripcion de la información de la pista
mediante el pipe asyncsi existe una pista en reproducción se renderiza el componente en caso
contrario no se muestra, en esta vista se muestra una información mas completa de la
pista en reproducción, aquí se podrá apreciar el álbum al que pertenece la canción
mediante la etiqueta HTML img, usando la directiva [src] se establece la url fuente
necesaria para mostrar la imagen, este dato se obtiene del atributo cover del objeto
track.
src/app/modules/player/player.component.css
.player{
    height: 100%;
}
Se asigna la altura del componente a 100%.

A continuación vamos a configurar las rutas para que puedan renderizarse las vistas
home y player mediante la directiva [routerLink] colocada en los componente anteriores.
src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './modules/home/home.component';
import { PlayerComponent } from './modules/player/player.component';

const routes: Routes = [
  {
    path: 'home',
    component: HomeComponent
  },
  {
    path: 'player',
    component: PlayerComponent
  },
  {
    path: '',
    component: HomeComponent
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }    
      
El array de rutas se establece con dos objetos json, cada uno de ellos tiene dos
atributos, path: es la ruta que invocara la instanciacion del componente sobre la
etiqueta <router-link></router-link> y component: aquí se hace referencia al
componente en cuestión, si el path esta vacio como en esta url
http://localhost:4200/ se invoqua directamente la vista home.
Ahora vamos a modificar la clase principal de la aplicación, allí colocaremos el <router-link></router-link> y los componentes compartidos.

src/app/app.component.html

En este archivo se borrara todo el codigo generado por Angular y se colocara el
siguiente:
<div>
  <div class="row">
    <div class="col-md-2 pr-0">
      <app-sidebar></app-sidebar>
    </div>
    <div class="col-md-10 pl-0 bg">
      <router-outlet></router-outlet>
    </div>
  </div>
  <app-music-player></app-music-player>

</div>
Aqui podemos una fila con dos columnas, la primera columna invoca al componente 
sidebar y esta ubicada del lado izquierdo, la segunda esta ubicada del lado
derecho y contiene el <router-link></router-link> sobre el cual se presentaran
los componentes home y player, debajo de la fila esta el tablero principal con los
botones de reproducción y corresponde al componente music-player.

src/app/app.component.css
.bg{
    background-color: var(--bg-dark-3);
}  
con la clase bg cambiamos el fondo de la columna derecha a un color purpura oscuro.

Ahora llego el momento mas esperado, vamos a probar la aplicación, para ello 
ejecutaremos el siguiente comando en la terminal:
ng serve
ejecuta en el navegador la siguiente url: http://localhost:4200/ la web debe verse así:

Si presionas alguno de los botones de play en la parte superior derecha se debe escuchar 
la canción.

La canción seleccionada tendrá un un borde en la fila correspondiente de la tabla, 
puedes pausar la canción o expandirla para ver sus datos en la parte inferior derecha.


Presiona el botón Home del sidebar para ver la lista de canciones o Player para ver los
datos de la canción.

Espero que este tutorial haya sido de utilidad.


Comentarios

Entradas más populares de este blog

La nueva película de Barbie se vuelve viral usando tecnología de forma divertida

Toda la innovadora tecnología detrás de la transmisión de Wimbledon

¿La red 5g es peligrosa?