/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSimulator Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;
using log4net;
using Nini.Config;
using OpenMetaverse;
using OpenMetaverse.Imaging;
using CSJ2K;
using OpenSim.Framework;
using OpenSim.Region.Framework.Interfaces;
using OpenSim.Region.Framework.Scenes;
using OpenSim.Services.Interfaces;
namespace OpenSim.Region.CoreModules.Agent.TextureSender
{
public delegate void J2KDecodeDelegate(UUID AssetId);
public class J2KDecoderModule : IRegionModule, IJ2KDecoder
{
#region IRegionModule Members
private static readonly ILog m_log
= LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
///
/// Cached Decoded Layers
///
private readonly Dictionary m_cacheddecode = new Dictionary();
private bool OpenJpegFail = false;
private string CacheFolder = Util.dataDir() + "/j2kDecodeCache";
private int CacheTimeout = 720;
private J2KDecodeFileCache fCache = null;
private Thread CleanerThread = null;
private IAssetService AssetService = null;
private Scene m_Scene = null;
///
/// List of client methods to notify of results of decode
///
private readonly Dictionary> m_notifyList = new Dictionary>();
public J2KDecoderModule()
{
}
public void Initialise(Scene scene, IConfigSource source)
{
if (m_Scene == null)
m_Scene = scene;
IConfig j2kConfig = source.Configs["J2KDecoder"];
if (j2kConfig != null)
{
CacheFolder = j2kConfig.GetString("CacheDir", CacheFolder);
CacheTimeout = j2kConfig.GetInt("CacheTimeout", CacheTimeout);
}
if (fCache == null)
fCache = new J2KDecodeFileCache(CacheFolder, CacheTimeout);
scene.RegisterModuleInterface(this);
if (CleanerThread == null && CacheTimeout != 0)
{
CleanerThread = new Thread(CleanCache);
CleanerThread.Name = "J2KCleanerThread";
CleanerThread.IsBackground = true;
CleanerThread.Start();
}
}
public void PostInitialise()
{
AssetService = m_Scene.AssetService;
}
public void Close()
{
}
public string Name
{
get { return "J2KDecoderModule"; }
}
public bool IsSharedModule
{
get { return true; }
}
#endregion
#region IJ2KDecoder Members
public void decode(UUID AssetId, byte[] assetData, DecodedCallback decodedReturn)
{
// Dummy for if decoding fails.
OpenJPEG.J2KLayerInfo[] result = new OpenJPEG.J2KLayerInfo[0];
// Check if it's cached
bool cached = false;
lock (m_cacheddecode)
{
if (m_cacheddecode.ContainsKey(AssetId))
{
cached = true;
result = m_cacheddecode[AssetId];
}
}
// If it's cached, return the cached results
if (cached)
{
decodedReturn(AssetId, result);
}
else
{
// not cached, so we need to decode it
// Add to notify list and start decoding.
// Next request for this asset while it's decoding will only be added to the notify list
// once this is decoded, requests will be served from the cache and all clients in the notifylist will be updated
bool decode = false;
lock (m_notifyList)
{
if (m_notifyList.ContainsKey(AssetId))
{
m_notifyList[AssetId].Add(decodedReturn);
}
else
{
List notifylist = new List();
notifylist.Add(decodedReturn);
m_notifyList.Add(AssetId, notifylist);
decode = true;
}
}
// Do Decode!
if (decode)
{
doJ2kDecode(AssetId, assetData);
}
}
}
///
/// Provides a synchronous decode so that caller can be assured that this executes before the next line
///
///
///
public void syncdecode(UUID AssetId, byte[] j2kdata)
{
doJ2kDecode(AssetId, j2kdata);
}
#endregion
///
/// Decode Jpeg2000 Asset Data
///
/// UUID of Asset
/// Byte Array Asset Data
private void doJ2kDecode(UUID AssetId, byte[] j2kdata)
{
int DecodeTime = 0;
DecodeTime = Environment.TickCount;
OpenJPEG.J2KLayerInfo[] layers = null;
if (!fCache.TryLoadCacheForAsset(AssetId, out layers))
{
try
{
List layerStarts = CSJ2K.J2kImage.GetLayerBoundaries(new MemoryStream(j2kdata));
if (layerStarts != null && layerStarts.Count > 0)
{
layers = new OpenJPEG.J2KLayerInfo[layerStarts.Count];
for (int i = 0; i < layerStarts.Count; i++)
{
OpenJPEG.J2KLayerInfo layer = new OpenJPEG.J2KLayerInfo();
int start = layerStarts[i];
if (i == 0)
layer.Start = 0;
else
layer.Start = layerStarts[i];
if (i == layerStarts.Count - 1)
layer.End = j2kdata.Length;
else
layer.End = layerStarts[i + 1] - 1;
layers[i] = layer;
}
}
}
catch (Exception ex)
{
m_log.Warn("[J2KDecoderModule]: CSJ2K threw an exception decoding texture " + AssetId + ": " + ex.ToString());
}
if (layers.Length == 0)
{
m_log.Warn("[J2KDecoderModule]: OpenJPEG failed to decode any layer data for texture " + AssetId + ", guessing sane defaults");
// Layer decoding completely failed. Guess at sane defaults for the layer boundaries
layers = CreateDefaultLayers(j2kdata.Length);
}
// Cache Decoded layers
lock (m_cacheddecode)
{
if (m_cacheddecode.ContainsKey(AssetId))
m_cacheddecode.Remove(AssetId);
m_cacheddecode.Add(AssetId, layers);
}
}
// Notify Interested Parties
lock (m_notifyList)
{
if (m_notifyList.ContainsKey(AssetId))
{
foreach (DecodedCallback d in m_notifyList[AssetId])
{
if (d != null)
d.DynamicInvoke(AssetId, layers);
}
m_notifyList.Remove(AssetId);
}
}
}
private OpenJPEG.J2KLayerInfo[] CreateDefaultLayers(int j2kLength)
{
OpenJPEG.J2KLayerInfo[] layers = new OpenJPEG.J2KLayerInfo[5];
layers[0] = new OpenJPEG.J2KLayerInfo();
for (int i = 0; i < layers.Length; i++)
{
OpenJPEG.J2KLayerInfo layer = new OpenJPEG.J2KLayerInfo();
if (i == 0)
layer.Start = 0;
else
layer.Start = layers[i - 1].End + 1;
// These default layer sizes are based on a small sampling of real-world texture data
// with extra padding thrown in for good measure. This is a worst case fallback plan
// and will probably not gracefully handle all real world data
layer.End = (int)(160d * Math.Exp(1.3d * (double)(i + 1)));
layers[i] = layer;
}
return layers;
}
private void CleanCache()
{
m_log.Info("[J2KDecoderModule]: Cleaner thread started");
while (true)
{
if (AssetService != null)
fCache.ScanCacheFiles(RedecodeTexture);
System.Threading.Thread.Sleep(600000);
}
}
private void RedecodeTexture(UUID assetID)
{
AssetBase texture = AssetService.Get(assetID.ToString());
if (texture == null)
return;
doJ2kDecode(assetID, texture.Data);
}
}
public class J2KDecodeFileCache
{
private readonly string m_cacheDecodeFolder;
private readonly int m_cacheTimeout;
private bool enabled = true;
private static readonly ILog m_log
= LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
///
/// Creates a new instance of a file cache
///
/// base folder for the cache. Will be created if it doesn't exist
public J2KDecodeFileCache(string pFolder, int timeout)
{
m_cacheDecodeFolder = pFolder;
m_cacheTimeout = timeout;
if (!Directory.Exists(pFolder))
Createj2KCacheFolder(pFolder);
}
///
/// Save Layers to Disk Cache
///
/// Asset to Save the layers. Used int he file name by default
/// The Layer Data from OpenJpeg
///
public bool SaveFileCacheForAsset(UUID AssetId, OpenJPEG.J2KLayerInfo[] Layers)
{
if (Layers.Length > 0 && enabled)
{
FileStream fsCache =
new FileStream(String.Format("{0}/{1}", m_cacheDecodeFolder, FileNameFromAssetId(AssetId)),
FileMode.Create);
StreamWriter fsSWCache = new StreamWriter(fsCache);
StringBuilder stringResult = new StringBuilder();
string strEnd = "\n";
for (int i = 0; i < Layers.Length; i++)
{
if (i == (Layers.Length - 1))
strEnd = "";
stringResult.AppendFormat("{0}|{1}|{2}{3}", Layers[i].Start, Layers[i].End, Layers[i].End - Layers[i].Start, strEnd);
}
fsSWCache.Write(stringResult.ToString());
fsSWCache.Close();
fsSWCache.Dispose();
fsCache.Dispose();
return true;
}
return false;
}
///
/// Loads the Layer data from the disk cache
/// Returns true if load succeeded
///
/// AssetId that we're checking the cache for
/// out layers to save to
/// true if load succeeded
public bool TryLoadCacheForAsset(UUID AssetId, out OpenJPEG.J2KLayerInfo[] Layers)
{
string filename = String.Format("{0}/{1}", m_cacheDecodeFolder, FileNameFromAssetId(AssetId));
Layers = new OpenJPEG.J2KLayerInfo[0];
if (!File.Exists(filename))
return false;
if (!enabled)
return false;
string readResult = String.Empty;
try
{
FileStream fsCachefile =
new FileStream(filename,
FileMode.Open);
StreamReader sr = new StreamReader(fsCachefile);
readResult = sr.ReadToEnd();
sr.Close();
sr.Dispose();
fsCachefile.Dispose();
}
catch (IOException ioe)
{
if (ioe is PathTooLongException)
{
m_log.Error(
"[J2KDecodeCache]: Cache Read failed. Path is too long.");
}
else if (ioe is DirectoryNotFoundException)
{
m_log.Error(
"[J2KDecodeCache]: Cache Read failed. Cache Directory does not exist!");
enabled = false;
}
else
{
m_log.Error(
"[J2KDecodeCache]: Cache Read failed. IO Exception.");
}
return false;
}
catch (UnauthorizedAccessException)
{
m_log.Error(
"[J2KDecodeCache]: Cache Read failed. UnauthorizedAccessException Exception. Do you have the proper permissions on this file?");
return false;
}
catch (ArgumentException ae)
{
if (ae is ArgumentNullException)
{
m_log.Error(
"[J2KDecodeCache]: Cache Read failed. No Filename provided");
}
else
{
m_log.Error(
"[J2KDecodeCache]: Cache Read failed. Filname was invalid");
}
return false;
}
catch (NotSupportedException)
{
m_log.Error(
"[J2KDecodeCache]: Cache Read failed, not supported. Cache disabled!");
enabled = false;
return false;
}
catch (Exception e)
{
m_log.ErrorFormat(
"[J2KDecodeCache]: Cache Read failed, unknown exception. Error: {0}",
e.ToString());
return false;
}
string[] lines = readResult.Split('\n');
if (lines.Length <= 0)
return false;
Layers = new OpenJPEG.J2KLayerInfo[lines.Length];
for (int i = 0; i < lines.Length; i++)
{
string[] elements = lines[i].Split('|');
if (elements.Length == 3)
{
int element1, element2;
try
{
element1 = Convert.ToInt32(elements[0]);
element2 = Convert.ToInt32(elements[1]);
}
catch (FormatException)
{
m_log.WarnFormat("[J2KDecodeCache]: Cache Read failed with ErrorConvert for {0}", AssetId);
Layers = new OpenJPEG.J2KLayerInfo[0];
return false;
}
Layers[i] = new OpenJPEG.J2KLayerInfo();
Layers[i].Start = element1;
Layers[i].End = element2;
}
else
{
// reading failed
m_log.WarnFormat("[J2KDecodeCache]: Cache Read failed for {0}", AssetId);
Layers = new OpenJPEG.J2KLayerInfo[0];
return false;
}
}
return true;
}
///
/// Routine which converts assetid to file name
///
/// asset id of the image
/// string filename
public string FileNameFromAssetId(UUID AssetId)
{
return String.Format("j2kCache_{0}.cache", AssetId);
}
public UUID AssetIdFromFileName(string fileName)
{
string rawId = fileName.Replace("j2kCache_", "").Replace(".cache", "");
UUID asset;
if (!UUID.TryParse(rawId, out asset))
return UUID.Zero;
return asset;
}
///
/// Creates the Cache Folder
///
/// Folder to Create
public void Createj2KCacheFolder(string pFolder)
{
try
{
Directory.CreateDirectory(pFolder);
}
catch (IOException ioe)
{
if (ioe is PathTooLongException)
{
m_log.Error(
"[J2KDecodeCache]: Cache Directory does not exist and create failed because the path to the cache folder is too long. Cache disabled!");
}
else if (ioe is DirectoryNotFoundException)
{
m_log.Error(
"[J2KDecodeCache]: Cache Directory does not exist and create failed because the supplied base of the directory folder does not exist. Cache disabled!");
}
else
{
m_log.Error(
"[J2KDecodeCache]: Cache Directory does not exist and create failed because of an IO Exception. Cache disabled!");
}
enabled = false;
}
catch (UnauthorizedAccessException)
{
m_log.Error(
"[J2KDecodeCache]: Cache Directory does not exist and create failed because of an UnauthorizedAccessException Exception. Cache disabled!");
enabled = false;
}
catch (ArgumentException ae)
{
if (ae is ArgumentNullException)
{
m_log.Error(
"[J2KDecodeCache]: Cache Directory does not exist and create failed because the folder provided is invalid! Cache disabled!");
}
else
{
m_log.Error(
"[J2KDecodeCache]: Cache Directory does not exist and create failed because no cache folder was provided! Cache disabled!");
}
enabled = false;
}
catch (NotSupportedException)
{
m_log.Error(
"[J2KDecodeCache]: Cache Directory does not exist and create failed because it's not supported. Cache disabled!");
enabled = false;
}
catch (Exception e)
{
m_log.ErrorFormat(
"[J2KDecodeCache]: Cache Directory does not exist and create failed because of an unknown exception. Cache disabled! Error: {0}",
e.ToString());
enabled = false;
}
}
public void ScanCacheFiles(J2KDecodeDelegate decode)
{
DirectoryInfo dir = new DirectoryInfo(m_cacheDecodeFolder);
FileInfo[] files = dir.GetFiles("j2kCache_*.cache");
foreach (FileInfo f in files)
{
TimeSpan fileAge = DateTime.Now - f.CreationTime;
if (m_cacheTimeout != 0 && fileAge >= TimeSpan.FromMinutes(m_cacheTimeout))
{
File.Delete(f.Name);
decode(AssetIdFromFileName(f.Name));
System.Threading.Thread.Sleep(5000);
}
}
}
}
}