diff options
Diffstat (limited to 'xbmc/cores/paplayer')
-rw-r--r-- | xbmc/cores/paplayer/AudioDecoder.cpp | 401 | ||||
-rw-r--r-- | xbmc/cores/paplayer/AudioDecoder.h | 91 | ||||
-rw-r--r-- | xbmc/cores/paplayer/CMakeLists.txt | 13 | ||||
-rw-r--r-- | xbmc/cores/paplayer/CachingCodec.h | 18 | ||||
-rw-r--r-- | xbmc/cores/paplayer/CodecFactory.cpp | 112 | ||||
-rw-r--r-- | xbmc/cores/paplayer/CodecFactory.h | 23 | ||||
-rw-r--r-- | xbmc/cores/paplayer/ICodec.h | 82 | ||||
-rw-r--r-- | xbmc/cores/paplayer/PAPlayer.cpp | 1207 | ||||
-rw-r--r-- | xbmc/cores/paplayer/PAPlayer.h | 154 | ||||
-rw-r--r-- | xbmc/cores/paplayer/VideoPlayerCodec.cpp | 533 | ||||
-rw-r--r-- | xbmc/cores/paplayer/VideoPlayerCodec.h | 65 |
11 files changed, 2699 insertions, 0 deletions
diff --git a/xbmc/cores/paplayer/AudioDecoder.cpp b/xbmc/cores/paplayer/AudioDecoder.cpp new file mode 100644 index 0000000..03dc907 --- /dev/null +++ b/xbmc/cores/paplayer/AudioDecoder.cpp @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "AudioDecoder.h" + +#include "CodecFactory.h" +#include "FileItem.h" +#include "ICodec.h" +#include "ServiceBroker.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationVolumeHandling.h" +#include "music/tags/MusicInfoTag.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/log.h" + +#include <cmath> +#include <mutex> + +CAudioDecoder::CAudioDecoder() +{ + m_codec = NULL; + m_rawBuffer = nullptr; + + m_eof = false; + + m_status = STATUS_NO_FILE; + m_canPlay = false; + + // output buffer (for transferring data from the Pcm Buffer to the rest of the audio chain) + memset(&m_outputBuffer, 0, OUTPUT_SAMPLES * sizeof(float)); + memset(&m_pcmInputBuffer, 0, INPUT_SIZE * sizeof(unsigned char)); + memset(&m_inputBuffer, 0, INPUT_SAMPLES * sizeof(float)); + + m_rawBufferSize = 0; +} + +CAudioDecoder::~CAudioDecoder() +{ + Destroy(); +} + +void CAudioDecoder::Destroy() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_status = STATUS_NO_FILE; + + m_pcmBuffer.Destroy(); + + if ( m_codec ) + delete m_codec; + m_codec = NULL; + + m_canPlay = false; +} + +bool CAudioDecoder::Create(const CFileItem &file, int64_t seekOffset) +{ + Destroy(); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + // reset our playback timing variables + m_eof = false; + + // get correct cache size + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + unsigned int filecache = settings->GetInt(CSettings::SETTING_CACHEAUDIO_INTERNET); + if ( file.IsHD() ) + filecache = settings->GetInt(CSettings::SETTING_CACHE_HARDDISK); + else if ( file.IsOnDVD() ) + filecache = settings->GetInt(CSettings::SETTING_CACHEAUDIO_DVDROM); + else if ( file.IsOnLAN() ) + filecache = settings->GetInt(CSettings::SETTING_CACHEAUDIO_LAN); + + // create our codec + m_codec=CodecFactory::CreateCodecDemux(file, filecache * 1024); + + if (!m_codec || !m_codec->Init(file, filecache * 1024)) + { + CLog::Log(LOGERROR, "CAudioDecoder: Unable to Init Codec while loading file {}", + file.GetDynPath()); + Destroy(); + return false; + } + unsigned int blockSize = (m_codec->m_bitsPerSample >> 3) * m_codec->m_format.m_channelLayout.Count(); + + if (blockSize == 0) + { + CLog::Log(LOGERROR, "CAudioDecoder: Codec provided invalid parameters ({}-bit, {} channels)", + m_codec->m_bitsPerSample, GetFormat().m_channelLayout.Count()); + return false; + } + + /* allocate the pcmBuffer for 2 seconds of audio */ + m_pcmBuffer.Create(2 * blockSize * m_codec->m_format.m_sampleRate); + + if (file.HasMusicInfoTag()) + { + // set total time from the given tag + if (file.GetMusicInfoTag()->GetDuration()) + m_codec->SetTotalTime(file.GetMusicInfoTag()->GetDuration()); + + // update ReplayGain from the given tag if it's better then original (cuesheet) + ReplayGain rgInfo = m_codec->m_tag.GetReplayGain(); + bool anySet = false; + if (!rgInfo.Get(ReplayGain::ALBUM).Valid() + && file.GetMusicInfoTag()->GetReplayGain().Get(ReplayGain::ALBUM).Valid()) + { + rgInfo.Set(ReplayGain::ALBUM, file.GetMusicInfoTag()->GetReplayGain().Get(ReplayGain::ALBUM)); + anySet = true; + } + if (!rgInfo.Get(ReplayGain::TRACK).Valid() + && file.GetMusicInfoTag()->GetReplayGain().Get(ReplayGain::TRACK).Valid()) + { + rgInfo.Set(ReplayGain::TRACK, file.GetMusicInfoTag()->GetReplayGain().Get(ReplayGain::TRACK)); + anySet = true; + } + if (anySet) + m_codec->m_tag.SetReplayGain(rgInfo); + } + + if (seekOffset) + m_codec->Seek(seekOffset); + + m_status = STATUS_QUEUING; + + m_rawBufferSize = 0; + + return true; +} + +AEAudioFormat CAudioDecoder::GetFormat() +{ + AEAudioFormat format; + if (!m_codec) + return format; + return m_codec->m_format; +} + +unsigned int CAudioDecoder::GetChannels() +{ + return GetFormat().m_channelLayout.Count(); +} + +int64_t CAudioDecoder::Seek(int64_t time) +{ + m_pcmBuffer.Clear(); + m_rawBufferSize = 0; + if (!m_codec) + return 0; + if (time < 0) time = 0; + if (time > m_codec->m_TotalTime) time = m_codec->m_TotalTime; + return m_codec->Seek(time); +} + +void CAudioDecoder::SetTotalTime(int64_t time) +{ + if (m_codec) + m_codec->m_TotalTime = time; +} + +int64_t CAudioDecoder::TotalTime() +{ + if (m_codec) + return m_codec->m_TotalTime; + return 0; +} + +unsigned int CAudioDecoder::GetDataSize(bool checkPktSize) +{ + if (m_status == STATUS_QUEUING || m_status == STATUS_NO_FILE) + return 0; + + if (m_codec->m_format.m_dataFormat != AE_FMT_RAW) + { + // check for end of file and end of buffer + if (m_status == STATUS_ENDING) + { + if (m_pcmBuffer.getMaxReadSize() == 0) + m_status = STATUS_ENDED; + else if (checkPktSize && m_pcmBuffer.getMaxReadSize() < PACKET_SIZE) + m_status = STATUS_ENDED; + } + return std::min(m_pcmBuffer.getMaxReadSize() / (m_codec->m_bitsPerSample >> 3), (unsigned int)OUTPUT_SAMPLES); + } + else + { + if (m_status == STATUS_ENDING) + m_status = STATUS_ENDED; + return m_rawBufferSize; + } +} + +void *CAudioDecoder::GetData(unsigned int samples) +{ + unsigned int size = samples * (m_codec->m_bitsPerSample >> 3); + if (size > sizeof(m_outputBuffer)) + { + CLog::Log(LOGERROR, "CAudioDecoder::GetData - More data was requested then we have space to buffer!"); + return NULL; + } + + if (size > m_pcmBuffer.getMaxReadSize()) + { + CLog::Log( + LOGWARNING, + "CAudioDecoder::GetData() more bytes/samples ({}) requested than we have to give ({})!", + size, m_pcmBuffer.getMaxReadSize()); + size = m_pcmBuffer.getMaxReadSize(); + } + + if (m_pcmBuffer.ReadData((char *)m_outputBuffer, size)) + { + if (m_status == STATUS_ENDING && m_pcmBuffer.getMaxReadSize() == 0) + m_status = STATUS_ENDED; + + return m_outputBuffer; + } + + CLog::Log(LOGERROR, "CAudioDecoder::GetData() ReadBinary failed with {} samples", samples); + return NULL; +} + +uint8_t *CAudioDecoder::GetRawData(int &size) +{ + if (m_status == STATUS_ENDING) + m_status = STATUS_ENDED; + + if (m_rawBufferSize) + { + size = m_rawBufferSize; + m_rawBufferSize = 0; + return m_rawBuffer; + } + return nullptr; +} + +int CAudioDecoder::ReadSamples(int numsamples) +{ + if (m_status == STATUS_NO_FILE || m_status == STATUS_ENDING || m_status == STATUS_ENDED) + return RET_SLEEP; // nothing loaded yet + + // start playing once we're fully queued and we're ready to go + if (m_status == STATUS_QUEUED && m_canPlay) + m_status = STATUS_PLAYING; + + // grab a lock to ensure the codec is created at this point. + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_codec->m_format.m_dataFormat != AE_FMT_RAW) + { + // Read in more data + int maxsize = std::min<int>(INPUT_SAMPLES, m_pcmBuffer.getMaxWriteSize() / (m_codec->m_bitsPerSample >> 3)); + numsamples = std::min<int>(numsamples, maxsize); + numsamples -= (numsamples % GetFormat().m_channelLayout.Count()); // make sure it's divisible by our number of channels + if (numsamples) + { + size_t readSize = 0; + int result = m_codec->ReadPCM( + m_pcmInputBuffer, static_cast<size_t>(numsamples * (m_codec->m_bitsPerSample >> 3)), + &readSize); + + if (result != READ_ERROR && readSize) + { + // move it into our buffer + m_pcmBuffer.WriteData((char *)m_pcmInputBuffer, readSize); + + // update status + if (m_status == STATUS_QUEUING && m_pcmBuffer.getMaxReadSize() > m_pcmBuffer.getSize() * 0.9) + { + CLog::Log(LOGINFO, "AudioDecoder: File is queued"); + m_status = STATUS_QUEUED; + } + + if (result == READ_EOF) // EOF reached + { + // setup ending if we're within set time of the end (currently just EOF) + m_eof = true; + if (m_status < STATUS_ENDING) + m_status = STATUS_ENDING; + } + + return RET_SUCCESS; + } + if (result == READ_ERROR) + { + // error decoding, lets finish up and get out + CLog::Log(LOGERROR, "CAudioDecoder: Error while decoding {}", result); + return RET_ERROR; + } + if (result == READ_EOF) + { + m_eof = true; + // setup ending if we're within set time of the end (currently just EOF) + if (m_status < STATUS_ENDING) + m_status = STATUS_ENDING; + } + } + } + else + { + if (m_rawBufferSize == 0) + { + int result = m_codec->ReadRaw(&m_rawBuffer, &m_rawBufferSize); + if (result == READ_SUCCESS && m_rawBufferSize) + { + //! @todo trash this useless ringbuffer + if (m_status == STATUS_QUEUING) + { + m_status = STATUS_QUEUED; + } + return RET_SUCCESS; + } + else if (result == READ_ERROR) + { + // error decoding, lets finish up and get out + CLog::Log(LOGERROR, "CAudioDecoder: Error while decoding {}", result); + return RET_ERROR; + } + else if (result == READ_EOF) + { + m_eof = true; + // setup ending if we're within set time of the end (currently just EOF) + if (m_status < STATUS_ENDING) + m_status = STATUS_ENDING; + } + } + } + return RET_SLEEP; // nothing to do +} + +bool CAudioDecoder::CanSeek() +{ + if (m_codec) + return m_codec->CanSeek(); + else + return false; +} + +float CAudioDecoder::GetReplayGain(float &peakVal) +{ +#define REPLAY_GAIN_DEFAULT_LEVEL 89.0f + auto& components = CServiceBroker::GetAppComponents(); + const auto appVolume = components.GetComponent<CApplicationVolumeHandling>(); + + const auto& replayGainSettings = appVolume->GetReplayGainSettings(); + if (replayGainSettings.iType == ReplayGain::NONE) + return 1.0f; + + // Compute amount of gain + float replaydB = (float)replayGainSettings.iNoGainPreAmp; + float peak = 1.0f; + const ReplayGain& rgInfo = m_codec->m_tag.GetReplayGain(); + if (replayGainSettings.iType == ReplayGain::ALBUM) + { + if (rgInfo.Get(ReplayGain::ALBUM).HasGain()) + { + replaydB = (float)replayGainSettings.iPreAmp + rgInfo.Get(ReplayGain::ALBUM).Gain(); + if (rgInfo.Get(ReplayGain::ALBUM).HasPeak()) + peak = rgInfo.Get(ReplayGain::ALBUM).Peak(); + } + else if (rgInfo.Get(ReplayGain::TRACK).HasGain()) + { + replaydB = (float)replayGainSettings.iPreAmp + rgInfo.Get(ReplayGain::TRACK).Gain(); + if (rgInfo.Get(ReplayGain::TRACK).HasPeak()) + peak = rgInfo.Get(ReplayGain::TRACK).Peak(); + } + } + else if (replayGainSettings.iType == ReplayGain::TRACK) + { + if (rgInfo.Get(ReplayGain::TRACK).HasGain()) + { + replaydB = (float)replayGainSettings.iPreAmp + rgInfo.Get(ReplayGain::TRACK).Gain(); + if (rgInfo.Get(ReplayGain::TRACK).HasPeak()) + peak = rgInfo.Get(ReplayGain::TRACK).Peak(); + } + else if (rgInfo.Get(ReplayGain::ALBUM).HasGain()) + { + replaydB = (float)replayGainSettings.iPreAmp + rgInfo.Get(ReplayGain::ALBUM).Gain(); + if (rgInfo.Get(ReplayGain::ALBUM).HasPeak()) + peak = rgInfo.Get(ReplayGain::ALBUM).Peak(); + } + } + // convert to a gain type + float replaygain = std::pow(10.0f, (replaydB - REPLAY_GAIN_DEFAULT_LEVEL) * 0.05f); + + CLog::Log(LOGDEBUG, + "AudioDecoder::GetReplayGain - Final Replaygain applied: {:f}, Track/Album Gain {:f}, " + "Peak {:f}", + replaygain, replaydB, peak); + + peakVal = peak; + return replaygain; +} + diff --git a/xbmc/cores/paplayer/AudioDecoder.h b/xbmc/cores/paplayer/AudioDecoder.h new file mode 100644 index 0000000..9d0eac6 --- /dev/null +++ b/xbmc/cores/paplayer/AudioDecoder.h @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" +#include "utils/RingBuffer.h" + +struct AEAudioFormat; +class CFileItem; +class ICodec; + +#define PACKET_SIZE 3840 // audio packet size - we keep 1 in reserve for gapless playback + // using a multiple of 1, 2, 3, 4, 5, 6 to guarantee track alignment + // note that 7 or higher channels won't work too well. + +#define INPUT_SIZE PACKET_SIZE * 3 // input data size we read from the codecs at a time + // * 3 to allow 24 bit audio + +#define OUTPUT_SAMPLES PACKET_SIZE // max number of output samples +#define INPUT_SAMPLES PACKET_SIZE // number of input samples (distributed over channels) + +#define STATUS_NO_FILE 0 +#define STATUS_QUEUING 1 +#define STATUS_QUEUED 2 +#define STATUS_PLAYING 3 +#define STATUS_ENDING 4 +#define STATUS_ENDED 5 + +// return codes from decoders +#define RET_ERROR -1 +#define RET_SUCCESS 0 +#define RET_SLEEP 1 + +class CAudioDecoder +{ +public: + CAudioDecoder(); + ~CAudioDecoder(); + + bool Create(const CFileItem &file, int64_t seekOffset); + void Destroy(); + + int ReadSamples(int numsamples); + + bool CanSeek(); + int64_t Seek(int64_t time); + int64_t TotalTime(); + void SetTotalTime(int64_t time); + void Start() { m_canPlay = true;}; // cause a pre-buffered stream to start. + int GetStatus() { return m_status; } + void SetStatus(int status) { m_status = status; } + + AEAudioFormat GetFormat(); + unsigned int GetChannels(); + // Data management + unsigned int GetDataSize(bool checkPktSize); + void *GetData(unsigned int samples); + uint8_t* GetRawData(int &size); + ICodec *GetCodec() const { return m_codec; } + float GetReplayGain(float &peakVal); + +private: + // pcm buffer + CRingBuffer m_pcmBuffer; + + // output buffer (for transferring data from the Pcm Buffer to the rest of the audio chain) + float m_outputBuffer[OUTPUT_SAMPLES]; + + // input buffer (for transferring data from the Codecs to our Pcm Ringbuffer + uint8_t m_pcmInputBuffer[INPUT_SIZE]; + float m_inputBuffer[INPUT_SAMPLES]; + + uint8_t *m_rawBuffer; + int m_rawBufferSize; + + // status + bool m_eof; + int m_status; + bool m_canPlay; + + // the codec we're using + ICodec* m_codec; + + CCriticalSection m_critSection; +}; diff --git a/xbmc/cores/paplayer/CMakeLists.txt b/xbmc/cores/paplayer/CMakeLists.txt new file mode 100644 index 0000000..8da2e7c --- /dev/null +++ b/xbmc/cores/paplayer/CMakeLists.txt @@ -0,0 +1,13 @@ +set(SOURCES AudioDecoder.cpp + CodecFactory.cpp + PAPlayer.cpp + VideoPlayerCodec.cpp) + +set(HEADERS AudioDecoder.h + CachingCodec.h + CodecFactory.h + ICodec.h + PAPlayer.h + VideoPlayerCodec.h) + +core_add_library(paplayer) diff --git a/xbmc/cores/paplayer/CachingCodec.h b/xbmc/cores/paplayer/CachingCodec.h new file mode 100644 index 0000000..55ac36b --- /dev/null +++ b/xbmc/cores/paplayer/CachingCodec.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "ICodec.h" + +class CachingCodec : public ICodec +{ +public: + virtual ~CachingCodec() {} + virtual int GetCacheLevel() const { return -1; } +}; diff --git a/xbmc/cores/paplayer/CodecFactory.cpp b/xbmc/cores/paplayer/CodecFactory.cpp new file mode 100644 index 0000000..70684d8 --- /dev/null +++ b/xbmc/cores/paplayer/CodecFactory.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "CodecFactory.h" + +#include "ServiceBroker.h" +#include "URL.h" +#include "VideoPlayerCodec.h" +#include "addons/AudioDecoder.h" +#include "addons/ExtsMimeSupportList.h" +#include "addons/addoninfo/AddonType.h" +#include "utils/StringUtils.h" + +using namespace KODI::ADDONS; + +ICodec* CodecFactory::CreateCodec(const CURL& urlFile) +{ + std::string fileType = urlFile.GetFileType(); + StringUtils::ToLower(fileType); + + auto addonInfos = CServiceBroker::GetExtsMimeSupportList().GetExtensionSupportedAddonInfos( + "." + fileType, CExtsMimeSupportList::FilterSelect::all); + for (const auto& addonInfo : addonInfos) + { + // Check asked and given extension is supported by only for here allowed audiodecoder addons. + if (addonInfo.first == ADDON::AddonType::AUDIODECODER) + { + std::unique_ptr<CAudioDecoder> result = std::make_unique<CAudioDecoder>(addonInfo.second); + if (!result->CreateDecoder()) + continue; + + return result.release(); + } + } + + VideoPlayerCodec *dvdcodec = new VideoPlayerCodec(); + return dvdcodec; +} + +ICodec* CodecFactory::CreateCodecDemux(const CFileItem& file, unsigned int filecache) +{ + CURL urlFile(file.GetDynPath()); + std::string content = file.GetMimeType(); + StringUtils::ToLower(content); + if (!content.empty()) + { + auto addonInfos = CServiceBroker::GetExtsMimeSupportList().GetMimetypeSupportedAddonInfos( + content, CExtsMimeSupportList::FilterSelect::all); + for (const auto& addonInfo : addonInfos) + { + // Check asked and given mime type is supported by only for here allowed audiodecoder addons. + if (addonInfo.first == ADDON::AddonType::AUDIODECODER) + { + std::unique_ptr<CAudioDecoder> result = std::make_unique<CAudioDecoder>(addonInfo.second); + if (!result->CreateDecoder() && result->SupportsFile(file.GetPath())) + continue; + + return result.release(); + } + } + } + + if( content == "audio/mpeg" || + content == "audio/mpeg3" || + content == "audio/mp3" || + content == "audio/aac" || + content == "audio/aacp" || + content == "audio/x-ms-wma" || + content == "audio/x-ape" || + content == "audio/ape" || + content == "application/ogg" || + content == "audio/ogg" || + content == "audio/x-xbmc-pcm" || + content == "audio/flac" || + content == "audio/x-flac" || + content == "application/x-flac" + ) + { + VideoPlayerCodec *dvdcodec = new VideoPlayerCodec(); + dvdcodec->SetContentType(content); + return dvdcodec; + } + else if (urlFile.IsProtocol("shout")) + { + VideoPlayerCodec *dvdcodec = new VideoPlayerCodec(); + dvdcodec->SetContentType("audio/mp3"); + return dvdcodec; // if we got this far with internet radio - content-type was wrong. gamble on mp3. + } + else if (urlFile.IsFileType("wav") || + content == "audio/wav" || + content == "audio/x-wav") + { + VideoPlayerCodec *dvdcodec = new VideoPlayerCodec(); + dvdcodec->SetContentType("audio/x-spdif-compressed"); + if (dvdcodec->Init(file, filecache)) + { + return dvdcodec; + } + + dvdcodec = new VideoPlayerCodec(); + dvdcodec->SetContentType(content); + return dvdcodec; + } + else + return CreateCodec(urlFile); +} + diff --git a/xbmc/cores/paplayer/CodecFactory.h b/xbmc/cores/paplayer/CodecFactory.h new file mode 100644 index 0000000..01a2614 --- /dev/null +++ b/xbmc/cores/paplayer/CodecFactory.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "ICodec.h" + +class CFileItem; + +class CodecFactory +{ +public: + CodecFactory() = default; + virtual ~CodecFactory() = default; + static ICodec* CreateCodec(const CURL& urlFile); + static ICodec* CreateCodecDemux(const CFileItem& file, unsigned int filecache); +}; + diff --git a/xbmc/cores/paplayer/ICodec.h b/xbmc/cores/paplayer/ICodec.h new file mode 100644 index 0000000..1b45280 --- /dev/null +++ b/xbmc/cores/paplayer/ICodec.h @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "cores/AudioEngine/Utils/AEAudioFormat.h" +#include "filesystem/File.h" +#include "music/tags/MusicInfoTag.h" + +#include <string> + +#define READ_EOF -1 +#define READ_SUCCESS 0 +#define READ_ERROR 1 + +class CFileItem; + +class ICodec +{ +public: + ICodec() + { + m_TotalTime = 0; + m_bitRate = 0; + m_bitsPerSample = 0; + m_bitsPerCodedSample = 0; + }; + virtual ~ICodec() = default; + + // Virtual functions that all codecs should implement. Note that these may need + // enhancing and or refactoring at a later date. It works currently well for MP3 and + // APE codecs. + + // Init(filename) + // This routine should handle any initialization necessary. At a minimum it needs to: + // 1. Load any dlls and make sure any buffers etc. are allocated. + // 2. Load the file (or at least attempt to load it) + // 3. Fill in the m_TotalTime, m_SampleRate, m_BitsPerSample and m_Channels parameters. + virtual bool Init(const CFileItem &file, unsigned int filecache)=0; + + virtual bool CanSeek() {return true;} + + // Seek() + // Should seek to the appropriate time (in ms) in the file, and return the + // time to which we managed to seek (in the case where seeking is problematic) + // This is used in FFwd/Rewd so can be called very often. + virtual bool Seek(int64_t iSeekTime)=0; + + // ReadPCM() + // Decodes audio into pBuffer up to size bytes. The actual amount of returned data + // is given in actualsize. Returns READ_SUCCESS on success. Returns READ_EOF when + // the data has been exhausted, and READ_ERROR on error. + virtual int ReadPCM(uint8_t* pBuffer, size_t size, size_t* actualsize) = 0; + + virtual int ReadRaw(uint8_t **pBuffer, int *bufferSize) { return READ_ERROR; } + + // CanInit() + // Should return true if the codec can be initialized + // eg. check if a dll needed for the codec exists + virtual bool CanInit()=0; + + // set the total time - useful when info comes from a preset tag + virtual void SetTotalTime(int64_t totaltime) {} + + virtual bool IsCaching() const {return false;} + virtual int GetCacheLevel() const {return -1;} + + int64_t m_TotalTime; // time in ms + int m_bitRate; + int m_bitsPerSample; + int m_bitsPerCodedSample; + std::string m_CodecName; + MUSIC_INFO::CMusicInfoTag m_tag; + XFILE::CFile m_file; + AEAudioFormat m_format; +}; + diff --git a/xbmc/cores/paplayer/PAPlayer.cpp b/xbmc/cores/paplayer/PAPlayer.cpp new file mode 100644 index 0000000..05bf906 --- /dev/null +++ b/xbmc/cores/paplayer/PAPlayer.cpp @@ -0,0 +1,1207 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PAPlayer.h" + +#include "FileItem.h" +#include "ICodec.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "Util.h" +#include "cores/AudioEngine/Interfaces/AE.h" +#include "cores/AudioEngine/Interfaces/AEStream.h" +#include "cores/AudioEngine/Utils/AEStreamData.h" +#include "cores/AudioEngine/Utils/AEUtil.h" +#include "cores/DataCacheCore.h" +#include "cores/VideoPlayer/Process/ProcessInfo.h" +#include "messaging/ApplicationMessenger.h" +#include "music/tags/MusicInfoTag.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "threads/SystemClock.h" +#include "utils/JobManager.h" +#include "utils/log.h" +#include "video/Bookmark.h" + +#include <mutex> + +using namespace std::chrono_literals; + +#define TIME_TO_CACHE_NEXT_FILE 5000 /* 5 seconds before end of song, start caching the next song */ +#define FAST_XFADE_TIME 80 /* 80 milliseconds */ +#define MAX_SKIP_XFADE_TIME 2000 /* max 2 seconds crossfade on track skip */ + +// PAP: Psycho-acoustic Audio Player +// Supporting all open audio codec standards. +// First one being nullsoft's nsv audio decoder format + +PAPlayer::PAPlayer(IPlayerCallback& callback) : + IPlayer(callback), + CThread("PAPlayer"), + m_signalSpeedChange(false), + m_playbackSpeed(1 ), + m_isPlaying(false), + m_isPaused(false), + m_isFinished(false), + m_defaultCrossfadeMS (0), + m_upcomingCrossfadeMS(0), + m_audioCallback(NULL ), + m_jobCounter(0), + m_newForcedPlayerTime(-1), + m_newForcedTotalTime (-1) +{ + memset(&m_playerGUIData, 0, sizeof(m_playerGUIData)); + m_processInfo.reset(CProcessInfo::CreateInstance()); + m_processInfo->SetDataCache(&CServiceBroker::GetDataCacheCore()); +} + +PAPlayer::~PAPlayer() +{ + CloseFile(); +} + +void PAPlayer::SoftStart(bool wait/* = false */) +{ + std::unique_lock<CCriticalSection> lock(m_streamsLock); + for(StreamList::iterator itt = m_streams.begin(); itt != m_streams.end(); ++itt) + { + StreamInfo* si = *itt; + if (si->m_fadeOutTriggered) + continue; + + si->m_stream->Resume(); + si->m_stream->FadeVolume(0.0f, 1.0f, FAST_XFADE_TIME); + } + + if (wait) + { + /* wait for them to fade in */ + lock.unlock(); + CThread::Sleep(std::chrono::milliseconds(FAST_XFADE_TIME)); + lock.lock(); + + /* be sure they have faded in */ + while(wait) + { + wait = false; + for(StreamList::iterator itt = m_streams.begin(); itt != m_streams.end(); ++itt) + { + StreamInfo* si = *itt; + if (si->m_stream->IsFading()) + { + lock.unlock(); + wait = true; + CThread::Sleep(1ms); + lock.lock(); + break; + } + } + } + } +} + +void PAPlayer::SoftStop(bool wait/* = false */, bool close/* = true */) +{ + /* fade all the streams out fast for a nice soft stop */ + std::unique_lock<CCriticalSection> lock(m_streamsLock); + for(StreamList::iterator itt = m_streams.begin(); itt != m_streams.end(); ++itt) + { + StreamInfo* si = *itt; + if (si->m_stream) + si->m_stream->FadeVolume(1.0f, 0.0f, FAST_XFADE_TIME); + + if (close) + { + si->m_prepareTriggered = true; + si->m_playNextTriggered = true; + si->m_fadeOutTriggered = true; + } + } + + /* if we are going to wait for them to finish fading */ + if(wait) + { + // fail safe timer, do not wait longer than 1000ms + XbmcThreads::EndTime<> timer(1000ms); + + /* wait for them to fade out */ + lock.unlock(); + CThread::Sleep(std::chrono::milliseconds(FAST_XFADE_TIME)); + lock.lock(); + + /* be sure they have faded out */ + while(wait && !CServiceBroker::GetActiveAE()->IsSuspended() && !timer.IsTimePast()) + { + wait = false; + for(StreamList::iterator itt = m_streams.begin(); itt != m_streams.end(); ++itt) + { + StreamInfo* si = *itt; + if (si->m_stream && si->m_stream->IsFading()) + { + lock.unlock(); + wait = true; + CThread::Sleep(1ms); + lock.lock(); + break; + } + } + } + + /* if we are not closing the streams, pause them */ + if (!close) + { + for(StreamList::iterator itt = m_streams.begin(); itt != m_streams.end(); ++itt) + { + StreamInfo* si = *itt; + si->m_stream->Pause(); + } + } + } +} + +void PAPlayer::CloseAllStreams(bool fade/* = true */) +{ + if (!fade) + { + std::unique_lock<CCriticalSection> lock(m_streamsLock); + while (!m_streams.empty()) + { + StreamInfo* si = m_streams.front(); + m_streams.pop_front(); + + if (si->m_stream) + { + CloseFileCB(*si); + si->m_stream.reset(); + } + + si->m_decoder.Destroy(); + delete si; + } + + while (!m_finishing.empty()) + { + StreamInfo* si = m_finishing.front(); + m_finishing.pop_front(); + + if (si->m_stream) + { + CloseFileCB(*si); + si->m_stream.reset(); + } + + si->m_decoder.Destroy(); + delete si; + } + m_currentStream = nullptr; + } + else + { + SoftStop(false, true); + std::unique_lock<CCriticalSection> lock(m_streamsLock); + m_currentStream = NULL; + } +} + +bool PAPlayer::OpenFile(const CFileItem& file, const CPlayerOptions &options) +{ + m_defaultCrossfadeMS = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MUSICPLAYER_CROSSFADE) * 1000; + m_fullScreen = options.fullscreen; + + if (m_streams.size() > 1 || !m_defaultCrossfadeMS || m_isPaused) + { + CloseAllStreams(!m_isPaused); + StopThread(); + m_isPaused = false; // Make sure to reset the pause state + } + + { + std::unique_lock<CCriticalSection> lock(m_streamsLock); + m_jobCounter++; + } + CServiceBroker::GetJobManager()->Submit([=]() { QueueNextFileEx(file, false); }, this, + CJob::PRIORITY_NORMAL); + + std::unique_lock<CCriticalSection> lock(m_streamsLock); + if (m_streams.size() == 2) + { + //do a short crossfade on trackskip, set to max 2 seconds for these prev/next transitions + m_upcomingCrossfadeMS = std::min(m_defaultCrossfadeMS, (unsigned int)MAX_SKIP_XFADE_TIME); + + //start transition to next track + StreamInfo* si = m_streams.front(); + si->m_playNextAtFrame = si->m_framesSent; //start next track at current frame + si->m_prepareTriggered = true; //next track is ready to go + } + lock.unlock(); + + if (!IsRunning()) + Create(); + + /* trigger playback start */ + m_isPlaying = true; + m_startEvent.Set(); + + // OnPlayBackStarted to be made only once. Callback processing may be slower than player process + // so clear signal flag first otherwise async stream processing could also make callback + m_signalStarted = false; + m_callback.OnPlayBackStarted(file); + + return true; +} + +void PAPlayer::UpdateCrossfadeTime(const CFileItem& file) +{ + // we explicitly disable crossfading for audio cds + if (file.IsCDDA()) + m_upcomingCrossfadeMS = 0; + else + m_upcomingCrossfadeMS = m_defaultCrossfadeMS = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MUSICPLAYER_CROSSFADE) * 1000; + + if (m_upcomingCrossfadeMS) + { + if (!m_currentStream || (file.HasMusicInfoTag() && + !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICPLAYER_CROSSFADEALBUMTRACKS) && + m_currentStream->m_fileItem->HasMusicInfoTag() && + (m_currentStream->m_fileItem->GetMusicInfoTag()->GetAlbum() != "") && + (m_currentStream->m_fileItem->GetMusicInfoTag()->GetAlbum() == + file.GetMusicInfoTag()->GetAlbum()) && + (m_currentStream->m_fileItem->GetMusicInfoTag()->GetDiscNumber() == + file.GetMusicInfoTag()->GetDiscNumber()) && + (m_currentStream->m_fileItem->GetMusicInfoTag()->GetTrackNumber() == + file.GetMusicInfoTag()->GetTrackNumber() - 1))) + { + //do not crossfade when playing consecutive albumtracks + m_upcomingCrossfadeMS = 0; + } + } +} + +bool PAPlayer::QueueNextFile(const CFileItem &file) +{ + { + std::unique_lock<CCriticalSection> lock(m_streamsLock); + m_jobCounter++; + } + CServiceBroker::GetJobManager()->Submit([this, file]() { QueueNextFileEx(file, true); }, this, + CJob::PRIORITY_NORMAL); + + return true; +} + +bool PAPlayer::QueueNextFileEx(const CFileItem &file, bool fadeIn) +{ + if (m_currentStream) + { + // check if we advance a track of a CUE sheet + // if this is the case we don't need to open a new stream + std::string newURL = file.GetDynURL().GetFileName(); + std::string oldURL = m_currentStream->m_fileItem->GetDynURL().GetFileName(); + if (newURL.compare(oldURL) == 0 && file.GetStartOffset() && + file.GetStartOffset() == m_currentStream->m_fileItem->GetEndOffset() && m_currentStream && + m_currentStream->m_prepareTriggered) + { + m_currentStream->m_nextFileItem.reset(new CFileItem(file)); + m_upcomingCrossfadeMS = 0; + return true; + } + m_currentStream->m_nextFileItem.reset(); + } + + StreamInfo *si = new StreamInfo(); + si->m_fileItem = std::make_unique<CFileItem>(file); + + // Start stream at zero offset + si->m_startOffset = 0; + //File item start offset defines where in song to resume + double starttime = CUtil::ConvertMilliSecsToSecs(si->m_fileItem->GetStartOffset()); + + // Music from cuesheet => "item_start" and offset match + // Start offset defines where this song starts in file of multiple songs + if (si->m_fileItem->HasProperty("item_start") && + (si->m_fileItem->GetProperty("item_start").asInteger() == si->m_fileItem->GetStartOffset())) + { + // Start stream at offset from cuesheet + si->m_startOffset = si->m_fileItem->GetStartOffset(); + starttime = 0; // No resume point + } + + if (!si->m_decoder.Create(file, si->m_startOffset)) + { + CLog::Log(LOGWARNING, "PAPlayer::QueueNextFileEx - Failed to create the decoder"); + + // advance playlist + AdvancePlaylistOnError(*si->m_fileItem); + m_callback.OnQueueNextItem(); + + delete si; + return false; + } + + /* decode until there is data-available */ + si->m_decoder.Start(); + while (si->m_decoder.GetDataSize(true) == 0) + { + int status = si->m_decoder.GetStatus(); + if (status == STATUS_ENDED || + status == STATUS_NO_FILE || + si->m_decoder.ReadSamples(PACKET_SIZE) == RET_ERROR) + { + CLog::Log(LOGINFO, "PAPlayer::QueueNextFileEx - Error reading samples"); + + si->m_decoder.Destroy(); + // advance playlist + AdvancePlaylistOnError(*si->m_fileItem); + m_callback.OnQueueNextItem(); + delete si; + return false; + } + + /* yield our time so that the main PAP thread doesn't stall */ + CThread::Sleep(1ms); + } + + // set m_upcomingCrossfadeMS depending on type of file and user settings + UpdateCrossfadeTime(*si->m_fileItem); + + /* init the streaminfo struct */ + si->m_audioFormat = si->m_decoder.GetFormat(); + // si->m_startOffset already initialized + si->m_endOffset = file.GetEndOffset(); + si->m_bytesPerSample = CAEUtil::DataFormatToBits(si->m_audioFormat.m_dataFormat) >> 3; + si->m_bytesPerFrame = si->m_bytesPerSample * si->m_audioFormat.m_channelLayout.Count(); + si->m_started = false; + si->m_finishing = false; + si->m_framesSent = 0; + si->m_seekNextAtFrame = 0; + si->m_seekFrame = -1; + si->m_stream = NULL; + si->m_volume = (fadeIn && m_upcomingCrossfadeMS) ? 0.0f : 1.0f; + si->m_fadeOutTriggered = false; + si->m_isSlaved = false; + + si->m_decoderTotal = si->m_decoder.TotalTime(); + int64_t streamTotalTime = si->m_decoderTotal; + if (si->m_endOffset) + streamTotalTime = si->m_endOffset - si->m_startOffset; + + // Seek to a resume point + if (si->m_fileItem->HasProperty("StartPercent") && + (si->m_fileItem->GetProperty("StartPercent").asDouble() > 0) && + (si->m_fileItem->GetProperty("StartPercent").asDouble() <= 100)) + { + si->m_seekFrame = + si->m_audioFormat.m_sampleRate * + CUtil::ConvertMilliSecsToSecs(static_cast<int>(+(static_cast<double>( + streamTotalTime * (si->m_fileItem->GetProperty("StartPercent").asDouble() / 100.0))))); + } + else if (starttime > 0) + si->m_seekFrame = si->m_audioFormat.m_sampleRate * starttime; + else if (si->m_fileItem->HasProperty("audiobook_bookmark")) + si->m_seekFrame = si->m_audioFormat.m_sampleRate * + CUtil::ConvertMilliSecsToSecs( + si->m_fileItem->GetProperty("audiobook_bookmark").asInteger()); + + si->m_prepareNextAtFrame = 0; + // cd drives don't really like it to be crossfaded or prepared + if (!file.IsCDDA()) + { + if (streamTotalTime >= TIME_TO_CACHE_NEXT_FILE + m_defaultCrossfadeMS) + si->m_prepareNextAtFrame = (int)((streamTotalTime - TIME_TO_CACHE_NEXT_FILE - m_defaultCrossfadeMS) * si->m_audioFormat.m_sampleRate / 1000.0f); + } + + if (m_currentStream && ((m_currentStream->m_audioFormat.m_dataFormat == AE_FMT_RAW) || (si->m_audioFormat.m_dataFormat == AE_FMT_RAW))) + { + m_currentStream->m_prepareTriggered = false; + m_currentStream->m_waitOnDrain = true; + m_currentStream->m_prepareNextAtFrame = 0; + si->m_decoder.Destroy(); + delete si; + return false; + } + + si->m_prepareTriggered = false; + si->m_playNextAtFrame = 0; + si->m_playNextTriggered = false; + si->m_waitOnDrain = false; + + if (!PrepareStream(si)) + { + CLog::Log(LOGINFO, "PAPlayer::QueueNextFileEx - Error preparing stream"); + + si->m_decoder.Destroy(); + // advance playlist + AdvancePlaylistOnError(*si->m_fileItem); + m_callback.OnQueueNextItem(); + delete si; + return false; + } + + /* add the stream to the list */ + std::unique_lock<CCriticalSection> lock(m_streamsLock); + m_streams.push_back(si); + //update the current stream to start playing the next track at the correct frame. + UpdateStreamInfoPlayNextAtFrame(m_currentStream, m_upcomingCrossfadeMS); + + return true; +} + +void PAPlayer::UpdateStreamInfoPlayNextAtFrame(StreamInfo *si, unsigned int crossFadingTime) +{ + // if no crossfading or cue sheet, wait for eof + if (si && (crossFadingTime || si->m_endOffset)) + { + int64_t streamTotalTime = si->m_decoder.TotalTime(); + if (si->m_endOffset) + streamTotalTime = si->m_endOffset - si->m_startOffset; + if (streamTotalTime < crossFadingTime) + si->m_playNextAtFrame = (int)((streamTotalTime / 2) * si->m_audioFormat.m_sampleRate / 1000.0f); + else + si->m_playNextAtFrame = (int)((streamTotalTime - crossFadingTime) * si->m_audioFormat.m_sampleRate / 1000.0f); + } +} + +inline bool PAPlayer::PrepareStream(StreamInfo *si) +{ + /* if we have a stream we are already prepared */ + if (si->m_stream) + return true; + + /* get a paused stream */ + AEAudioFormat format = si->m_audioFormat; + si->m_stream = CServiceBroker::GetActiveAE()->MakeStream( + format, + AESTREAM_PAUSED + ); + + if (!si->m_stream) + { + CLog::Log(LOGDEBUG, "PAPlayer::PrepareStream - Failed to get IAEStream"); + return false; + } + + si->m_stream->SetVolume(si->m_volume); + float peak = 1.0; + float gain = si->m_decoder.GetReplayGain(peak); + if (peak * gain <= 1.0f) + // No clipping protection needed + si->m_stream->SetReplayGain(gain); + else if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICPLAYER_REPLAYGAINAVOIDCLIPPING)) + // Normalise volume reducing replaygain to avoid needing clipping protection, plays file at lower level + si->m_stream->SetReplayGain(1.0f / fabs(peak)); + else + // Clipping protection (when enabled in AE) by audio limiting, applied just where needed + si->m_stream->SetAmplification(gain); + + /* if its not the first stream and crossfade is not enabled */ + if (m_currentStream && m_currentStream != si && !m_upcomingCrossfadeMS) + { + /* slave the stream for gapless */ + si->m_isSlaved = true; + m_currentStream->m_stream->RegisterSlave(si->m_stream.get()); + } + + /* fill the stream's buffer */ + while(si->m_stream->IsBuffering()) + { + int status = si->m_decoder.GetStatus(); + if (status == STATUS_ENDED || + status == STATUS_NO_FILE || + si->m_decoder.ReadSamples(PACKET_SIZE) == RET_ERROR) + { + CLog::Log(LOGINFO, "PAPlayer::PrepareStream - Stream Finished"); + break; + } + + if (!QueueData(si)) + break; + + /* yield our time so that the main PAP thread doesn't stall */ + CThread::Sleep(1ms); + } + + CLog::Log(LOGINFO, "PAPlayer::PrepareStream - Ready"); + + return true; +} + +bool PAPlayer::CloseFile(bool reopen) +{ + if (reopen) + CServiceBroker::GetActiveAE()->KeepConfiguration(3000); + + if (!m_isPaused) + SoftStop(true, true); + CloseAllStreams(false); + + /* wait for the thread to terminate */ + StopThread(true);//true - wait for end of thread + + // wait for any pending jobs to complete + { + std::unique_lock<CCriticalSection> lock(m_streamsLock); + while (m_jobCounter > 0) + { + lock.unlock(); + m_jobEvent.Wait(100ms); + lock.lock(); + } + } + + return true; +} + +void PAPlayer::Process() +{ + if (!m_startEvent.Wait(100ms)) + { + CLog::Log(LOGDEBUG, "PAPlayer::Process - Failed to receive start event"); + return; + } + + CLog::Log(LOGDEBUG, "PAPlayer::Process - Playback started"); + while(m_isPlaying && !m_bStop) + { + /* this needs to happen outside of any locks to prevent deadlocks */ + if (m_signalSpeedChange) + { + m_callback.OnPlayBackSpeedChanged(m_playbackSpeed); + m_signalSpeedChange = false; + } + + double freeBufferTime = 0.0; + ProcessStreams(freeBufferTime); + + // if none of our streams wants at least 10ms of data, we sleep + if (freeBufferTime < 0.01) + { + CThread::Sleep(10ms); + } + + if (m_newForcedPlayerTime != -1) + { + if (SetTimeInternal(m_newForcedPlayerTime)) + { + m_newForcedPlayerTime = -1; + } + } + + if (m_newForcedTotalTime != -1) + { + if (SetTotalTimeInternal(m_newForcedTotalTime)) + { + m_newForcedTotalTime = -1; + } + } + + GetTimeInternal(); //update for GUI + } + m_isPlaying = false; +} + +inline void PAPlayer::ProcessStreams(double &freeBufferTime) +{ + std::unique_lock<CCriticalSection> sharedLock(m_streamsLock); + if (m_isFinished && m_streams.empty() && m_finishing.empty()) + { + m_isPlaying = false; + freeBufferTime = 1.0; + return; + } + + /* destroy any drained streams */ + for (auto itt = m_finishing.begin(); itt != m_finishing.end();) + { + StreamInfo* si = *itt; + if (si->m_stream->IsDrained()) + { + itt = m_finishing.erase(itt); + CloseFileCB(*si); + delete si; + CLog::Log(LOGDEBUG, "PAPlayer::ProcessStreams - Stream Freed"); + } + else + ++itt; + } + + sharedLock.unlock(); + std::unique_lock<CCriticalSection> lock(m_streamsLock); + + for(StreamList::iterator itt = m_streams.begin(); itt != m_streams.end(); ++itt) + { + StreamInfo* si = *itt; + if (!m_currentStream && !si->m_started) + { + m_currentStream = si; + UpdateGUIData(si); //update for GUI + } + /* if the stream is finishing */ + if ((si->m_playNextTriggered && si->m_stream && !si->m_stream->IsFading()) || !ProcessStream(si, freeBufferTime)) + { + if (!si->m_prepareTriggered) + { + if (si->m_waitOnDrain) + { + si->m_stream->Drain(true); + si->m_waitOnDrain = false; + } + si->m_prepareTriggered = true; + m_callback.OnQueueNextItem(); + } + + /* remove the stream */ + itt = m_streams.erase(itt); + /* if its the current stream */ + if (si == m_currentStream) + { + /* if it was the last stream */ + if (itt == m_streams.end()) + { + /* if it didn't trigger the next queue item */ + if (!si->m_prepareTriggered) + { + if (si->m_waitOnDrain) + { + si->m_stream->Drain(true); + si->m_waitOnDrain = false; + } + m_callback.OnQueueNextItem(); + si->m_prepareTriggered = true; + } + m_currentStream = NULL; + } + else + { + m_currentStream = *itt; + UpdateGUIData(*itt); //update for GUI + } + } + + /* unregister the audio callback */ + si->m_stream->UnRegisterAudioCallback(); + si->m_decoder.Destroy(); + si->m_stream->Drain(false); + m_finishing.push_back(si); + return; + } + + if (!si->m_started) + continue; + + // is it time to prepare the next stream? + if (si->m_prepareNextAtFrame > 0 && !si->m_prepareTriggered && si->m_framesSent >= si->m_prepareNextAtFrame) + { + si->m_prepareTriggered = true; + m_callback.OnQueueNextItem(); + } + + // it is time to start playing the next stream? + if (si->m_playNextAtFrame > 0 && !si->m_playNextTriggered && !si->m_nextFileItem && si->m_framesSent >= si->m_playNextAtFrame) + { + if (!si->m_prepareTriggered) + { + si->m_prepareTriggered = true; + m_callback.OnQueueNextItem(); + } + + if (!m_isFinished) + { + if (m_upcomingCrossfadeMS) + { + si->m_stream->FadeVolume(1.0f, 0.0f, m_upcomingCrossfadeMS); + si->m_fadeOutTriggered = true; + } + m_currentStream = NULL; + + /* unregister the audio callback */ + si->m_stream->UnRegisterAudioCallback(); + } + + si->m_playNextTriggered = true; + } + } +} + +inline bool PAPlayer::ProcessStream(StreamInfo *si, double &freeBufferTime) +{ + /* if playback needs to start on this stream, do it */ + if (si == m_currentStream && !si->m_started) + { + si->m_started = true; + si->m_stream->RegisterAudioCallback(m_audioCallback); + if (!si->m_isSlaved) + si->m_stream->Resume(); + si->m_stream->FadeVolume(0.0f, 1.0f, m_upcomingCrossfadeMS); + if (m_signalStarted) + m_callback.OnPlayBackStarted(*si->m_fileItem); + m_signalStarted = true; + if (m_fullScreen) + { + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SWITCHTOFULLSCREEN); + m_fullScreen = false; + } + m_callback.OnAVStarted(*si->m_fileItem); + } + + /* if we have not started yet and the stream has been primed */ + unsigned int space = si->m_stream->GetSpace(); + if (!si->m_started && !space) + return true; + + if (!m_playbackSpeed) + return true; + + /* see if it is time yet to FF/RW or a direct seek */ + if (!si->m_playNextTriggered && ((m_playbackSpeed != 1 && si->m_framesSent >= si->m_seekNextAtFrame) || si->m_seekFrame > -1)) + { + int64_t time = (int64_t)0; + /* if its a direct seek */ + if (si->m_seekFrame > -1) + { + time = (int64_t)((float)si->m_seekFrame / (float)si->m_audioFormat.m_sampleRate * 1000.0f); + si->m_framesSent = (int)(si->m_seekFrame - ((float)si->m_startOffset * (float)si->m_audioFormat.m_sampleRate) / 1000.0f); + si->m_seekFrame = -1; + m_playerGUIData.m_time = time; //update for GUI + si->m_seekNextAtFrame = 0; + CDataCacheCore::GetInstance().SetPlayTimes(0, time, 0, m_playerGUIData.m_totalTime); + } + /* if its FF/RW */ + else + { + si->m_framesSent += si->m_audioFormat.m_sampleRate * (m_playbackSpeed - 1); + si->m_seekNextAtFrame = si->m_framesSent + si->m_audioFormat.m_sampleRate / 2; + time = (int64_t)(((float)si->m_framesSent / (float)si->m_audioFormat.m_sampleRate * 1000.0f) + (float)si->m_startOffset); + } + + /* if we are seeking back before the start of the track start normal playback */ + if (time < si->m_startOffset || si->m_framesSent < 0) + { + time = si->m_startOffset; + si->m_framesSent = 0; + si->m_seekNextAtFrame = 0; + SetSpeed(1); + } + + si->m_decoder.Seek(time); + } + + int status = si->m_decoder.GetStatus(); + if (status == STATUS_ENDED || + status == STATUS_NO_FILE || + si->m_decoder.ReadSamples(PACKET_SIZE) == RET_ERROR || + ((si->m_endOffset) && (si->m_framesSent / si->m_audioFormat.m_sampleRate >= (si->m_endOffset - si->m_startOffset) / 1000))) + { + if (si == m_currentStream && si->m_nextFileItem) + { + CloseFileCB(*si); + + // update current stream with info of next track + si->m_startOffset = si->m_nextFileItem->GetStartOffset(); + if (si->m_nextFileItem->GetEndOffset()) + si->m_endOffset = si->m_nextFileItem->GetEndOffset(); + else + si->m_endOffset = 0; + si->m_framesSent = 0; + + *si->m_fileItem = *si->m_nextFileItem; + si->m_nextFileItem.reset(); + + int64_t streamTotalTime = si->m_decoder.TotalTime() - si->m_startOffset; + if (si->m_endOffset) + streamTotalTime = si->m_endOffset - si->m_startOffset; + + // calculate time when to prepare next stream + si->m_prepareNextAtFrame = 0; + if (streamTotalTime >= TIME_TO_CACHE_NEXT_FILE + m_defaultCrossfadeMS) + si->m_prepareNextAtFrame = (int)((streamTotalTime - TIME_TO_CACHE_NEXT_FILE - m_defaultCrossfadeMS) * si->m_audioFormat.m_sampleRate / 1000.0f); + + si->m_prepareTriggered = false; + si->m_playNextAtFrame = 0; + si->m_playNextTriggered = false; + si->m_seekNextAtFrame = 0; + + //update the current stream to start playing the next track at the correct frame. + UpdateStreamInfoPlayNextAtFrame(m_currentStream, m_upcomingCrossfadeMS); + + UpdateGUIData(si); + if (m_signalStarted) + m_callback.OnPlayBackStarted(*si->m_fileItem); + m_signalStarted = true; + m_callback.OnAVStarted(*si->m_fileItem); + } + else + { + CLog::Log(LOGINFO, "PAPlayer::ProcessStream - Stream Finished"); + return false; + } + } + + if (!QueueData(si)) + return false; + + /* update free buffer time if we are running */ + if (si->m_started) + { + if (si->m_stream->IsBuffering()) + freeBufferTime = 1.0; + else + { + double free_space; + if (si->m_audioFormat.m_dataFormat != AE_FMT_RAW) + free_space = (double)(si->m_stream->GetSpace() / si->m_bytesPerSample) / si->m_audioFormat.m_sampleRate; + else + { + if (si->m_audioFormat.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_TRUEHD && + !m_processInfo->WantsRawPassthrough()) + { + free_space = static_cast<double>(si->m_stream->GetSpace()) * + si->m_audioFormat.m_streamInfo.GetDuration() / 1000 / 2; + } + else + { + free_space = static_cast<double>(si->m_stream->GetSpace()) * + si->m_audioFormat.m_streamInfo.GetDuration() / 1000; + } + } + + freeBufferTime = std::max(freeBufferTime , free_space); + } + } + + return true; +} + +bool PAPlayer::QueueData(StreamInfo *si) +{ + unsigned int space = si->m_stream->GetSpace(); + + if (si->m_audioFormat.m_dataFormat != AE_FMT_RAW) + { + unsigned int samples = std::min(si->m_decoder.GetDataSize(false), space / si->m_bytesPerSample); + if (!samples) + return true; + + // we want complete frames + samples -= samples % si->m_audioFormat.m_channelLayout.Count(); + + uint8_t* data = (uint8_t*)si->m_decoder.GetData(samples); + if (!data) + { + CLog::Log(LOGERROR, "PAPlayer::QueueData - Failed to get data from the decoder"); + return false; + } + + unsigned int frames = samples/si->m_audioFormat.m_channelLayout.Count(); + unsigned int added = si->m_stream->AddData(&data, 0, frames, nullptr); + si->m_framesSent += added; + } + else + { + if (!space) + return true; + + int size; + uint8_t *data = si->m_decoder.GetRawData(size); + if (data && size) + { + int added = si->m_stream->AddData(&data, 0, size, nullptr); + if (added != size) + { + CLog::Log(LOGERROR, "PAPlayer::QueueData - unknown error"); + return false; + } + if (si->m_audioFormat.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_TRUEHD && + !m_processInfo->WantsRawPassthrough()) + { + si->m_framesSent += si->m_audioFormat.m_streamInfo.GetDuration() / 1000 / 2 * + si->m_audioFormat.m_streamInfo.m_sampleRate; + } + else + { + si->m_framesSent += si->m_audioFormat.m_streamInfo.GetDuration() / 1000 * + si->m_audioFormat.m_streamInfo.m_sampleRate; + } + } + } + + const ICodec* codec = si->m_decoder.GetCodec(); + m_playerGUIData.m_cacheLevel = codec ? codec->GetCacheLevel() : 0; //update for GUI + + return true; +} + +void PAPlayer::OnExit() +{ + //@todo signal OnPlayBackError if there was an error on last stream + if (m_isFinished && !m_bStop) + m_callback.OnPlayBackEnded(); + else + m_callback.OnPlayBackStopped(); +} + +void PAPlayer::OnNothingToQueueNotify() +{ + m_isFinished = true; +} + +bool PAPlayer::IsPlaying() const +{ + return m_isPlaying; +} + +void PAPlayer::Pause() +{ + if (m_isPaused) + { + SetSpeed(1); + } + else + { + SetSpeed(0); + } +} + +void PAPlayer::SetVolume(float volume) +{ + +} + +void PAPlayer::SetDynamicRangeCompression(long drc) +{ + +} + +void PAPlayer::SetSpeed(float speed) +{ + m_playbackSpeed = static_cast<int>(speed); + CDataCacheCore::GetInstance().SetSpeed(1.0, speed); + if (m_playbackSpeed != 0 && m_isPaused) + { + m_isPaused = false; + SoftStart(); + m_callback.OnPlayBackResumed(); + } + else if (m_playbackSpeed == 0 && !m_isPaused) + { + m_isPaused = true; + SoftStop(true, false); + m_callback.OnPlayBackPaused(); + } + m_signalSpeedChange = true; +} + +int64_t PAPlayer::GetTimeInternal() +{ + std::unique_lock<CCriticalSection> lock(m_streamsLock); + if (!m_currentStream) + return 0; + + double time = ((double)m_currentStream->m_framesSent / (double)m_currentStream->m_audioFormat.m_sampleRate); + if (m_currentStream->m_stream) + time -= m_currentStream->m_stream->GetDelay(); + time = time * 1000.0; + + m_playerGUIData.m_time = (int64_t)time; //update for GUI + CDataCacheCore::GetInstance().SetPlayTimes(0, time, 0, m_playerGUIData.m_totalTime); + + return (int64_t)time; +} + +bool PAPlayer::SetTotalTimeInternal(int64_t time) +{ + std::unique_lock<CCriticalSection> lock(m_streamsLock); + if (!m_currentStream) + { + return false; + } + + m_currentStream->m_decoder.SetTotalTime(time); + UpdateGUIData(m_currentStream); + + return true; +} + +bool PAPlayer::SetTimeInternal(int64_t time) +{ + std::unique_lock<CCriticalSection> lock(m_streamsLock); + if (!m_currentStream) + return false; + + m_currentStream->m_framesSent = time / 1000 * m_currentStream->m_audioFormat.m_sampleRate; + + if (m_currentStream->m_stream) + m_currentStream->m_framesSent += m_currentStream->m_stream->GetDelay() * m_currentStream->m_audioFormat.m_sampleRate; + + return true; +} + +void PAPlayer::SetTime(int64_t time) +{ + m_newForcedPlayerTime = time; +} + +int64_t PAPlayer::GetTotalTime64() +{ + std::unique_lock<CCriticalSection> lock(m_streamsLock); + if (!m_currentStream) + return 0; + + int64_t total = m_currentStream->m_decoder.TotalTime(); + if (m_currentStream->m_endOffset) + total = m_currentStream->m_endOffset; + total -= m_currentStream->m_startOffset; + return total; +} + +void PAPlayer::SetTotalTime(int64_t time) +{ + m_newForcedTotalTime = time; +} + +int PAPlayer::GetCacheLevel() const +{ + return m_playerGUIData.m_cacheLevel; +} + +void PAPlayer::GetAudioStreamInfo(int index, AudioStreamInfo& info) const +{ + info.bitrate = m_playerGUIData.m_audioBitrate; + info.channels = m_playerGUIData.m_channelCount; + info.codecName = m_playerGUIData.m_codec; + info.samplerate = m_playerGUIData.m_sampleRate; + info.bitspersample = m_playerGUIData.m_bitsPerSample; +} + +bool PAPlayer::CanSeek() const +{ + return m_playerGUIData.m_canSeek; +} + +void PAPlayer::Seek(bool bPlus, bool bLargeStep, bool bChapterOverride) +{ + if (!CanSeek()) return; + + long long seek; + const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings(); + if (advancedSettings->m_musicUseTimeSeeking && m_playerGUIData.m_totalTime > 2 * advancedSettings->m_musicTimeSeekForwardBig) + { + if (bLargeStep) + seek = bPlus ? advancedSettings->m_musicTimeSeekForwardBig : advancedSettings->m_musicTimeSeekBackwardBig; + else + seek = bPlus ? advancedSettings->m_musicTimeSeekForward : advancedSettings->m_musicTimeSeekBackward; + seek *= 1000; + seek += m_playerGUIData.m_time; + } + else + { + float percent; + if (bLargeStep) + percent = bPlus ? static_cast<float>(advancedSettings->m_musicPercentSeekForwardBig) : static_cast<float>(advancedSettings->m_musicPercentSeekBackwardBig); + else + percent = bPlus ? static_cast<float>(advancedSettings->m_musicPercentSeekForward) : static_cast<float>(advancedSettings->m_musicPercentSeekBackward); + seek = static_cast<long long>(GetTotalTime64() * (GetPercentage() + percent) / 100); + } + + SeekTime(seek); +} + +void PAPlayer::SeekTime(int64_t iTime /*=0*/) +{ + if (!CanSeek()) return; + + std::unique_lock<CCriticalSection> lock(m_streamsLock); + if (!m_currentStream) + return; + + int64_t seekOffset = iTime - GetTimeInternal(); + + if (m_playbackSpeed != 1) + SetSpeed(1); + + m_currentStream->m_seekFrame = (int)((float)m_currentStream->m_audioFormat.m_sampleRate * ((float)iTime + (float)m_currentStream->m_startOffset) / 1000.0f); + m_callback.OnPlayBackSeek(iTime, seekOffset); +} + +void PAPlayer::SeekPercentage(float fPercent /*=0*/) +{ + if (fPercent < 0.0f ) fPercent = 0.0f; + if (fPercent > 100.0f) fPercent = 100.0f; + SeekTime((int64_t)(fPercent * 0.01f * (float)GetTotalTime64())); +} + +float PAPlayer::GetPercentage() +{ + if (m_playerGUIData.m_totalTime > 0) + return m_playerGUIData.m_time * 100.0f / m_playerGUIData.m_totalTime; + + return 0.0f; +} + +void PAPlayer::UpdateGUIData(StreamInfo *si) +{ + /* Store data need by external threads in member + * structure to prevent locking conflicts when + * data required by GUI and main application + */ + std::unique_lock<CCriticalSection> lock(m_streamsLock); + + m_playerGUIData.m_sampleRate = si->m_audioFormat.m_sampleRate; + m_playerGUIData.m_channelCount = si->m_audioFormat.m_channelLayout.Count(); + m_playerGUIData.m_canSeek = si->m_decoder.CanSeek(); + + const ICodec* codec = si->m_decoder.GetCodec(); + + m_playerGUIData.m_audioBitrate = codec ? codec->m_bitRate : 0; + strncpy(m_playerGUIData.m_codec,codec ? codec->m_CodecName.c_str() : "",20); + m_playerGUIData.m_cacheLevel = codec ? codec->GetCacheLevel() : 0; + m_playerGUIData.m_bitsPerSample = (codec && codec->m_bitsPerCodedSample) ? codec->m_bitsPerCodedSample : si->m_bytesPerSample << 3; + + int64_t total = si->m_decoder.TotalTime(); + if (si->m_endOffset) + total = m_currentStream->m_endOffset; + total -= m_currentStream->m_startOffset; + m_playerGUIData.m_totalTime = total; + + CServiceBroker::GetDataCacheCore().SignalAudioInfoChange(); +} + +void PAPlayer::OnJobComplete(unsigned int jobID, bool success, CJob *job) +{ + std::unique_lock<CCriticalSection> lock(m_streamsLock); + m_jobCounter--; + m_jobEvent.Set(); +} + +void PAPlayer::CloseFileCB(StreamInfo &si) +{ + IPlayerCallback *cb = &m_callback; + CFileItem fileItem(*si.m_fileItem); + CBookmark bookmark; + double total = si.m_decoderTotal; + if (si.m_endOffset) + total = si.m_endOffset; + total -= si.m_startOffset; + bookmark.totalTimeInSeconds = total / 1000; + bookmark.timeInSeconds = (static_cast<double>(si.m_framesSent) / + static_cast<double>(si.m_audioFormat.m_sampleRate)); + bookmark.timeInSeconds -= si.m_stream->GetDelay(); + bookmark.player = m_name; + bookmark.playerState = GetPlayerState(); + CServiceBroker::GetJobManager()->Submit([=]() { cb->OnPlayerCloseFile(fileItem, bookmark); }, + CJob::PRIORITY_NORMAL); +} + +void PAPlayer::AdvancePlaylistOnError(CFileItem &fileItem) +{ + if (m_signalStarted) + m_callback.OnPlayBackStarted(fileItem); + m_signalStarted = true; + m_callback.OnAVStarted(fileItem); +} diff --git a/xbmc/cores/paplayer/PAPlayer.h b/xbmc/cores/paplayer/PAPlayer.h new file mode 100644 index 0000000..fe2489d --- /dev/null +++ b/xbmc/cores/paplayer/PAPlayer.h @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "AudioDecoder.h" +#include "cores/AudioEngine/Interfaces/AE.h" +#include "cores/AudioEngine/Interfaces/IAudioCallback.h" +#include "cores/IPlayer.h" +#include "threads/CriticalSection.h" +#include "threads/Thread.h" +#include "utils/Job.h" + +#include <atomic> +#include <list> +#include <vector> + +class IAEStream; +class CFileItem; +class CProcessInfo; + +class PAPlayer : public IPlayer, public CThread, public IJobCallback +{ +friend class CQueueNextFileJob; +public: + explicit PAPlayer(IPlayerCallback& callback); + ~PAPlayer() override; + + bool OpenFile(const CFileItem& file, const CPlayerOptions &options) override; + bool QueueNextFile(const CFileItem &file) override; + void OnNothingToQueueNotify() override; + bool CloseFile(bool reopen = false) override; + bool IsPlaying() const override; + void Pause() override; + bool HasVideo() const override { return false; } + bool HasAudio() const override { return true; } + bool CanSeek() const override; + void Seek(bool bPlus = true, bool bLargeStep = false, bool bChapterOverride = false) override; + void SeekPercentage(float fPercent = 0.0f) override; + void SetVolume(float volume) override; + void SetDynamicRangeCompression(long drc) override; + void SetSpeed(float speed = 0) override; + int GetCacheLevel() const override; + void SetTotalTime(int64_t time) override; + void GetAudioStreamInfo(int index, AudioStreamInfo& info) const override; + void SetTime(int64_t time) override; + void SeekTime(int64_t iTime = 0) override; + void GetAudioCapabilities(std::vector<int>& audioCaps) const override {} + + int GetAudioStreamCount() const override { return 1; } + int GetAudioStream() override { return 0; } + + // implementation of IJobCallback + void OnJobComplete(unsigned int jobID, bool success, CJob *job) override; + + struct + { + char m_codec[21]; + int64_t m_time; + int64_t m_totalTime; + int m_channelCount; + int m_bitsPerSample; + int m_sampleRate; + int m_audioBitrate; + int m_cacheLevel; + bool m_canSeek; + } m_playerGUIData; + +protected: + // implementation of CThread + void OnStartup() override {} + void Process() override; + void OnExit() override; + float GetPercentage(); + +private: + struct StreamInfo + { + std::unique_ptr<CFileItem> m_fileItem; + std::unique_ptr<CFileItem> m_nextFileItem; + CAudioDecoder m_decoder; /* the stream decoder */ + int64_t m_startOffset; /* the stream start offset */ + int64_t m_endOffset; /* the stream end offset */ + int64_t m_decoderTotal = 0; + AEAudioFormat m_audioFormat; + unsigned int m_bytesPerSample; /* number of bytes per audio sample */ + unsigned int m_bytesPerFrame; /* number of bytes per audio frame */ + + bool m_started; /* if playback of this stream has been started */ + bool m_finishing; /* if this stream is finishing */ + int m_framesSent; /* number of frames sent to the stream */ + int m_prepareNextAtFrame; /* when to prepare the next stream */ + bool m_prepareTriggered; /* if the next stream has been prepared */ + int m_playNextAtFrame; /* when to start playing the next stream */ + bool m_playNextTriggered; /* if this stream has started the next one */ + bool m_fadeOutTriggered; /* if the stream has been told to fade out */ + int m_seekNextAtFrame; /* the FF/RR sample to seek at */ + int m_seekFrame; /* the exact position to seek too, -1 for none */ + + IAE::StreamPtr m_stream; /* the playback stream */ + float m_volume; /* the initial volume level to set the stream to on creation */ + + bool m_isSlaved; /* true if the stream has been slaved to another */ + bool m_waitOnDrain; /* wait for stream being drained in AE */ + }; + + typedef std::list<StreamInfo*> StreamList; + + bool m_signalSpeedChange; /* true if OnPlaybackSpeedChange needs to be called */ + bool m_signalStarted = true; + std::atomic_int m_playbackSpeed; /* the playback speed (1 = normal) */ + bool m_isPlaying; + bool m_isPaused; + bool m_isFinished; /* if there are no more songs in the queue */ + bool m_fullScreen; + unsigned int m_defaultCrossfadeMS; /* how long the default crossfade is in ms */ + unsigned int m_upcomingCrossfadeMS; /* how long the upcoming crossfade is in ms */ + CEvent m_startEvent; /* event for playback start */ + StreamInfo* m_currentStream = nullptr; + IAudioCallback* m_audioCallback; /* the viz audio callback */ + + CCriticalSection m_streamsLock; /* lock for the stream list */ + StreamList m_streams; /* playing streams */ + StreamList m_finishing; /* finishing streams */ + int m_jobCounter; + CEvent m_jobEvent; + int64_t m_newForcedPlayerTime; + int64_t m_newForcedTotalTime; + std::unique_ptr<CProcessInfo> m_processInfo; + + bool QueueNextFileEx(const CFileItem &file, bool fadeIn); + void SoftStart(bool wait = false); + void SoftStop(bool wait = false, bool close = true); + void CloseAllStreams(bool fade = true); + void ProcessStreams(double &freeBufferTime); + bool PrepareStream(StreamInfo *si); + bool ProcessStream(StreamInfo *si, double &freeBufferTime); + bool QueueData(StreamInfo *si); + int64_t GetTotalTime64(); + void UpdateCrossfadeTime(const CFileItem& file); + void UpdateStreamInfoPlayNextAtFrame(StreamInfo *si, unsigned int crossFadingTime); + void UpdateGUIData(StreamInfo *si); + int64_t GetTimeInternal(); + bool SetTimeInternal(int64_t time); + bool SetTotalTimeInternal(int64_t time); + void CloseFileCB(StreamInfo &si); + void AdvancePlaylistOnError(CFileItem &fileItem); +}; + diff --git a/xbmc/cores/paplayer/VideoPlayerCodec.cpp b/xbmc/cores/paplayer/VideoPlayerCodec.cpp new file mode 100644 index 0000000..a77a27d --- /dev/null +++ b/xbmc/cores/paplayer/VideoPlayerCodec.cpp @@ -0,0 +1,533 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "VideoPlayerCodec.h" + +#include "ServiceBroker.h" +#include "URL.h" +#include "cores/AudioEngine/AEResampleFactory.h" +#include "cores/AudioEngine/Interfaces/AE.h" +#include "cores/AudioEngine/Utils/AEUtil.h" +#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h" +#include "cores/VideoPlayer/DVDDemuxers/DVDDemuxUtils.h" +#include "cores/VideoPlayer/DVDDemuxers/DVDFactoryDemuxer.h" +#include "cores/VideoPlayer/DVDInputStreams/DVDFactoryInputStream.h" +#include "cores/VideoPlayer/DVDStreamInfo.h" +#include "music/tags/TagLoaderTagLib.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +VideoPlayerCodec::VideoPlayerCodec() : m_processInfo(CProcessInfo::CreateInstance()) +{ + m_CodecName = "VideoPlayer"; +} + +VideoPlayerCodec::~VideoPlayerCodec() +{ + DeInit(); +} + +AEAudioFormat VideoPlayerCodec::GetFormat() +{ + AEAudioFormat format; + if (m_pAudioCodec) + { + format = m_pAudioCodec->GetFormat(); + } + return format; +} + +void VideoPlayerCodec::SetContentType(const std::string &strContent) +{ + m_strContentType = strContent; + StringUtils::ToLower(m_strContentType); +} + +void VideoPlayerCodec::SetPassthroughStreamType(CAEStreamInfo::DataType streamType) +{ + m_srcFormat.m_streamInfo.m_type = streamType; +} + +bool VideoPlayerCodec::Init(const CFileItem &file, unsigned int filecache) +{ + // take precaution if Init()ialized earlier + if (m_bInited) + { + // keep things as is if Init() was done with known strFile + if (m_strFileName == file.GetDynPath()) + return true; + + // got differing filename, so cleanup before starting over + DeInit(); + } + + m_nDecodedLen = 0; + + CFileItem fileitem(file); + fileitem.SetMimeType(m_strContentType); + fileitem.SetMimeTypeForInternetFile(); + m_pInputStream = CDVDFactoryInputStream::CreateInputStream(NULL, fileitem); + if (!m_pInputStream) + { + CLog::Log(LOGERROR, "{}: Error creating input stream for {}", __FUNCTION__, file.GetDynPath()); + return false; + } + + //! @todo + //! convey CFileItem::ContentLookup() into Open() + if (!m_pInputStream->Open()) + { + CLog::Log(LOGERROR, "{}: Error opening file {}", __FUNCTION__, file.GetDynPath()); + if (m_pInputStream.use_count() > 1) + throw std::runtime_error("m_pInputStream reference count is greater than 1"); + m_pInputStream.reset(); + return false; + } + + m_pDemuxer = NULL; + + try + { + m_pDemuxer = CDVDFactoryDemuxer::CreateDemuxer(m_pInputStream); + if (!m_pDemuxer) + { + if (m_pInputStream.use_count() > 1) + throw std::runtime_error("m_pInputStream reference count is greater than 1"); + m_pInputStream.reset(); + CLog::Log(LOGERROR, "{}: Error creating demuxer", __FUNCTION__); + return false; + } + } + catch(...) + { + CLog::Log(LOGERROR, "{}: Exception thrown when opening demuxer", __FUNCTION__); + if (m_pDemuxer) + { + delete m_pDemuxer; + m_pDemuxer = NULL; + } + return false; + } + + CDemuxStream* pStream = NULL; + m_nAudioStream = -1; + int64_t demuxerId = -1; + for (auto stream : m_pDemuxer->GetStreams()) + { + if (stream && stream->type == STREAM_AUDIO) + { + m_nAudioStream = stream->uniqueId; + demuxerId = stream->demuxerId; + pStream = stream; + break; + } + } + + if (m_nAudioStream == -1) + { + CLog::Log(LOGERROR, "{}: Could not find audio stream", __FUNCTION__); + delete m_pDemuxer; + m_pDemuxer = NULL; + if (m_pInputStream.use_count() > 1) + throw std::runtime_error("m_pInputStream reference count is greater than 1"); + m_pInputStream.reset(); + return false; + } + + CDVDStreamInfo hint(*pStream, true); + + CAEStreamInfo::DataType ptStreamTye = + GetPassthroughStreamType(hint.codec, hint.samplerate, hint.profile); + m_pAudioCodec = CDVDFactoryCodec::CreateAudioCodec(hint, *m_processInfo, true, true, ptStreamTye); + if (!m_pAudioCodec) + { + CLog::Log(LOGERROR, "{}: Could not create audio codec", __FUNCTION__); + delete m_pDemuxer; + m_pDemuxer = NULL; + if (m_pInputStream.use_count() > 1) + throw std::runtime_error("m_pInputStream reference count is greater than 1"); + m_pInputStream.reset(); + return false; + } + + // Extract ReplayGain info + // tagLoaderTagLib.Load will try to determine tag type by file extension, so set fallback by contentType + std::string strFallbackFileExtension = ""; + if (m_strContentType == "audio/aacp" || + m_strContentType == "audio/aac") + strFallbackFileExtension = "m4a"; + else if (m_strContentType == "audio/x-ms-wma") + strFallbackFileExtension = "wma"; + else if (m_strContentType == "audio/x-ape" || + m_strContentType == "audio/ape") + strFallbackFileExtension = "ape"; + CTagLoaderTagLib tagLoaderTagLib; + tagLoaderTagLib.Load(file.GetDynPath(), m_tag, strFallbackFileExtension); + + // we have to decode initial data in order to get channels/samplerate + // for sanity - we read no more than 10 packets + int nErrors = 0; + for (int nPacket = 0; + nPacket < 10 && (m_channels == 0 || m_format.m_sampleRate == 0 || m_format.m_frameSize == 0); + nPacket++) + { + uint8_t dummy[256]; + size_t nSize = 256; + if (ReadPCM(dummy, nSize, &nSize) == READ_ERROR) + ++nErrors; + + m_srcFormat = m_pAudioCodec->GetFormat(); + m_format = m_srcFormat; + m_channels = m_srcFormat.m_channelLayout.Count(); + m_bitsPerSample = CAEUtil::DataFormatToBits(m_srcFormat.m_dataFormat); + m_bitsPerCodedSample = static_cast<CDemuxStreamAudio*>(pStream)->iBitsPerSample; + } + if (nErrors >= 10) + { + CLog::Log(LOGDEBUG, "{}: Could not decode data", __FUNCTION__); + return false; + } + + // test if seeking is supported + m_bCanSeek = false; + if (m_pInputStream->Seek(0, SEEK_POSSIBLE)) + { + if (Seek(1)) + { + // rewind stream to beginning + Seek(0); + m_bCanSeek = true; + } + else + { + m_pInputStream->Seek(0, SEEK_SET); + if (!m_pDemuxer->Reset()) + return false; + } + } + + if (m_channels == 0) // no data - just guess and hope for the best + { + m_srcFormat.m_channelLayout = CAEChannelInfo(AE_CH_LAYOUT_2_0); + m_channels = m_srcFormat.m_channelLayout.Count(); + } + + if (m_srcFormat.m_sampleRate == 0) + m_srcFormat.m_sampleRate = 44100; + + m_TotalTime = m_pDemuxer->GetStreamLength(); + m_bitRate = m_pAudioCodec->GetBitRate(); + if (!m_bitRate && m_TotalTime) + { + m_bitRate = (int)(((m_pInputStream->GetLength()*1000) / m_TotalTime) * 8); + } + m_CodecName = m_pDemuxer->GetStreamCodecName(demuxerId, m_nAudioStream); + + m_needConvert = false; + if (NeedConvert(m_srcFormat.m_dataFormat)) + { + m_needConvert = true; + // if we don't know the framesize yet, we will fail when converting + if (m_srcFormat.m_frameSize == 0) + return false; + + m_pResampler = ActiveAE::CAEResampleFactory::Create(); + + SampleConfig dstConfig, srcConfig; + dstConfig.channel_layout = CAEUtil::GetAVChannelLayout(m_srcFormat.m_channelLayout); + dstConfig.channels = m_channels; + dstConfig.sample_rate = m_srcFormat.m_sampleRate; + dstConfig.fmt = CAEUtil::GetAVSampleFormat(AE_FMT_FLOAT); + dstConfig.bits_per_sample = CAEUtil::DataFormatToUsedBits(AE_FMT_FLOAT); + dstConfig.dither_bits = CAEUtil::DataFormatToDitherBits(AE_FMT_FLOAT); + + srcConfig.channel_layout = CAEUtil::GetAVChannelLayout(m_srcFormat.m_channelLayout); + srcConfig.channels = m_channels; + srcConfig.sample_rate = m_srcFormat.m_sampleRate; + srcConfig.fmt = CAEUtil::GetAVSampleFormat(m_srcFormat.m_dataFormat); + srcConfig.bits_per_sample = CAEUtil::DataFormatToUsedBits(m_srcFormat.m_dataFormat); + srcConfig.dither_bits = CAEUtil::DataFormatToDitherBits(m_srcFormat.m_dataFormat); + + m_pResampler->Init(dstConfig, srcConfig, + false, + false, + M_SQRT1_2, + NULL, + AE_QUALITY_UNKNOWN, + false); + + m_planes = AE_IS_PLANAR(m_srcFormat.m_dataFormat) ? m_channels : 1; + m_format = m_srcFormat; + m_format.m_dataFormat = AE_FMT_FLOAT; + m_bitsPerSample = CAEUtil::DataFormatToBits(m_format.m_dataFormat); + } + + m_strFileName = file.GetDynPath(); + m_bInited = true; + + return true; +} + +void VideoPlayerCodec::DeInit() +{ + if (m_pDemuxer != NULL) + { + delete m_pDemuxer; + m_pDemuxer = NULL; + } + + if (m_pInputStream.use_count() > 1) + throw std::runtime_error("m_pInputStream reference count is greater than 1"); + m_pInputStream.reset(); + + m_pAudioCodec.reset(); + + delete m_pResampler; + m_pResampler = NULL; + + // cleanup format information + m_TotalTime = 0; + m_bitsPerSample = 0; + m_bitRate = 0; + m_channels = 0; + m_format.m_dataFormat = AE_FMT_INVALID; + + m_nDecodedLen = 0; + + m_strFileName = ""; + m_bInited = false; +} + +bool VideoPlayerCodec::Seek(int64_t iSeekTime) +{ + // default to announce backwards seek if !m_pPacket to not make FFmpeg + // skip mpeg audio frames at playback start + bool seekback = true; + + bool ret = m_pDemuxer->SeekTime((int)iSeekTime, seekback); + m_pAudioCodec->Reset(); + + m_nDecodedLen = 0; + + return ret; +} + +int VideoPlayerCodec::ReadPCM(uint8_t* pBuffer, size_t size, size_t* actualsize) +{ + if (m_nDecodedLen > 0) + { + size_t nLen = (size < m_nDecodedLen) ? size : m_nDecodedLen; + *actualsize = nLen; + if (m_needConvert) + { + int samples = *actualsize / (m_bitsPerSample>>3); + int frames = samples / m_channels; + m_pResampler->Resample(&pBuffer, frames, m_audioFrame.data, frames, 1.0); + for (int i=0; i<m_planes; i++) + { + m_audioFrame.data[i] += frames*m_srcFormat.m_frameSize/m_planes; + } + } + else + { + memcpy(pBuffer, m_audioFrame.data[0], *actualsize); + m_audioFrame.data[0] += (*actualsize); + } + m_nDecodedLen -= nLen; + return READ_SUCCESS; + } + + m_nDecodedLen = 0; + m_pAudioCodec->GetData(m_audioFrame); + int bytes = m_audioFrame.nb_frames * m_audioFrame.framesize; + + if (!bytes) + { + DemuxPacket* pPacket = nullptr; + do + { + if (pPacket) + CDVDDemuxUtils::FreeDemuxPacket(pPacket); + pPacket = m_pDemuxer->Read(); + } while (pPacket && pPacket->iStreamId != m_nAudioStream); + + if (!pPacket) + { + return READ_EOF; + } + + pPacket->pts = DVD_NOPTS_VALUE; + pPacket->dts = DVD_NOPTS_VALUE; + + int ret = m_pAudioCodec->AddData(*pPacket); + CDVDDemuxUtils::FreeDemuxPacket(pPacket); + if (ret < 0) + { + return READ_ERROR; + } + + m_pAudioCodec->GetData(m_audioFrame); + bytes = m_audioFrame.nb_frames * m_audioFrame.framesize; + } + + m_nDecodedLen = bytes; + // scale decoded bytes to destination format + if (m_needConvert) + m_nDecodedLen *= (m_bitsPerSample>>3) / (m_srcFormat.m_frameSize / m_channels); + + *actualsize = (m_nDecodedLen <= size) ? m_nDecodedLen : size; + if (*actualsize > 0) + { + if (m_needConvert) + { + int samples = *actualsize / (m_bitsPerSample>>3); + int frames = samples / m_channels; + m_pResampler->Resample(&pBuffer, frames, m_audioFrame.data, frames, 1.0); + for (int i=0; i<m_planes; i++) + { + m_audioFrame.data[i] += frames*m_srcFormat.m_frameSize/m_planes; + } + } + else + { + memcpy(pBuffer, m_audioFrame.data[0], *actualsize); + m_audioFrame.data[0] += *actualsize; + } + m_nDecodedLen -= *actualsize; + } + + return READ_SUCCESS; +} + +int VideoPlayerCodec::ReadRaw(uint8_t **pBuffer, int *bufferSize) +{ + DemuxPacket* pPacket; + + m_nDecodedLen = 0; + DVDAudioFrame audioframe; + + m_pAudioCodec->GetData(audioframe); + if (audioframe.nb_frames) + { + return READ_SUCCESS; + } + + do + { + pPacket = m_pDemuxer->Read(); + } while (pPacket && pPacket->iStreamId != m_nAudioStream); + + if (!pPacket) + { + return READ_EOF; + } + pPacket->pts = DVD_NOPTS_VALUE; + pPacket->dts = DVD_NOPTS_VALUE; + int ret = m_pAudioCodec->AddData(*pPacket); + CDVDDemuxUtils::FreeDemuxPacket(pPacket); + if (ret < 0) + { + return READ_ERROR; + } + + m_pAudioCodec->GetData(audioframe); + if (audioframe.nb_frames) + { + *bufferSize = audioframe.nb_frames; + *pBuffer = audioframe.data[0]; + } + else + { + *bufferSize = 0; + } + + return READ_SUCCESS; +} + +bool VideoPlayerCodec::CanInit() +{ + return true; +} + +bool VideoPlayerCodec::CanSeek() +{ + return m_bCanSeek; +} + +bool VideoPlayerCodec::NeedConvert(AEDataFormat fmt) +{ + if (fmt == AE_FMT_RAW) + return false; + + switch(fmt) + { + case AE_FMT_U8: + case AE_FMT_S16NE: + case AE_FMT_S32NE: + case AE_FMT_FLOAT: + case AE_FMT_DOUBLE: + return false; + default: + return true; + } +} + +CAEStreamInfo::DataType VideoPlayerCodec::GetPassthroughStreamType(AVCodecID codecId, + int samplerate, + int profile) +{ + AEAudioFormat format; + format.m_dataFormat = AE_FMT_RAW; + format.m_sampleRate = samplerate; + format.m_streamInfo.m_type = CAEStreamInfo::DataType::STREAM_TYPE_NULL; + switch (codecId) + { + case AV_CODEC_ID_AC3: + format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_AC3; + format.m_streamInfo.m_sampleRate = samplerate; + break; + + case AV_CODEC_ID_EAC3: + format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_EAC3; + format.m_streamInfo.m_sampleRate = samplerate; + break; + + case AV_CODEC_ID_DTS: + if (profile == FF_PROFILE_DTS_HD_HRA) + format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_DTSHD; + else if (profile == FF_PROFILE_DTS_HD_MA) + format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_DTSHD_MA; + else + format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_DTSHD_CORE; + format.m_streamInfo.m_sampleRate = samplerate; + break; + + case AV_CODEC_ID_TRUEHD: + format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_TRUEHD; + format.m_streamInfo.m_sampleRate = samplerate; + break; + + default: + format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_NULL; + } + + bool supports = CServiceBroker::GetActiveAE()->SupportsRaw(format); + + if (!supports && codecId == AV_CODEC_ID_DTS && + format.m_streamInfo.m_type != CAEStreamInfo::STREAM_TYPE_DTSHD_CORE && + CServiceBroker::GetActiveAE()->UsesDtsCoreFallback()) + { + format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_DTSHD_CORE; + supports = CServiceBroker::GetActiveAE()->SupportsRaw(format); + } + + if (supports) + return format.m_streamInfo.m_type; + else + return CAEStreamInfo::DataType::STREAM_TYPE_NULL; +} diff --git a/xbmc/cores/paplayer/VideoPlayerCodec.h b/xbmc/cores/paplayer/VideoPlayerCodec.h new file mode 100644 index 0000000..08a9c2f --- /dev/null +++ b/xbmc/cores/paplayer/VideoPlayerCodec.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "ICodec.h" +#include "cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodec.h" +#include "cores/VideoPlayer/DVDDemuxers/DVDDemux.h" +#include "cores/VideoPlayer/DVDInputStreams/DVDInputStream.h" + +namespace ActiveAE +{ + class IAEResample; +}; + +class VideoPlayerCodec : public ICodec +{ +public: + VideoPlayerCodec(); + ~VideoPlayerCodec() override; + + bool Init(const CFileItem &file, unsigned int filecache) override; + bool Seek(int64_t iSeekTime) override; + int ReadPCM(uint8_t* pBuffer, size_t size, size_t* actualsize) override; + int ReadRaw(uint8_t **pBuffer, int *bufferSize) override; + bool CanInit() override; + bool CanSeek() override; + + void DeInit(); + AEAudioFormat GetFormat(); + void SetContentType(const std::string &strContent); + + bool NeedConvert(AEDataFormat fmt); + void SetPassthroughStreamType(CAEStreamInfo::DataType streamType); + +private: + CAEStreamInfo::DataType GetPassthroughStreamType(AVCodecID codecId, int samplerate, int profile); + + CDVDDemux* m_pDemuxer{nullptr}; + std::shared_ptr<CDVDInputStream> m_pInputStream; + std::unique_ptr<CDVDAudioCodec> m_pAudioCodec; + + std::string m_strContentType; + std::string m_strFileName; + int m_nAudioStream{-1}; + size_t m_nDecodedLen{0}; + + bool m_bInited{false}; + bool m_bCanSeek{false}; + + ActiveAE::IAEResample* m_pResampler{nullptr}; + DVDAudioFrame m_audioFrame{}; + int m_planes{0}; + bool m_needConvert{false}; + AEAudioFormat m_srcFormat{}; + int m_channels{0}; + + std::unique_ptr<CProcessInfo> m_processInfo; +}; + |