Engee documentation
Notebook

Universal media player

In modern analytics and data processing, it is often necessary to visualise and play media files directly in the development environment. The code presented here implements a universal media player capable of handling audio, video and images. This solution is particularly useful for researchers, data analysts and engineers working in Engee, providing them with a convenient tool to interactively view and listen to media files without the need for external applications.

The main technologies used in the example are

  1. Base64 encoding is used to embed media files directly into HTML via Data URLs, allowing them to be played without the need for a server or file system.
  2. HTML5 and JavaScript provide an interactive player interface with control buttons and the ability to switch between files.
  3. Julia packages:
    • Base64for encoding binary data;
    • FileIO for working with file system;
    • WAV for processing WAV-files (reading and changing sampling rate).

Implementation

This function detects the type of files, encodes them in Base64 and generates HTML with JavaScript to control playback.

Input parameters and supported formats

1. Basic function parameters:

  • path::AbstractString - the path to the media file or folder.

    • If a folder is specified, the player will download all supported files of the selected file type (mode).
    • If a file is specified, only the file will be processed.
  • mode::String (optional, default "audio") - media type:

    • "audio" - audio player (WAV, MP3)
    • "video" - video player (MP4)
    • "image" - image viewer (JPG, PNG, GIF, WEBP)
  • fs::Union{Nothing, Int} (optional, only for mode="audio") - sampling frequency (Hz).

    • If nothing - the original frequency of the file is used.
    • If a number is set (e.g. 24000) - WAV files are oversampled, MP3s remain unchanged.

2. Supported file formats:

Mode (mode) Expansions Processing
"audio" .wav, .mp3 WAV: oversampling (if fs is set), normalisation. MP3: encoded as is.
"video" .mp4 No processing, direct Base64 encoding.
"image" .jpg, .jpeg, .png, .gif, .webp Unmodified. Major image formats are supported.
In [ ]:
using Base64
using FileIO
using WAV

"""
Параметры:
- path: путь к файлу или папке с медиафайлами
- mode: тип контента ("audio", "video" или "image")
- fs: частота дискретизации (только для аудио, опционально)

Возвращает:
- HTML/JS виджет для отображения в Engee
"""
function media_player(path::AbstractString; mode::String="audio", fs::Union{Nothing, Int}=nothing)
    # Словарь поддерживаемых форматов и соответствующих им функций кодирования
    ext_map = Dict(
        # Аудио: WAV (с обработкой) и MP3 (как есть)
        "audio" => ([".wav", ".mp3"], (f -> base64encode_audio(f, fs))),
        # Видео: только MP4
        "video" => ([".mp4"], base64encode_video),
        # Изображения: основные форматы
        "image" => ([".jpg", ".jpeg", ".png", ".gif", ".webp"], base64encode_image)
    )
    
    # Проверка корректности режима работы
    haskey(ext_map, mode) || error("Неподдерживаемый режим: $mode")
    # Получаем список расширений и функцию-кодировщик для выбранного режима
    supported_extensions, encoder = ext_map[mode]
    # Получаем список файлов (один файл или все файлы из директории)
    files = isdir(path) ? readdir(path, join=true) : [path]
    # Фильтруем файлы по поддерживаемым расширениям
    selected_files = filter(f -> any(endswith(lowercase(f), ext) for ext in supported_extensions), files)
    isempty(selected_files) && error("Файлы формата $mode не найдены по указанному пути.")
    # Подготавливаем метаданные для отображения
    filenames = [basename(f) for f in selected_files]
    # Кодируем все файлы в base64 и получаем их MIME-типы
    encoded_data = [encoder(f) for f in selected_files]
    base64_encoded = [data[1] for data in encoded_data]
    mime_types = [data[2] for data in encoded_data]
    # Генерируем уникальный ID для элементов HTML (для поддержки нескольких плееров)
    unique_id = string(rand(UInt))
    # Настраиваем стили в зависимости от типа контента
    bg_color = "#f5f5f5"  # единый цвет фона для всех типов медиа
    text_color = "#555"   # единый цвет текста для всех типов медиа
    # Определяем HTML-тег в зависимости от типа медиа
    media_tag_name = mode == "audio" ? "audio" : mode == "video" ? "video" : "img"
    # Добавляем элементы управления для аудио/видео
    controls_attr = mode in ["audio", "video"] ? "controls autoplay" : ""
    # Генерируем HTML для медиа-элемента
    media_html = if mode == "image"
        # Тег img для изображений
        """<img id="$(media_tag_name)_$(unique_id)" src="data:$(mime_types[1]);base64,$(base64_encoded[1])" 
           style="max-width: 100%; border-radius: 10px;" />"""
    else
        # Теги audio/video с source внутри
        """
        <$media_tag_name id="$(media_tag_name)_$(unique_id)" $controls_attr style="width: 100%; border-radius: 10px;">
            <source id="src_$(unique_id)" src="data:$(mime_types[1]);base64,$(base64_encoded[1])" type="$(mime_types[1])" />
        </$media_tag_name>
        """
    end
    # Генерируем полный HTML-код плеера с элементами управления
    html_interface = """
    <div style="background: $bg_color; border-radius: 15px; padding: 15px; max-width: 640px; margin: 0 auto;">
        <!-- Панель управления с кнопками и именем файла -->
        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-size: 14px; color: $text_color;">
            <!-- Кнопка "назад" -->
            <button id="prevBtn_$(unique_id)" style="background: none; border: none; cursor: pointer; font-size: 22px;">⏮️</button>
            <!-- Имя текущего файла -->
            <span id="name_$(unique_id)" style="font-weight: bold;">$(filenames[1]) (1 of $(length(filenames)))</span>
            <!-- Кнопка "вперед" -->
            <button id="nextBtn_$(unique_id)" style="background: none; border: none; cursor: pointer; font-size: 22px;">⏭️</button>
            <!-- Дополнительная кнопка цикла для аудио -->
            $(mode == "audio" ? """<button id="loopToggle_$(unique_id)" style="background: none; border: none; cursor: pointer; font-size: 20px;" title="Toggle Loop">🔂</button>""" : "")
        </div>
        <!-- Основной медиа-элемент -->
        $media_html
    </div>
    
    <!-- JavaScript для управления плеером -->
    <script>
        // Текущий индекс файла
        let currentIndex_$(unique_id) = 0;
        // Данные файлов в base64
        const mediaFiles_$(unique_id) = [$(join(["\"$(e)\"" for e in base64_encoded], ","))];
        // MIME-типы файлов
        const mimeTypes_$(unique_id) = [$(join(["\"$(m)\"" for m in mime_types], ","))];
        // Имена файлов
        const fileNames_$(unique_id) = [$(join(["\"$(n)\"" for n in filenames], ","))];
        // Флаг циклического воспроизведения
        let loopEnabled_$(unique_id) = false;
        // Получаем DOM-элементы
        const mediaElement_$(unique_id) = document.getElementById("$(media_tag_name)_$(unique_id)");
        const sourceElement_$(unique_id) = document.getElementById("src_$(unique_id)");
        const nameElement_$(unique_id) = document.getElementById("name_$(unique_id)");
        /**
         * Функция обновления текущего медиа-файла
         * @param newIndex - индекс нового файла
         */
        function updateMedia_$(unique_id)(newIndex) {
            // Корректируем индекс при выходе за границы
            if (newIndex < 0) newIndex = mediaFiles_$(unique_id).length - 1;
            if (newIndex >= mediaFiles_$(unique_id).length) newIndex = 0;
            currentIndex_$(unique_id) = newIndex;
            const currentMime = mimeTypes_$(unique_id)[newIndex];
            const mediaData = "data:" + currentMime + ";base64," + mediaFiles_$(unique_id)[newIndex];
            // Обновляем в зависимости от типа медиа
            if ("$mode" === "image") {
                // Для изображений просто меняем src
                mediaElement_$(unique_id).src = mediaData;
            } else {
                // Для аудио/видео обновляем source и запускаем воспроизведение
                sourceElement_$(unique_id).src = mediaData;
                sourceElement_$(unique_id).type = currentMime;
                mediaElement_$(unique_id).load();
                mediaElement_$(unique_id).play();
            }
            // Обновляем отображаемое имя файла
            nameElement_$(unique_id).innerText = fileNames_$(unique_id)[newIndex] + 
                " (" + (newIndex + 1) + " of " + mediaFiles_$(unique_id).length + ")";
        }
        // Назначаем обработчики кнопок
        document.getElementById("prevBtn_$(unique_id)").onclick = function() {
            updateMedia_$(unique_id)(currentIndex_$(unique_id) - 1);
        };
        document.getElementById("nextBtn_$(unique_id)").onclick = function() {
            updateMedia_$(unique_id)(currentIndex_$(unique_id) + 1);
        };
        // Дополнительные функции для аудио
        $(mode == "audio" ? """
        const loopButton_$(unique_id) = document.getElementById("loopToggle_$(unique_id)");
        // Обработчик кнопки цикла
        loopButton_$(unique_id).onclick = function() {
            loopEnabled_$(unique_id) = !loopEnabled_$(unique_id);
            loopButton_$(unique_id).innerHTML = loopEnabled_$(unique_id) ? "🔁" : "🔂";
            loopButton_$(unique_id).style.color = loopEnabled_$(unique_id) ? "dodgerblue" : "";
        };
        // Обработчик окончания воспроизведения
        mediaElement_$(unique_id).addEventListener("ended", function() {
            if (loopEnabled_$(unique_id)) {
                updateMedia_$(unique_id)(currentIndex_$(unique_id) + 1);
            }
        });
        """ : "")
    </script>
    """
    # Отображаем сгенерированный HTML
    display("text/html", html_interface)
end

# Кодирует аудиофайл в base64 с возможной передискретизацией (для WAV).
function base64encode_audio(filepath, fs)
    ext = lowercase(splitext(filepath)[2])
    if ext == ".wav"
        # Чтение WAV-файла
        audio_data, original_fs = wavread(filepath)
        # Нормализация данных (моно/стерео)
        audio_data = ndims(audio_data) == 1 ? reshape(audio_data, :, 1) : audio_data
        audio_data = clamp.(audio_data, -1, 1)  # Ограничение амплитуды
        # Конвертация в 16-битный целочисленный формат
        audio_int = round.(Int16, audio_data .* typemax(Int16))
        # Запись в буфер с новой частотой дискретизации (если указана)
        buffer = IOBuffer()
        wavwrite(audio_int, buffer; Fs=fs === nothing ? original_fs : fs, nbits=16)
        seekstart(buffer)
        # Кодирование в base64
        return base64encode(take!(buffer)), "audio/wav"
    elseif ext == ".mp3"
        # MP3 кодируется как есть
        return base64encode(read(filepath)), "audio/mpeg"
    else
        error("Неподдерживаемый аудиоформат: $ext")
    end
end

function base64encode_video(filepath) # Кодирует видеофайл (MP4) в base64.
    lowercase(splitext(filepath)[2]) == ".mp4" || error("Поддерживается только MP4")
    return base64encode(read(filepath)), "video/mp4"
end

function base64encode_image(filepath) # Кодирует изображение в base64.
    ext = lowercase(splitext(filepath)[2])
    # Определяем MIME-тип по расширению
    mime_type = if ext == ".jpg" || ext == ".jpeg"
        "image/jpeg"
    elseif ext == ".png"
        "image/png"
    elseif ext == ".gif"
        "image/gif"
    elseif ext == ".webp"
        "image/webp"
    else
        error("Неподдерживаемый формат изображения: $ext")
    end
    return base64encode(read(filepath)), mime_type
end
Out[0]:
base64encode_image (generic function with 1 method)

Let's move on to testing the developed algorithm and start with the peculiarities of processing.

  • For WAV:
    • automatic amplitude normalisation (limited to [-1, 1]),
    • mono and stereo support,
    • if fs is set, sampling frequency changes (only for WAV, MP3 ignores this parameter).
  • For MP3/MP4/images:
    • data is Base64 encoded with no changes.
  • The interface contains:
    • toggle buttons (⏮️, ⏭️),
    • for audio - cycle button (🔂/🔁),
    • autorun at switching (for audio/video).
In [ ]:
media_player("$(@__DIR__)/test_files/sample_3s.wav", mode="audio")
sample_3s.wav (1 of 1)
In [ ]:
media_player("test_files", mode="audio", fs = 24000)
sample_3s.wav (1 of 2)
In [ ]:
media_player("test_files", mode="image")
Intro Engee.gif (1 of 2)
No description has been provided for this image
In [ ]:
media_player("test_files", mode="video")
Engee neon.mp4 (1 of 1)

Conclusion

The presented example demonstrates Engee's capabilities in creating interactive multimedia tools.

This media player will be a convenient solution for:

  1. quickly check and analyse media files during the development process,
  2. show the results of audio and video processing,
  3. create interactive reports with media elements.

Particularly valuable is the ability to programmatically process audio (e.g. change the sample rate) before playback. This makes the tool useful for digital signal processing and machine learning tasks.

This media player is an excellent example of how you can go beyond traditional computing tasks without leaving the engineering environment.