Media Library

Felipe Almeida

AuthorFelipe AlmeidaPlugin TypeCommunity PluginPriceFreeVersionvlatestLicenseMIT
NativePHP^3.0PHP^8.2Android21+

Query the device's audio library from a NativePHP Mobile app. On Android it reads the system MediaStore, so you get ready-to-use track metadata (title, artist, album, genre, duration, year, track number), the containing folder, a content URI for playback and an artwork URI — without parsing files yourself.

Status: Android is implemented and battle-tested. iOS is scaffolded but not yet implemented.


Requirements

  • NativePHP Mobile ^3.0
  • PHP ^8.2
  • Android minSdk 21+ (folder/bucket grouping uses MediaStore buckets on API 29+)

Installation

composer require musicplayer/media-library

Publish the NativePHP plugin provider (once per app) and register the plugin:

php artisan vendor:publish --tag=nativephp-plugins-provider
php artisan native:plugin:register musicplayer/media-library

Verify it is registered:

php artisan native:plugin:list

Local development (from source)

Add a path repository to your app's composer.json, then require it:

{
    "repositories": [
        { "type": "path", "url": "./packages/musicplayer/media-library" }
    ]
}

composer require musicplayer/media-library:@dev


Permissions

The plugin declares the read permissions in its manifest, which are merged into your AndroidManifest.xml at build time:

  • android.permission.READ_MEDIA_AUDIO (Android 13+ / API 33+)
  • android.permission.READ_EXTERNAL_STORAGE (older versions)

You must have the permission granted at runtime before calling queryAudio() — otherwise the MediaStore returns an empty result (no exception). Request it through your app's normal permission flow. For quick testing you can grant it via ADB:

adb shell pm grant <your.app.id> android.permission.READ_MEDIA_AUDIO


Usage

use Musicplayer\MediaLibrary\Facades\MediaLibrary;

// Confirm the native side is loaded
$status = MediaLibrary::getStatus();
// => ['status' => 'ready', 'provider' => 'MediaStore']

// Get every audio track indexed by the device
$tracks = MediaLibrary::queryAudio();

You can also resolve it from the container instead of the facade:

$tracks = app(\Musicplayer\MediaLibrary\MediaLibrary::class)->queryAudio();

Return shape

queryAudio() returns an array of associative arrays, one per track:

Key Type Description
id string MediaStore item id
uri string content:// URI of the track (use this to play it)
path string Absolute file path (_data) when available
title string Track title
artist string Artist
album string Album
album_id string MediaStore album id
artwork_uri string content://media/external/audio/albumart/<album_id>
duration int Duration in seconds
year int Release year (0 if unknown)
track int Track number (0 if unknown)
size int File size in bytes
mime string MIME type (e.g. audio/mpeg)
folder string Containing folder name (MediaStore bucket / parent dir)

Empty string / 0 is used for missing values (never null), so the payload is always JSON-safe across the bridge.

Example: group by folder

$byFolder = collect(MediaLibrary::queryAudio())
    ->groupBy('folder')
    ->map->count();


Runtime permission

use Musicplayer\MediaLibrary\Facades\MediaLibrary;

if (! MediaLibrary::checkPermission()) {
    MediaLibrary::requestPermission(); // shows the native permission dialog
    // Ask the user to retry once granted.
}

Folder picker (Storage Access Framework)

pickFolder() opens the native folder picker. It is asynchronous: the result is delivered to the frontend as a DOM native-event, and the access is persisted across app restarts (takePersistableUriPermission).

MediaLibrary::pickFolder();

// Vue / JS — listen for the chosen folder
document.addEventListener('native-event', (e) => {
    if (e.detail.event === 'folder:chosen') {
        const { uri, name } = e.detail.payload; // content tree URI + folder name
        // e.g. POST it to your backend to persist + scan
    }
    // 'folder:cancelled' is emitted if the user backs out.
});

Then read the audio inside the chosen tree (content URIs, via MediaMetadataRetriever):

$tracks = MediaLibrary::scanTree($treeUri); // [{ uri, title, artist, album, duration, size, mime }, ...]

Building

Native code changes are picked up when you (re)build the native project:

php artisan native:run        # prepares the bundle, compiles the plugin, runs the app

On Windows, if native:run cannot invoke the Gradle wrapper, build directly:

cd nativephp/android
./gradlew.bat assembleDebug   # or ./gradlew on macOS/Linux


Bridge functions

Function Params Returns
MediaLibrary.QueryAudio context { tracks: [...], count: int }
MediaLibrary.CheckPermission context { granted: bool }
MediaLibrary.RequestPermission activity { granted: bool, requested: bool }
MediaLibrary.PickFolder activity { started: true } — result via folder:chosen event
MediaLibrary.ScanTree context { tracks: [...], count: int }
MediaLibrary.GetStatus context { status: "ready", provider: "MediaStore" }

Roadmap

  • iOS implementation (MPMediaQuery)
  • Optional filtering parameters (by folder, by album, paging)
  • Album-art bytes accessor for webview rendering