All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sun.media.jfxmedia.locator.HLSConnectionHolder Maven / Gradle / Ivy

There is a newer version: 24-ea+5
Show newest version
/*
 * Copyright (c) 2010, 2022, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.sun.media.jfxmedia.locator;

import com.sun.media.jfxmedia.MediaError;
import com.sun.media.jfxmediaimpl.MediaUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLConnection;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;

final class HLSConnectionHolder extends ConnectionHolder {

    private URLConnection urlConnection = null;
    private URLConnection headerConnection = null;
    private ReadableByteChannel headerChannel = null;
    private PlaylistThread playlistThread = new PlaylistThread();
    private VariantPlaylist variantPlaylist = null;
    private Playlist currentPlaylist = null;
    private int mediaFileIndex = -1;
    private CountDownLatch readySignal = new CountDownLatch(1);
    private Semaphore liveSemaphore = new Semaphore(0);
    private boolean isPlaylistClosed = false;
    private boolean isBitrateAdjustable = false;
    private long startTime = -1;
    private boolean sendHeader = false;
    private static final long HLS_VALUE_FLOAT_MULTIPLIER = 1000;
    private static final int HLS_PROP_GET_DURATION = 1;
    private static final int HLS_PROP_GET_HLS_MODE = 2;
    private static final int HLS_PROP_GET_MIMETYPE = 3;
    private static final int HLS_PROP_LOAD_SEGMENT = 4;
    private static final int HLS_PROP_SEGMENT_START_TIME = 5;
    private static final int HLS_VALUE_MIMETYPE_UNKNOWN = -1;
    private static final int HLS_VALUE_MIMETYPE_MP2T = 1;
    private static final int HLS_VALUE_MIMETYPE_MP3 = 2;
    private static final int HLS_VALUE_MIMETYPE_FMP4 = 3;
    private static final int HLS_VALUE_MIMETYPE_AAC = 4;
    private static final String CHARSET_UTF_8 = "UTF-8";
    private static final String CHARSET_US_ASCII = "US-ASCII";

    HLSConnectionHolder(URI uri) throws IOException {
        playlistThread.setPlaylistURI(uri);
        init();
    }

    private void init() {
        playlistThread.putState(PlaylistThread.STATE_INIT);
        playlistThread.start();
    }

    @Override
    public int readNextBlock() throws IOException {
        if (isBitrateAdjustable && startTime == -1) {
            startTime = System.currentTimeMillis();
        }

        if (headerChannel != null) {
            buffer.rewind();
            if (buffer.limit() < buffer.capacity()) {
                buffer.limit(buffer.capacity());
            }
            int read = headerChannel.read(buffer);
            if (read == -1) {
                resetHeaderConnection();
            } else {
                return read;
            }
        }

        int read = super.readNextBlock();
        if (isBitrateAdjustable && read == -1) {
            long readTime = System.currentTimeMillis() - startTime;
            startTime = -1;
            adjustBitrate(readTime);
        }

        return read;
    }

    @Override
    int readBlock(long position, int size) throws IOException {
        throw new IOException();
    }

    @Override
    boolean needBuffer() {
        return true;
    }

    @Override
    boolean isSeekable() {
        return true;
    }

    @Override
    boolean isRandomAccess() {
        return false; // Only by segments
    }

    @Override
    public long seek(long position) {
        try {
            readySignal.await();
        } catch (InterruptedException e) {
            return -1;
        }

        return (long) (currentPlaylist.seek(position) * HLS_VALUE_FLOAT_MULTIPLIER);
    }

    @Override
    public void closeConnection() {
        currentPlaylist.close();
        super.closeConnection();
        resetConnection();
        playlistThread.putState(PlaylistThread.STATE_EXIT);
    }

    @Override
    int property(int prop, int value) {
        try {
            readySignal.await();
        } catch (InterruptedException e) {
            return -1;
        }

        switch (prop) {
            case HLS_PROP_GET_DURATION:
                return (int)(currentPlaylist.getDuration() * HLS_VALUE_FLOAT_MULTIPLIER);
            case HLS_PROP_GET_HLS_MODE:
                return 1;
            case HLS_PROP_GET_MIMETYPE:
                return currentPlaylist.getMimeType();
            case HLS_PROP_LOAD_SEGMENT:
                return loadNextSegment();
            case HLS_PROP_SEGMENT_START_TIME:
                return (int)(currentPlaylist.getMediaFileStartTime() * HLS_VALUE_FLOAT_MULTIPLIER);
            default:
                return -1;
        }
    }

    private void resetConnection() {
        super.closeConnection();

        resetHeaderConnection();

        Locator.closeConnection(urlConnection);
        urlConnection = null;
    }

    private void resetHeaderConnection() {
        try {
            if (headerChannel != null) {
                headerChannel.close();
            }
        } catch (IOException ioex) {}
        finally {
            headerChannel = null;
        }

        Locator.closeConnection(headerConnection);
        headerConnection = null;
    }


    // Returns -1 EOS or critical error
    // Returns positive size of segment if no issues.
    // Returns negative size of segment if discontinuity.
    private int loadNextSegment() {
        resetConnection();

        String mediaFile;
        int headerLength = 0;

        if (sendHeader) {
            mediaFile = currentPlaylist.getHeaderFile();
            if (mediaFile == null) {
                return -1;
            }

            try {
                URI uri = new URI(mediaFile);
                headerConnection = uri.toURL().openConnection();
                headerChannel = openHeaderChannel();
                headerLength = headerConnection.getContentLength();
            } catch (IOException | URISyntaxException e) {
                return -1;
            }
            sendHeader = false;
        }

        mediaFile = currentPlaylist.getNextMediaFile();
        if (mediaFile == null) {
            if (currentPlaylist.isFragmentedMP4()) {
                sendHeader = true;
            }
            return -1;
        }

        try {
            URI uri = new URI(mediaFile);
            urlConnection = uri.toURL().openConnection();
            channel = openChannel();
        } catch (IOException | URISyntaxException e) {
            return -1;
        }

        if (currentPlaylist.isCurrentMediaFileDiscontinuity()) {
            return (-1 * (urlConnection.getContentLength() + headerLength));
        } else {
            return (urlConnection.getContentLength() + headerLength);
        }
    }

    private ReadableByteChannel openChannel() throws IOException {
        return Channels.newChannel(urlConnection.getInputStream());
    }

    private ReadableByteChannel openHeaderChannel() throws IOException {
        return Channels.newChannel(headerConnection.getInputStream());
    }

    private void adjustBitrate(long readTime) {
        int avgBitrate = (int)(((long) urlConnection.getContentLength() * 8 * 1000) / readTime);

        Playlist playlist = variantPlaylist.getPlaylistBasedOnBitrate(avgBitrate);
        if (playlist != null && playlist != currentPlaylist) {
            if (currentPlaylist.isLive()) {
                playlist.update(currentPlaylist.getNextMediaFile());
                playlistThread.setReloadPlaylist(playlist);
            }

            playlist.setForceDiscontinuity(true);
            currentPlaylist = playlist;
            if (currentPlaylist.isFragmentedMP4()) {
                sendHeader = true;
            }
        }
    }

    private static String stripParameters(String mediaFile) {
        int qp = mediaFile.indexOf('?');
        if (qp > 0) {
            mediaFile = mediaFile.substring(0, qp); // Strip all possible http parameters.
        }
        return mediaFile;
    }

    private class PlaylistThread extends Thread {

        public static final int STATE_INIT = 0;
        public static final int STATE_EXIT = 1;
        public static final int STATE_RELOAD_PLAYLIST = 2;
        private final BlockingQueue stateQueue = new LinkedBlockingQueue<>();
        private URI playlistURI = null;
        private Playlist reloadPlaylist = null;
        private final Object reloadLock = new Object();
        private volatile boolean stopped = false;

        private PlaylistThread() {
            setName("JFXMedia HLS Playlist Thread");
            setDaemon(true);
        }

        private void setPlaylistURI(URI playlistURI) {
            this.playlistURI = playlistURI;
        }

        private void setReloadPlaylist(Playlist playlist) {
            synchronized(reloadLock) {
                reloadPlaylist = playlist;
            }
        }

        @Override
        public void run() {
            while (!stopped) {
                try {
                    int state = stateQueue.take();
                    switch (state) {
                        case STATE_INIT:
                            stateInit();
                            break;
                        case STATE_EXIT:
                            stopped = true;
                            break;
                        case STATE_RELOAD_PLAYLIST:
                            stateReloadPlaylist();
                            break;
                        default:
                            break;
                    }
                } catch (InterruptedException e) {
                }
            }
        }

        private void putState(int state) {
            if (stateQueue != null) {
                try {
                    stateQueue.put(state);
                } catch (InterruptedException ex) {
                }
            }
        }

        private void stateInit() {
            if (playlistURI == null) {
                return;
            }

            try {
                PlaylistParser parser = new PlaylistParser();
                parser.load(playlistURI);

                if (parser.isVariantPlaylist()) {
                    variantPlaylist = new VariantPlaylist(playlistURI);

                    while (parser.hasNext()) {
                        variantPlaylist.addPlaylistInfo(parser.getString(), parser.getInteger());
                    }
                } else {
                    if (currentPlaylist == null) {
                        currentPlaylist = new Playlist(parser.isLivePlaylist(), parser.getTargetDuration());
                        currentPlaylist.setPlaylistURI(playlistURI);
                    }

                    if (currentPlaylist.setSequenceNumber(parser.getSequenceNumber())) {
                        while (parser.hasNext()) {
                            currentPlaylist.addMediaFile(parser.getString(), parser.getDouble(), parser.getBoolean());
                        }
                    }

                    if (variantPlaylist != null) {
                        variantPlaylist.addPlaylist(currentPlaylist);
                    }
                }

                // Update variant playlists
                if (variantPlaylist != null) {
                    while (variantPlaylist.hasNext()) {
                        try {
                            currentPlaylist = new Playlist(variantPlaylist.getPlaylistURI());
                            currentPlaylist.update(null);
                            variantPlaylist.addPlaylist(currentPlaylist);
                        } catch (URISyntaxException | MalformedURLException e) {
                        }
                    }
                }

                // Always start with first data playlist
                if (variantPlaylist != null) {
                    currentPlaylist = variantPlaylist.getPlaylist(0);
                    isBitrateAdjustable = true;
                }

                // Start reloading live playlist
                if (currentPlaylist.isLive()) {
                    setReloadPlaylist(currentPlaylist);
                    putState(STATE_RELOAD_PLAYLIST);
                }

                // If we have playlist with fMP4, set flag to add header
                // to first data segment and adjust index to 0
                if (currentPlaylist.isFragmentedMP4()) {
                    sendHeader = true;
                    mediaFileIndex = 0;
                }
            } finally {
                readySignal.countDown();
            }
        }

        private void stateReloadPlaylist() {
            try {
                long timeout;
                synchronized(reloadLock) {
                    timeout = reloadPlaylist.getTargetDuration() / 2;
                }
                Thread.sleep(timeout);
            } catch (InterruptedException ex) {
                return;
            }

            synchronized(reloadLock) {
                reloadPlaylist.update(null);
            }

            putState(STATE_RELOAD_PLAYLIST);
        }
    }

    private static class PlaylistParser {

        private boolean isFirstLine = true;
        private boolean isLineMediaFileURI = false;
        private boolean isEndList = false;
        private boolean isLinePlaylistURI = false;
        private boolean isVariantPlaylist = false;
        private boolean isDiscontinuity = false;
        private int targetDuration = 0;
        private int sequenceNumber = 0;
        private int dataListIndex = -1;
        private List dataListString = new ArrayList<>();
        private List dataListInteger = new ArrayList<>();
        private List dataListDouble = new ArrayList<>();
        private List dataListBoolean = new ArrayList<>();

        private void load(URI uri) {
            HttpURLConnection connection = null;
            BufferedReader reader = null;
            try {
                connection = (HttpURLConnection) uri.toURL().openConnection();
                connection.setRequestMethod("GET");

                if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                    MediaUtils.error(this, MediaError.ERROR_LOCATOR_CONNECTION_LOST.code(), "HTTP responce code: " + connection.getResponseCode(), null);
                }

                Charset charset = getCharset(uri.toURL().toExternalForm(), connection.getContentType());
                if (charset != null) {
                    reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), charset));
                }

                if (reader != null) {
                    boolean result;
                    do {
                        result = parseLine(reader.readLine());
                    } while (result);
                }
            } catch (MalformedURLException e) {
            } catch (IOException e) {
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException e) {}

                    Locator.closeConnection(connection);
                }
            }
        }

        private boolean isVariantPlaylist() {
            return isVariantPlaylist;
        }

        private boolean isLivePlaylist() {
            return !isEndList;
        }

        private int getTargetDuration() {
            return targetDuration;
        }

        private int getSequenceNumber() {
            return sequenceNumber;
        }

        private boolean hasNext() {
            dataListIndex++;
            return dataListString.size() > dataListIndex ||
                    dataListInteger.size() > dataListIndex ||
                    dataListDouble.size() > dataListIndex ||
                    dataListBoolean.size() > dataListIndex;
        }

        private String getString() {
            return dataListString.get(dataListIndex);
        }

        private Integer getInteger() {
            return dataListInteger.get(dataListIndex);
        }

        private Double getDouble() {
            return dataListDouble.get(dataListIndex);
        }

        private Boolean getBoolean() {
            return dataListBoolean.get(dataListIndex);
        }

        private boolean parseLine(String line) {
            if (line == null) {
                return false;
            }

            // First line of playlist must be "#EXTM3U"
            if (isFirstLine) {
                if (line.compareTo("#EXTM3U") != 0) {
                    return false;
                }

                isFirstLine = false;
                return true;
            }

            // Ignore blank lines and comments
            if (line.isEmpty() || (line.startsWith("#") && !line.startsWith("#EXT"))) {
                return true;
            }

            if (line.startsWith("#EXTINF")) { // #EXTINF
                //#EXTINF:,
                String[] s1 = line.split(":");
                if (s1.length == 2 && s1[1].length() > 0) {
                    String[] s2 = s1[1].split(",");
                    if (s2.length >= 1) { // We have duration
                        dataListDouble.add(Double.parseDouble(s2[0]));
                    }
                }

                isLineMediaFileURI = true;
            } else if (line.startsWith("#EXT-X-TARGETDURATION")) {
                // #EXT-X-TARGETDURATION:<s>
                String[] s1 = line.split(":");
                if (s1.length == 2 && s1[1].length() > 0) {
                    targetDuration = Integer.parseInt(s1[1]);
                }
            } else if (line.startsWith("#EXT-X-MEDIA-SEQUENCE")) {
                // #EXT-X-MEDIA-SEQUENCE:<number>
                String[] s1 = line.split(":");
                if (s1.length == 2 && s1[1].length() > 0) {
                    sequenceNumber = Integer.parseInt(s1[1]);
                }
            } else if (line.startsWith("#EXT-X-STREAM-INF")) {
                // #EXT-X-STREAM-INF:<attribute-list>
                isVariantPlaylist = true;

                int bitrate = 0;
                String[] s1 = line.split(":");
                if (s1.length == 2 && s1[1].length() > 0) {
                    String[] s2 = s1[1].split(",");
                    if (s2.length > 0) {
                        for (int i = 0; i < s2.length; i++) {
                            s2[i] = s2[i].trim();
                            if (s2[i].startsWith("BANDWIDTH")) {
                                String[] s3 = s2[i].split("=");
                                if (s3.length == 2 && s3[1].length() > 0) {
                                    bitrate = Integer.parseInt(s3[1]);
                                }
                            }
                        }
                    }
                }

                if (bitrate < 1) {
                    return false;
                }

                dataListInteger.add(bitrate);

                isLinePlaylistURI = true; // Next line will be URI to playlist
            } else if (line.startsWith("#EXT-X-ENDLIST")) { // #EXT-X-ENDLIST
                isEndList = true;
            } else if (line.startsWith("#EXT-X-DISCONTINUITY")) { // #EXT-X-DISCONTINUITY
                isDiscontinuity = true;
            } else if (line.startsWith("#EXT-X-MAP")) {
                String[] s1 = line.split(":");
                if (s1.length == 2 && s1[1].length() > 0) {
                    String[] s2 = s1[1].split(",");
                    if (s2.length > 0) {
                        for (int i = 0; i < s2.length; i++) {
                            s2[i] = s2[i].trim();
                            if (s2[i].startsWith("URI")) {
                                String[] s3 = s2[i].split("=");
                                if (s3.length == 2 && s3[1].length() > 0) {
                                    String dataFile =
                                            s3[1].replaceAll("^\"+|\"+$", "");
                                    dataListString.add(dataFile);
                                    // GStreamer expects start of stream to be
                                    // discontinuity.
                                    dataListBoolean.add(true);
                                    dataListDouble.add(Double.valueOf(targetDuration));
                                }
                            }
                        }
                    }
                }
            } else if (isLinePlaylistURI) {
                isLinePlaylistURI = false;
                dataListString.add(line);
            } else if (isLineMediaFileURI) {
                // We can have additional tags after #EXTINF such as
                // #EXT-X-BITRATE for fMP4 playlist, so ignore them.
                if (!line.startsWith("#")) {
                    isLineMediaFileURI = false;
                    dataListString.add(line);
                    dataListBoolean.add(isDiscontinuity);
                    isDiscontinuity = false;
                }
            }

            return true;
        }

        private Charset getCharset(String url, String mimeType) {
            if ((url != null && stripParameters(url).endsWith(".m3u8")) || (mimeType != null && mimeType.equals("application/vnd.apple.mpegurl"))) {
                if (Charset.isSupported(CHARSET_UTF_8)) {
                    return Charset.forName(CHARSET_UTF_8);
                }
            } else if ((url != null && stripParameters(url).endsWith(".m3u")) || (mimeType != null && mimeType.equals("audio/mpegurl"))) {
                if (Charset.isSupported(CHARSET_US_ASCII)) {
                    return Charset.forName(CHARSET_US_ASCII);
                }
            }

            return null;
        }
    }

    private static class VariantPlaylist {

        private URI playlistURI = null;
        private int infoIndex = -1;
        private List<String> playlistsLocations = new ArrayList<>();
        private List<Integer> playlistsBitrates = new ArrayList<>();
        private List<Playlist> playlists = new ArrayList<>();
        private String mediaFileExtension = null; // Will be set to media file extension of first playlist

        private VariantPlaylist(URI uri) {
            playlistURI = uri;
        }

        private void addPlaylistInfo(String location, int bitrate) {
            playlistsLocations.add(location);
            playlistsBitrates.add(bitrate);
        }

        private void addPlaylist(Playlist playlist) {
            if (mediaFileExtension == null) {
                mediaFileExtension = playlist.getMediaFileExtension();
            } else {
                if (!mediaFileExtension.equals(playlist.getMediaFileExtension())) {
                    playlistsLocations.remove(infoIndex);
                    playlistsBitrates.remove(infoIndex);
                    infoIndex--;
                    return; // Ignore playlist with different media type
                }
            }
            playlists.add(playlist);
        }

        private Playlist getPlaylist(int index) {
            if (playlists.size() > index) {
                return playlists.get(index);
            } else {
                return null;
            }
        }

        private boolean hasNext() {
            infoIndex++;
            return playlistsLocations.size() > infoIndex &&
                    playlistsBitrates.size() > infoIndex;
        }

        private URI getPlaylistURI() throws URISyntaxException, MalformedURLException {
            String location = playlistsLocations.get(infoIndex);
            if (location.startsWith("http://") || location.startsWith("https://")) {
                return new URI(location);
            } else {
                return new URI(playlistURI.toURL().toString().substring(0, playlistURI.toURL().toString().lastIndexOf("/") + 1) + location);
            }
        }

        private Playlist getPlaylistBasedOnBitrate(int bitrate) {
            int playlistIndex = -1;
            int playlistBitrate = 0;

            // Get bitrate that less then requested bitrate, but most closed to it
            for (int i = 0; i < playlistsBitrates.size(); i++) {
                int b = playlistsBitrates.get(i);
                if (b < bitrate) {
                    if (playlistIndex != -1) {
                        if (b > playlistBitrate) {
                            playlistBitrate = b;
                            playlistIndex = i;
                        }
                    } else {
                        playlistIndex = i;
                    }
                }
            }

            // If we did not find one (stall), then get the lowest bitrate possible
            if (playlistIndex == -1) {
                for (int i = 0; i < playlistsBitrates.size(); i++) {
                    int b = playlistsBitrates.get(i);
                    if (b < playlistBitrate || playlistIndex == -1) {
                        playlistBitrate = b;
                        playlistIndex = i;
                    }
                }
            }

            // Just in case
            if (playlistIndex < 0 || playlistIndex >= playlists.size()) {
                return null;
             } else {
                return playlists.get(playlistIndex);
            }
        }
    }

    private class Playlist {

        private boolean isLive = false;
        private volatile boolean isLiveWaiting = false;
        private volatile boolean isLiveStop = false;
        private long targetDuration = 0;
        private URI playlistURI = null;
        private final Object lock = new Object();
        private final List<String> mediaFiles = new ArrayList<>();
        private final List<Double> mediaFilesStartTimes = new ArrayList<>();
        private final List<Boolean> mediaFilesDiscontinuities = new ArrayList<>();
        private boolean needBaseURI = true;
        private String baseURI = null;
        private double startTime = 0.0;
        private double duration = 0.0;
        private int sequenceNumber = -1;
        private int sequenceNumberStart = -1;
        private boolean sequenceNumberUpdated = false;
        private boolean forceDiscontinuity = false;
        private int mimeType = HLS_VALUE_MIMETYPE_UNKNOWN;

        private Playlist(boolean isLive, int targetDuration) {
            this.isLive = isLive;
            this.targetDuration = targetDuration * 1000;

            if (isLive) {
                duration = -1.0;
            }
        }

        private Playlist(URI uri) {
            playlistURI = uri;
        }

        private void update(String nextMediaFile) {
            PlaylistParser parser = new PlaylistParser();
            parser.load(playlistURI);

            isLive = parser.isLivePlaylist();
            targetDuration = parser.getTargetDuration() * 1000;

            if (isLive) {
                duration = -1.0;
            }

            if (setSequenceNumber(parser.getSequenceNumber())) {
                while (parser.hasNext()) {
                    addMediaFile(parser.getString(), parser.getDouble(), parser.getBoolean());
                }
            }

            if (nextMediaFile != null) {
                synchronized (lock) {
                    for (int i = 0; i < mediaFiles.size(); i++) {
                        String mediaFile = mediaFiles.get(i);
                        if (nextMediaFile.endsWith(mediaFile)) {
                            mediaFileIndex = i - 1;
                            break;
                        }
                    }
                }
            }
        }

        private boolean isLive() {
            return isLive;
        }

        private boolean isFragmentedMP4() {
            return (getMimeType() == HLS_VALUE_MIMETYPE_FMP4);
        }

        private long getTargetDuration() {
            return targetDuration;
        }

        private void setPlaylistURI(URI uri) {
            playlistURI = uri;
        }

        private void addMediaFile(String URI, double duration, boolean isDiscontinuity) {
            synchronized (lock) {

                if (needBaseURI) {
                    setBaseURI(playlistURI.toString(), URI);
                }

                if (isLive) {
                    if (sequenceNumberUpdated) {
                        int index = mediaFiles.indexOf(URI);
                        if (index != -1) {
                            for (int i = 0; i < index; i++) {
                                mediaFiles.remove(0);
                                mediaFilesDiscontinuities.remove(0);
                                if (mediaFileIndex == -1) {
                                    forceDiscontinuity = true;
                                }
                                if (mediaFileIndex >= 0) {
                                    mediaFileIndex--;
                                }
                            }
                        }
                        sequenceNumberUpdated = false;
                    }

                    if (mediaFiles.contains(URI)) {
                        return; // Nothing to add
                    }
                }

                mediaFiles.add(URI);
                mediaFilesDiscontinuities.add(isDiscontinuity);

                if (isLive) {
                    if (isLiveWaiting) {
                        liveSemaphore.release();
                    }
                } else {
                    mediaFilesStartTimes.add(this.startTime);
                    this.startTime += duration;

                    // For fragmented MP4 we should not add duration of first
                    // segment, since it is header without actuall data.
                    if (mediaFiles.size() == 1) {
                        if (!isFragmentedMP4()) {
                            this.duration += duration;
                        }
                    } else {
                        this.duration += duration;
                    }
                }
            }
        }

        private String getNextMediaFile() {
            if (isLive) {
                synchronized (lock) {
                    isLiveWaiting = ((mediaFileIndex + 1) >= mediaFiles.size());
                }
                if (isLiveWaiting) {
                    try {
                        liveSemaphore.acquire();
                        isLiveWaiting = false;
                        if (isLiveStop) {
                            isLiveStop = false;
                            return null;
                        }
                    } catch (InterruptedException e) {
                        isLiveWaiting = false;
                        return null;
                    }
                }
                if (isPlaylistClosed) {
                    return null;
                }
            }

            synchronized (lock) {
                mediaFileIndex++;
                if (mediaFileIndex < mediaFiles.size()) {
                    if (baseURI != null) {
                        return baseURI + mediaFiles.get(mediaFileIndex);
                    } else {
                        return mediaFiles.get(mediaFileIndex);
                    }
                } else {
                    return null;
                }
            }
        }

        private String getHeaderFile() {
            synchronized (lock) {
                if (mediaFiles.size() > 0) {
                    if (baseURI != null) {
                        return baseURI + mediaFiles.get(0);
                    } else {
                        return mediaFiles.get(0);
                    }
                } else {
                    return null;
                }
            }
        }

        private double getMediaFileStartTime() {
            if (mediaFileIndex < mediaFiles.size()) {
                return mediaFilesStartTimes.get(mediaFileIndex);
            }

            return 0.0;
        }

        private double getDuration() {
            return duration;
        }

        private void setForceDiscontinuity(boolean value) {
            forceDiscontinuity = value;
        }

        private boolean isCurrentMediaFileDiscontinuity() {
            if (forceDiscontinuity) {
                forceDiscontinuity = false;
                return true;
            } else {
                return mediaFilesDiscontinuities.get(mediaFileIndex);
            }
        }

        private double seek(long time) {
            synchronized (lock) {
                if (isLive) {
                    if (time == 0) {
                        if (isFragmentedMP4()) {
                            mediaFileIndex = 0; // Skip header at 0 index
                            // we will send it with first segment if needed.
                        } else {
                            mediaFileIndex = -1;
                        }
                        if (isLiveWaiting) {
                            isLiveStop = true;
                            liveSemaphore.release();
                        }
                        return 0;
                    }
                } else {
                    time += targetDuration / 2000;

                    int mediaFileStartTimeSize = mediaFilesStartTimes.size();

                    for (int index = 0; index < mediaFileStartTimeSize; index++) {
                        if (time >= mediaFilesStartTimes.get(index)) {
                            if (index + 1 < mediaFileStartTimeSize) {
                                if (time < mediaFilesStartTimes.get(index + 1)) {
                                    if (isFragmentedMP4()) {
                                        mediaFileIndex = index;
                                    } else {
                                        mediaFileIndex = index - 1; // Seek will load segment and increment mediaFileIndex
                                    }
                                    return mediaFilesStartTimes.get(index);
                                }
                            } else {
                                if ((time - targetDuration / 2000) < duration) {
                                    if (isFragmentedMP4()) {
                                        mediaFileIndex = index;
                                    } else {
                                        mediaFileIndex = index - 1; // Seek will load segment and increment mediaFileIndex
                                    }
                                    return mediaFilesStartTimes.get(index);
                                } else if (Double.compare(time - targetDuration / 2000, duration) == 0) {
                                    return duration;
                                }
                            }
                        }
                    }
                }
            }

            return -1;
        }

        private int getMimeType() {
            synchronized (lock) {
                if (mimeType == HLS_VALUE_MIMETYPE_UNKNOWN) {
                    if (mediaFiles.size() > 0) {
                        if (stripParameters(mediaFiles.get(0)).endsWith(".ts")) {
                            mimeType = HLS_VALUE_MIMETYPE_MP2T;
                        } else if (stripParameters(mediaFiles.get(0)).endsWith(".mp3")) {
                            mimeType = HLS_VALUE_MIMETYPE_MP3;
                        } else if (stripParameters(mediaFiles.get(0)).endsWith(".mp4")
                                || stripParameters(mediaFiles.get(0)).endsWith(".m4s")) {
                            mimeType = HLS_VALUE_MIMETYPE_FMP4;
                        } else if (stripParameters(mediaFiles.get(0)).endsWith(".aac")) {
                            mimeType = HLS_VALUE_MIMETYPE_AAC;
                        }
                    }
                }
            }

            return mimeType;
        }

        private String getMediaFileExtension() {
            synchronized (lock) {
                if (mediaFiles.size() > 0) {
                    String mediaFile = stripParameters(mediaFiles.get(0));
                    int index = mediaFile.lastIndexOf(".");
                    if (index != -1) {
                        return mediaFile.substring(index);
                    }
                }
            }

            return null;
        }

        private boolean setSequenceNumber(int value) {
            if (sequenceNumberStart == -1) {
                sequenceNumberStart = value;
            } else if (sequenceNumber != value) {
                sequenceNumberUpdated = true;
                sequenceNumber = value;
            } else {
                return false;
            }

            return true;
        }

        private void close() {
            if (isLive) {
                isPlaylistClosed = true;
                liveSemaphore.release();
            }
        }

        private void setBaseURI(String playlistURI, String URI) {
            if (!URI.startsWith("http://") && !URI.startsWith("https://")) {
                baseURI = playlistURI.substring(0, playlistURI.lastIndexOf("/") + 1);
            }
            needBaseURI = false;
        }
    }
}
</code></pre>    <br/>
    <br/>
    <!--<div id="right-banner">-->
            <!--</div>-->
    <!--<div id="left-banner">-->
            <!--</div>-->
<div class='clear'></div>
</main>
</div>
<br/><br/>
    <div class="align-center">© 2015 - 2024 <a href="/legal-notice.php">Weber Informatics LLC</a> | <a href="/data-protection.php">Privacy Policy</a></div>
<br/><br/><br/><br/><br/><br/>
</body>
</html>