diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/AudioBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/AudioBroadcaster.java index 2c48963f..207c277d 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/AudioBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/AudioBroadcaster.java @@ -1,21 +1,24 @@ -/******************************************************************************* - * sdrtrunk - * Copyright (C) 2014-2017 Dennis Sheirer +/* * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. + * * ****************************************************************************** + * * Copyright (C) 2014-2019 Dennis Sheirer + * * + * * This program is free software: you can redistribute it and/or modify + * * it under the terms of the GNU General Public License as published by + * * the Free Software Foundation, either version 3 of the License, or + * * (at your option) any later version. + * * + * * This program 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 for more details. + * * + * * You should have received a copy of the GNU General Public License + * * along with this program. If not, see + * * ***************************************************************************** * - * This program 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 for more details. * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - * - ******************************************************************************/ + */ package io.github.dsheirer.audio.broadcast; import io.github.dsheirer.audio.convert.ISilenceGenerator; @@ -373,9 +376,9 @@ public abstract class AudioBroadcaster implements Listener broadcastAudio(mSilenceGenerator.generate(PROCESSOR_RUN_INTERVAL_MS)); } } - catch(Exception e) + catch(Throwable t) { - mLog.error("Error while processing audio streaming queue", e); + mLog.error("Error while processing audio streaming queue", t); } mProcessing.set(false); diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java index ee6a199c..8b53e94c 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java @@ -1,21 +1,23 @@ /* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2018 Dennis Sheirer * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. + * * ****************************************************************************** + * * Copyright (C) 2014-2019 Dennis Sheirer + * * + * * This program is free software: you can redistribute it and/or modify + * * it under the terms of the GNU General Public License as published by + * * the Free Software Foundation, either version 3 of the License, or + * * (at your option) any later version. + * * + * * This program 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 for more details. + * * + * * You should have received a copy of the GNU General Public License + * * along with this program. If not, see + * * ***************************************************************************** * - * This program 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 for more details. * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - * ***************************************************************************** */ package io.github.dsheirer.audio.broadcast.icecast; @@ -31,43 +33,26 @@ import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.Role; import io.github.dsheirer.properties.SystemProperties; import io.github.dsheirer.util.ThreadPool; -import org.apache.mina.core.RuntimeIoException; -import org.apache.mina.core.future.ConnectFuture; -import org.apache.mina.core.service.IoHandlerAdapter; -import org.apache.mina.core.session.IoSession; -import org.apache.mina.http.HttpClientCodec; -import org.apache.mina.http.HttpRequestImpl; -import org.apache.mina.http.api.HttpMethod; -import org.apache.mina.http.api.HttpRequest; -import org.apache.mina.http.api.HttpVersion; -import org.apache.mina.transport.socket.nio.NioSocketConnector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.net.InetSocketAddress; +import java.net.URI; import java.net.URLEncoder; -import java.nio.channels.UnresolvedAddressException; -import java.util.HashMap; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.concurrent.LinkedTransferQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdater { private final static Logger mLog = LoggerFactory.getLogger(IcecastBroadcastMetadataUpdater.class); private final static String UTF8 = "UTF-8"; - + private HttpClient mHttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); private IcecastConfiguration mIcecastConfiguration; private AliasModel mAliasModel; - private NioSocketConnector mSocketConnector; - private AtomicBoolean mUpdating = new AtomicBoolean(); - private Queue mMetadataQueue = new LinkedTransferQueue<>(); - private Map mHTTPHeaders; - private boolean mStackTraceLoggingSuppressed; + private boolean mConnectionLoggingSuppressed = false; /** * Icecast song metadata updater. Each metadata update is processed in the order received and the dispatch of @@ -81,41 +66,6 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate mAliasModel = aliasModel; } - /** - * Socket connector - lazy constructor. - */ - private NioSocketConnector getSocketConnector() - { - if(mSocketConnector == null) - { - mSocketConnector = new NioSocketConnector(); -// mSocketConnector.getFilterChain().addLast("logger", new LoggingFilter(IcecastBroadcastMetadataUpdater.class)); - mSocketConnector.getFilterChain().addLast("http_client_codec", new HttpClientCodec()); - - //Each metadata update session is single-use, so we'll shut it down upon success or failure - mSocketConnector.setHandler(new IoHandlerAdapter() - { - @Override - public void exceptionCaught(IoSession session, Throwable cause) throws Exception - { - //Single-use session - close it after we receive an error - session.closeNow(); - - mLog.error("Metadata update failed"); - } - - @Override - public void messageReceived(IoSession session, Object message) throws Exception - { - //Single-use session - close it after we receive a response - session.closeNow(); - } - }); - } - - return mSocketConnector; - } - /** * Sends a song metadata update to the remote server. Additional metadata updates received while the updater * is in the process of sending an existing metadata update will be queued and processed in received order. @@ -126,66 +76,87 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate */ public void update(IdentifierCollection identifierCollection) { - mMetadataQueue.offer(getSong(identifierCollection)); + StringBuilder sb = new StringBuilder(); - if(!mUpdating.get() && !mMetadataQueue.isEmpty()) + try { - Runnable runnable = new Runnable() + sb.append("http://"); + sb.append(mIcecastConfiguration.getHost()); + sb.append(":"); + sb.append(mIcecastConfiguration.getPort()); + sb.append("/admin/metadata?mode=updinfo&mount="); + sb.append(URLEncoder.encode(mIcecastConfiguration.getMountPoint(), UTF8)); + sb.append("&charset=UTF%2d8"); + sb.append("&song=").append(URLEncoder.encode(getSong(identifierCollection), UTF8)); + } + catch(UnsupportedEncodingException uee) + { + mLog.error("Error encoding metadata information to UTF-8", uee); + sb = null; + } + + if(sb != null) + { + final String metadataUpdateURL = sb.toString(); + URI uri = URI.create(metadataUpdateURL); + + ThreadPool.SCHEDULED.submit(new Runnable() { @Override public void run() { - if(!mMetadataQueue.isEmpty() && mUpdating.compareAndSet(false, true)) + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(uri) + .header(IcecastHeader.AUTHORIZATION.getValue(), mIcecastConfiguration.getBase64EncodedCredentials()) + .header(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName()) + .GET() + .build(); + + HttpResponse response = null; + try { - ConnectFuture connectFuture = getSocketConnector() - .connect(new InetSocketAddress(mIcecastConfiguration.getHost(), - mIcecastConfiguration.getPort())); - connectFuture.awaitUninterruptibly(); - IoSession session = connectFuture.getSession(); - - if(session != null) + response = mHttpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } + catch(IOException ioe) + { + if(!mConnectionLoggingSuppressed) { - String song = mMetadataQueue.poll(); + mLog.error("IO Error submitting Icecast metadata update [" + + (metadataUpdateURL != null ? metadataUpdateURL : "no url"), ioe); + mConnectionLoggingSuppressed = true; + } + } + catch(InterruptedException ie) + { + mLog.error("Interrupted Exception Error", ie); + } - while(song != null) + if(response != null) + { + if(response.statusCode() == 200) + { + mConnectionLoggingSuppressed = false; + } + else + { + if(!mConnectionLoggingSuppressed) { - HttpRequest updateRequest = createUpdateRequest(song); - - if(updateRequest != null) - { - session.write(updateRequest); - } - - //Fetch next metadata update to send - song = mMetadataQueue.poll(); + mLog.info("Error submitting Icecast 2 Metadata update to URL [" + metadataUpdateURL + + "] HTTP Response Code [" + response.statusCode() + "] Body [" + response.body() + "]"); + mConnectionLoggingSuppressed = true; } - - session.closeNow(); } } - catch(UnresolvedAddressException | RuntimeIoException ure) - { - //Do nothing - the server is temporarily unavailable - } - catch(Exception e) - { - Throwable throwableCause = e.getCause(); - - if(!mStackTraceLoggingSuppressed) - { - mLog.error("Error sending metadata update. Future errors will be suppressed", e); - mStackTraceLoggingSuppressed = true; - } - } - - mUpdating.set(false); + } + catch(Throwable t) + { + mLog.error("There was an error submitting an Icecast metadata update", t); } } - }; - - ThreadPool.SCHEDULED.schedule(runnable, 0l, TimeUnit.SECONDS); + }); } } @@ -213,7 +184,7 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate List aliases = aliasList.getAliases(to); - if(!aliases.isEmpty()) + if(aliases != null && !aliases.isEmpty()) { sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases)); } @@ -231,7 +202,7 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate List aliases = aliasList.getAliases(from); - if(!aliases.isEmpty()) + if(aliases != null && !aliases.isEmpty()) { sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases)); } @@ -248,46 +219,4 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate return sb.toString(); } - - /** - * Creates an HTTP GET request to update the metadata on the remote server - */ - private HttpRequest createUpdateRequest(String song) - { - try - { - StringBuilder sb = new StringBuilder(); - sb.append("mode=updinfo"); - sb.append("&mount=").append(URLEncoder.encode(mIcecastConfiguration.getMountPoint(), UTF8)); - sb.append("&charset=UTF%2d8"); - sb.append("&song=").append(URLEncoder.encode(song, UTF8)); - - HttpRequestImpl request = new HttpRequestImpl(HttpVersion.HTTP_1_1, HttpMethod.GET, "/admin/metadata", - sb.toString(), getHTTPHeaders()); - - return request; - } - catch(UnsupportedEncodingException e) - { - //This should never happen - mLog.error("UTF-8 encoding is not supported - can't update song metadata"); - } - - return null; - } - - /** - * HTTP headers to use for a metadata update request. - */ - private Map getHTTPHeaders() - { - if(mHTTPHeaders == null) - { - mHTTPHeaders = new HashMap<>(); - mHTTPHeaders.put(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName()); - mHTTPHeaders.put(IcecastHeader.AUTHORIZATION.getValue(), mIcecastConfiguration.getBase64EncodedCredentials()); - } - - return mHTTPHeaders; - } } diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v2/ShoutcastV2AudioBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v2/ShoutcastV2AudioBroadcaster.java index b5e92a81..35768fe2 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v2/ShoutcastV2AudioBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v2/ShoutcastV2AudioBroadcaster.java @@ -1,21 +1,23 @@ /* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2018 Dennis Sheirer * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. + * * ****************************************************************************** + * * Copyright (C) 2014-2019 Dennis Sheirer + * * + * * This program is free software: you can redistribute it and/or modify + * * it under the terms of the GNU General Public License as published by + * * the Free Software Foundation, either version 3 of the License, or + * * (at your option) any later version. + * * + * * This program 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 for more details. + * * + * * You should have received a copy of the GNU General Public License + * * along with this program. If not, see + * * ***************************************************************************** * - * This program 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 for more details. * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - * ***************************************************************************** */ package io.github.dsheirer.audio.broadcast.shoutcast.v2; @@ -278,6 +280,8 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBr if(identifierCollection != null) { + StringBuilder sbTitle2 = new StringBuilder(); + AliasList aliasList = mAliasModel.getAliasList(identifierCollection); Identifier to = identifierCollection.getIdentifier(IdentifierClass.USER, Form.PATCH_GROUP, Role.TO); @@ -289,37 +293,39 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBr if(to != null) { - sb.append("TO:").append(to); + sbTitle2.append("TO:").append(to); List aliases = aliasList.getAliases(to); if(!aliases.isEmpty()) { - sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases)); + sbTitle2.append(" ").append(Joiner.on(", ").skipNulls().join(aliases)); } } else { - sb.append("TO:UNKNOWN"); + sbTitle2.append("TO:UNKNOWN"); } Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.FROM); if(from != null) { - sb.append(" FROM:").append(from); + sbTitle2.append(" FROM:").append(from); List aliases = aliasList.getAliases(from); if(!aliases.isEmpty()) { - sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases)); + sbTitle2.append(" ").append(Joiner.on(", ").skipNulls().join(aliases)); } } else { - sb.append(" FROM:UNKNOWN"); + sbTitle2.append(" FROM:UNKNOWN"); } + + sb.append(UltravoxMetadata.TITLE_2.asXML(sbTitle2.toString())); } else {