mirror of
https://github.com/SubtitleEdit/subtitleedit.git
synced 2024-10-27 22:42:38 +01:00
Working on mp4 for next version...
git-svn-id: https://subtitleedit.googlecode.com/svn/trunk@673 99eadd0c-20b8-1223-b5c4-2a2b2df33de2
This commit is contained in:
parent
315c33f7f0
commit
fd772f5b24
@ -1440,6 +1440,14 @@ namespace Nikse.SubtitleEdit.Forms
|
||||
//return;
|
||||
}
|
||||
|
||||
if ((Path.GetExtension(fileName).ToLower() == ".mp4" ||
|
||||
Path.GetExtension(fileName).ToLower() == ".m4v")
|
||||
&& fi.Length > 10000)
|
||||
{
|
||||
if (ImportSubtitleFromMp4(fileName))
|
||||
return;
|
||||
}
|
||||
|
||||
if (fi.Length > 1024 * 1024 * 10) // max 10 mb
|
||||
{
|
||||
if (MessageBox.Show(string.Format(_language.FileXIsLargerThan10Mb + Environment.NewLine +
|
||||
@ -5789,6 +5797,73 @@ namespace Nikse.SubtitleEdit.Forms
|
||||
//}
|
||||
}
|
||||
|
||||
private bool ImportSubtitleFromMp4(string fileName)
|
||||
{
|
||||
Nikse.SubtitleEdit.Logic.Mp4.Mp4Parser mp4Parser = new Logic.Mp4.Mp4Parser(fileName);
|
||||
var mp4SubtitleTracks = mp4Parser.GetSubtitleTracks();
|
||||
if (mp4SubtitleTracks.Count == 00)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (mp4SubtitleTracks.Count == 1)
|
||||
{
|
||||
LoadMp4Subtitle(fileName, mp4SubtitleTracks[0]);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
MatroskaSubtitleChooser subtitleChooser = new MatroskaSubtitleChooser();
|
||||
subtitleChooser.Initialize(mp4SubtitleTracks);
|
||||
if (subtitleChooser.ShowDialog(this) == DialogResult.OK)
|
||||
{
|
||||
LoadMp4Subtitle(fileName, mp4SubtitleTracks[subtitleChooser.SelectedIndex]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadMp4Subtitle(string fileName, Logic.Mp4.Boxes.Trak mp4SubtitleTrack)
|
||||
{
|
||||
MakeHistoryForUndo(_language.BeforeImportFromMatroskaFile);
|
||||
_subtitleListViewIndex = -1;
|
||||
FileNew();
|
||||
_subtitle.Paragraphs.Clear();
|
||||
|
||||
for (int i = 0; i < mp4SubtitleTrack.Mdia.Minf.Stbl.EndTimeCodes.Count; i++)
|
||||
{
|
||||
if (mp4SubtitleTrack.Mdia.Minf.Stbl.Texts.Count > i)
|
||||
{
|
||||
var start = TimeSpan.FromSeconds(mp4SubtitleTrack.Mdia.Minf.Stbl.StartTimeCodes[i]);
|
||||
var end = TimeSpan.FromSeconds(mp4SubtitleTrack.Mdia.Minf.Stbl.EndTimeCodes[i]);
|
||||
string text = mp4SubtitleTrack.Mdia.Minf.Stbl.Texts[i];
|
||||
_subtitle.Paragraphs.Add(new Paragraph(text, start.TotalMilliseconds, end.TotalMilliseconds));
|
||||
}
|
||||
}
|
||||
|
||||
comboBoxEncoding.Text = "UTF-8";
|
||||
ShowStatus(_language.SubtitleImportedFromMatroskaFile);
|
||||
_subtitle.Renumber(1);
|
||||
_subtitle.WasLoadedWithFrameNumbers = false;
|
||||
if (fileName.ToLower().EndsWith(".mp4") || fileName.ToLower().EndsWith(".m4v"))
|
||||
{
|
||||
_fileName = fileName.Substring(0, fileName.Length - 4);
|
||||
Text = Title + " - " + _fileName;
|
||||
}
|
||||
else
|
||||
{
|
||||
Text = Title;
|
||||
}
|
||||
_fileDateTime = new DateTime();
|
||||
|
||||
_converted = true;
|
||||
|
||||
SubtitleListview1.Fill(_subtitle, _subtitleAlternate);
|
||||
if (_subtitle.Paragraphs.Count > 0)
|
||||
SubtitleListview1.SelectIndexAndEnsureVisible(0);
|
||||
ShowSource();
|
||||
}
|
||||
|
||||
private void SubtitleListview1_DragEnter(object sender, DragEventArgs e)
|
||||
{
|
||||
// make sure they're actually dropping files (not text or anything else)
|
||||
|
@ -39,7 +39,7 @@ namespace Nikse.SubtitleEdit.Forms
|
||||
}
|
||||
|
||||
public void Initialize(List<MatroskaSubtitleInfo> subtitleInfoList)
|
||||
{
|
||||
{
|
||||
foreach (MatroskaSubtitleInfo info in subtitleInfoList)
|
||||
{
|
||||
string s = string.Format(Configuration.Settings.Language.MatroskaSubtitleChooser.TrackXLanguageYTypeZ, info.TrackNumber, info.Name, info.Language, info.CodecId);
|
||||
@ -48,6 +48,19 @@ namespace Nikse.SubtitleEdit.Forms
|
||||
listBox1.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
internal void Initialize(List<Logic.Mp4.Boxes.Trak> mp4SubtitleTracks)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (var track in mp4SubtitleTracks)
|
||||
{
|
||||
i++;
|
||||
string s = string.Format("{0}: {1}", i, track.Mdia.Mdhd.LanguageString);
|
||||
listBox1.Items.Add(s);
|
||||
}
|
||||
listBox1.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
|
||||
private void FormMatroskaSubtitleChooser_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.KeyCode == Keys.Escape)
|
||||
@ -58,5 +71,6 @@ namespace Nikse.SubtitleEdit.Forms
|
||||
{
|
||||
DialogResult = DialogResult.OK;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
32
src/Logic/Mp4/Boxes/Helper.cs
Normal file
32
src/Logic/Mp4/Boxes/Helper.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Nikse.SubtitleEdit.Logic.Mp4.Boxes
|
||||
{
|
||||
public static class Helper
|
||||
{
|
||||
public static uint GetUInt(byte[] buffer, int index)
|
||||
{
|
||||
return (uint)((buffer[index] << 24) + (buffer[index + 1] << 16) + (buffer[index + 2] << 8) + buffer[index + 3]);
|
||||
}
|
||||
|
||||
public static UInt64 GetUInt64(byte[] buffer, int index)
|
||||
{
|
||||
return (UInt64)((buffer[index] << 56) + (buffer[index + 1] << 48) + (buffer[index + 2] << 40) + (buffer[index + 3] << 32) +
|
||||
(buffer[index] << 24) + (buffer[index + 1] << 16) + (buffer[index + 2] << 8) + buffer[index + 3]);
|
||||
|
||||
}
|
||||
|
||||
public static int GetWord(byte[] buffer, int index)
|
||||
{
|
||||
return (buffer[index] << 8) + buffer[index + 1];
|
||||
}
|
||||
|
||||
public static string GetString(byte[] buffer, int index, int count)
|
||||
{
|
||||
return Encoding.UTF8.GetString(buffer, index, count);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
488
src/Logic/Mp4/Boxes/Mdhd.cs
Normal file
488
src/Logic/Mp4/Boxes/Mdhd.cs
Normal file
@ -0,0 +1,488 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Nikse.SubtitleEdit.Logic.Mp4.Boxes
|
||||
{
|
||||
public class Mdhd
|
||||
{
|
||||
|
||||
public readonly UInt64 CreationTime;
|
||||
public readonly UInt64 ModificationTime;
|
||||
public readonly UInt32 TimeScale;
|
||||
public readonly UInt64 Duration;
|
||||
public readonly string Iso639ThreeLetterCode;
|
||||
public readonly int Quality;
|
||||
|
||||
public Mdhd(FileStream fs, uint size)
|
||||
{
|
||||
byte[] b = new byte[size - 4];
|
||||
fs.Read(b, 0, b.Length);
|
||||
int languageIndex = 20;
|
||||
int version = b[0];
|
||||
if (version == 0)
|
||||
{
|
||||
CreationTime = Helper.GetUInt(b, 4);
|
||||
ModificationTime = Helper.GetUInt(b, 8);
|
||||
TimeScale = Helper.GetUInt(b, 12);
|
||||
Duration = Helper.GetUInt(b, 16);
|
||||
Quality = Helper.GetWord(b, 22);
|
||||
}
|
||||
else
|
||||
{
|
||||
CreationTime = Helper.GetUInt64(b, 4);
|
||||
ModificationTime = Helper.GetUInt64(b, 12);
|
||||
TimeScale = Helper.GetUInt(b, 16);
|
||||
Duration = Helper.GetUInt64(b, 20);
|
||||
languageIndex = 24;
|
||||
Quality = Helper.GetWord(b, 26);
|
||||
}
|
||||
|
||||
// language code = skip first byte, 5 bytes + 5 bytes + 5 bytes (add 0x60 to get ascii value)
|
||||
int languageByte = ((b[languageIndex] << 1) >> 3) + 0x60;
|
||||
int languageByte2 = ((b[languageIndex] & 0x3) << 3) + (b[languageIndex+1] >> 5) + 0x60;
|
||||
int languageByte3 = (b[languageIndex+1] & 0x1f) + 0x60;
|
||||
char x = (char)languageByte;
|
||||
char x2 = (char)languageByte2;
|
||||
char x3 = (char)languageByte3;
|
||||
Iso639ThreeLetterCode = x.ToString() + x2.ToString() + x3.ToString();
|
||||
}
|
||||
|
||||
public string LanguageString
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (Iso639ThreeLetterCode)
|
||||
{
|
||||
case ("abk"): return "Abkhazian";
|
||||
case ("ace"): return "Achinese";
|
||||
case ("ach"): return "Acoli";
|
||||
case ("ada"): return "Adangme";
|
||||
case ("aar"): return "Afar";
|
||||
case ("afh"): return "Afrihili";
|
||||
case ("afr"): return "Afrikaans";
|
||||
case ("afa"): return "Afro-Asiatic (Other)";
|
||||
case ("aka"): return "Akan";
|
||||
case ("akk"): return "Akkadian";
|
||||
case ("alb"): return "Albanian";
|
||||
case ("sqi"): return "Albanian";
|
||||
case ("ale"): return "Aleut";
|
||||
case ("alg"): return "Algonquian languages";
|
||||
case ("tut"): return "Altaic (Other)";
|
||||
case ("amh"): return "Amharic";
|
||||
case ("apa"): return "Apache languages";
|
||||
case ("ara"): return "Arabic";
|
||||
case ("arc"): return "Aramaic";
|
||||
case ("arg"): return "Arabic";
|
||||
case ("arp"): return "Arapaho";
|
||||
case ("arn"): return "Araucanian";
|
||||
case ("arw"): return "Arawak";
|
||||
case ("arm"): return "Armenian";
|
||||
case ("hye"): return "Armenian";
|
||||
case ("art"): return "Artificial (Other)";
|
||||
case ("asm"): return "Assamese";
|
||||
case ("ava"): return "Avaric";
|
||||
case ("ath"): return "Athapascan languages";
|
||||
case ("ave"): return "Avestan";
|
||||
case ("awa"): return "Awadhi";
|
||||
case ("aym"): return "Aymara";
|
||||
case ("aze"): return "Azerbaijani";
|
||||
case ("nah"): return "Aztec";
|
||||
case ("ban"): return "Balinese";
|
||||
case ("bat"): return "Baltic (Other)";
|
||||
case ("bal"): return "Baluchi";
|
||||
case ("bam"): return "Bambara";
|
||||
case ("bai"): return "Bamileke languages";
|
||||
case ("bad"): return "Banda";
|
||||
case ("bnt"): return "Bantu (Other)";
|
||||
case ("bas"): return "Basa";
|
||||
case ("bak"): return "Bashkir";
|
||||
case ("baq"): return "Basque";
|
||||
case ("eus"): return "Basque";
|
||||
case ("bej"): return "Beja";
|
||||
case ("bem"): return "Bemba";
|
||||
case ("ben"): return "Bengali";
|
||||
case ("ber"): return "Berber (Other)";
|
||||
case ("bho"): return "Bhojpuri";
|
||||
case ("bih"): return "Bihari";
|
||||
case ("bik"): return "Bikol";
|
||||
case ("bin"): return "Bini";
|
||||
case ("bis"): return "Bislama";
|
||||
case ("bra"): return "Braj";
|
||||
case ("bre"): return "Breton";
|
||||
case ("bug"): return "Buginese";
|
||||
case ("bul"): return "Bulgarian";
|
||||
case ("bua"): return "Buriat";
|
||||
case ("bur"): return "Burmese";
|
||||
case ("mya"): return "Burmese";
|
||||
case ("bel"): return "Byelorussian";
|
||||
case ("cad"): return "Caddo";
|
||||
case ("car"): return "Carib";
|
||||
case ("cat"): return "Catalan";
|
||||
case ("cau"): return "Caucasian (Other)";
|
||||
case ("ceb"): return "Cebuano";
|
||||
case ("cel"): return "Celtic (Other)";
|
||||
case ("cai"): return "Central American Indian (Other)";
|
||||
case ("chg"): return "Chagatai";
|
||||
case ("cha"): return "Chamorro";
|
||||
case ("che"): return "Chechen";
|
||||
case ("chr"): return "Cherokee";
|
||||
case ("chy"): return "Cheyenne";
|
||||
case ("chb"): return "Chibcha";
|
||||
case ("chi"): return "Chinese";
|
||||
case ("zho"): return "Chinese";
|
||||
case ("chn"): return "Chinook jargon";
|
||||
case ("cho"): return "Choctaw";
|
||||
case ("chu"): return "Church Slavic";
|
||||
case ("chv"): return "Chuvash";
|
||||
case ("cop"): return "Coptic";
|
||||
case ("cor"): return "Cornish";
|
||||
case ("cos"): return "Corsican";
|
||||
case ("cre"): return "Cree";
|
||||
case ("mus"): return "Creek";
|
||||
case ("crp"): return "Creoles and Pidgins (Other)";
|
||||
case ("cpe"): return "Creoles and Pidgins, English-based (Other)";
|
||||
case ("cpf"): return "Creoles and Pidgins, French-based (Other)";
|
||||
case ("cpp"): return "Creoles and Pidgins, Portuguese-based (Other)";
|
||||
case ("cus"): return "Cushitic (Other)";
|
||||
case (" "): return "Croatian";
|
||||
case ("ces"): return "Czech";
|
||||
case ("cze"): return "Czech";
|
||||
case ("dak"): return "Dakota";
|
||||
case ("dan"): return "Danish";
|
||||
case ("del"): return "Delaware";
|
||||
case ("din"): return "Dinka";
|
||||
case ("div"): return "Divehi";
|
||||
case ("doi"): return "Dogri";
|
||||
case ("dra"): return "Dravidian (Other)";
|
||||
case ("dua"): return "Duala";
|
||||
case ("dut"): return "Dutch";
|
||||
case ("nla"): return "Dutch";
|
||||
case ("dum"): return "Dutch, Middle (ca. 1050-1350)";
|
||||
case ("dyu"): return "Dyula";
|
||||
case ("dzo"): return "Dzongkha";
|
||||
case ("efi"): return "Efik";
|
||||
case ("egy"): return "Egyptian (Ancient)";
|
||||
case ("eka"): return "Ekajuk";
|
||||
case ("elx"): return "Elamite";
|
||||
case ("eng"): return "English";
|
||||
case ("enm"): return "English, Middle (ca. 1100-1500)";
|
||||
case ("ang"): return "English, Old (ca. 450-1100)";
|
||||
case ("esk"): return "Eskimo (Other)";
|
||||
case ("epo"): return "Esperanto";
|
||||
case ("est"): return "Estonian";
|
||||
case ("ewe"): return "Ewe";
|
||||
case ("ewo"): return "Ewondo";
|
||||
case ("fan"): return "Fang";
|
||||
case ("fat"): return "Fanti";
|
||||
case ("fao"): return "Faroese";
|
||||
case ("fij"): return "Fijian";
|
||||
case ("fin"): return "Finnish";
|
||||
case ("fiu"): return "Finno-Ugrian (Other)";
|
||||
case ("fon"): return "Fon";
|
||||
case ("fra"): return "French";
|
||||
case ("fre"): return "French";
|
||||
case ("frm"): return "French, Middle (ca. 1400-1600)";
|
||||
case ("fro"): return "French, Old (842- ca. 1400)";
|
||||
case ("fry"): return "Frisian";
|
||||
case ("ful"): return "Fulah";
|
||||
case ("gaa"): return "Ga";
|
||||
case ("gae"): return "Gaelic (Scots)";
|
||||
case ("gdh"): return "Gaelic (Scots)";
|
||||
case ("glg"): return "Gallegan";
|
||||
case ("lug"): return "Ganda";
|
||||
case ("gay"): return "Gayo";
|
||||
case ("gez"): return "Geez";
|
||||
case ("geo"): return "Georgian";
|
||||
case ("kat"): return "Georgian";
|
||||
case ("deu"): return "German";
|
||||
case ("ger"): return "German";
|
||||
case ("gmh"): return "German, Middle High (ca. 1050-1500)";
|
||||
case ("goh"): return "German, Old High (ca. 750-1050)";
|
||||
case ("gem"): return "Germanic (Other)";
|
||||
case ("gil"): return "Gilbertese";
|
||||
case ("gon"): return "Gondi";
|
||||
case ("got"): return "Gothic";
|
||||
case ("grb"): return "Grebo";
|
||||
case ("grc"): return "Greek, Ancient (to 1453)";
|
||||
case ("ell"): return "Greek, Modern (1453-)";
|
||||
case ("gre"): return "Greek, Modern (1453-)";
|
||||
case ("kal"): return "Greenlandic";
|
||||
case ("grn"): return "Guarani";
|
||||
case ("guj"): return "Gujarati";
|
||||
case ("hai"): return "Haida";
|
||||
case ("hau"): return "Hausa";
|
||||
case ("haw"): return "Hawaiian";
|
||||
case ("heb"): return "Hebrew";
|
||||
case ("her"): return "Herero";
|
||||
case ("hil"): return "Hiligaynon";
|
||||
case ("him"): return "Himachali";
|
||||
case ("hin"): return "Hindi";
|
||||
case ("hmo"): return "Hiri Motu";
|
||||
case ("hun"): return "Hungarian";
|
||||
case ("hup"): return "Hupa";
|
||||
case ("iba"): return "Iban";
|
||||
case ("ice"): return "Icelandic";
|
||||
case ("ibo"): return "Igbo";
|
||||
case ("ijo"): return "Ijo";
|
||||
case ("ilo"): return "Iloko";
|
||||
case ("inc"): return "Indic (Other)";
|
||||
case ("ine"): return "Indo-European (Other)";
|
||||
case ("ind"): return "Indonesian";
|
||||
case ("ina"): return "Interlingua (International Auxiliary language Association)";
|
||||
// case ("ine"): return "Interlingue";
|
||||
case ("iku"): return "Inuktitut";
|
||||
case ("ipk"): return "Inupiak";
|
||||
case ("ira"): return "Iranian (Other)";
|
||||
case ("gai"): return "Irish";
|
||||
case ("iri"): return "Irish";
|
||||
case ("sga"): return "Irish, Old (to 900)";
|
||||
case ("mga"): return "Irish, Middle (900 - 1200)";
|
||||
case ("iro"): return "Iroquoian languages";
|
||||
case ("ita"): return "Italian";
|
||||
case ("jpn"): return "Japanese";
|
||||
case ("jav"): return "Javanese";
|
||||
case ("jaw"): return "Javanese";
|
||||
case ("jrb"): return "Judeo-Arabic";
|
||||
case ("jpr"): return "Judeo-Persian";
|
||||
case ("kab"): return "Kabyle";
|
||||
case ("kac"): return "Kachin";
|
||||
case ("kam"): return "Kamba";
|
||||
case ("kan"): return "Kannada";
|
||||
case ("kau"): return "Kanuri";
|
||||
case ("kaa"): return "Kara-Kalpak";
|
||||
case ("kar"): return "Karen";
|
||||
case ("kas"): return "Kashmiri";
|
||||
case ("kaw"): return "Kawi";
|
||||
case ("kaz"): return "Kazakh";
|
||||
case ("kha"): return "Khasi";
|
||||
case ("khm"): return "Khmer";
|
||||
case ("khi"): return "Khoisan (Other)";
|
||||
case ("kho"): return "Khotanese";
|
||||
case ("kik"): return "Kikuyu";
|
||||
case ("kin"): return "Kinyarwanda";
|
||||
case ("kir"): return "Kirghiz";
|
||||
case ("kom"): return "Komi";
|
||||
case ("kon"): return "Kongo";
|
||||
case ("kok"): return "Konkani";
|
||||
case ("kor"): return "Korean";
|
||||
case ("kpe"): return "Kpelle";
|
||||
case ("kro"): return "Kru";
|
||||
case ("kua"): return "Kuanyama";
|
||||
case ("kum"): return "Kumyk";
|
||||
case ("kur"): return "Kurdish";
|
||||
case ("kru"): return "Kurukh";
|
||||
case ("kus"): return "Kusaie";
|
||||
case ("kut"): return "Kutenai";
|
||||
case ("lad"): return "Ladino";
|
||||
case ("lah"): return "Lahnda";
|
||||
case ("lam"): return "Lamba";
|
||||
case ("oci"): return "Langue d'Oc (post 1500)";
|
||||
case ("lao"): return "Lao";
|
||||
case ("lat"): return "Latin";
|
||||
case ("lav"): return "Latvian";
|
||||
case ("ltz"): return "Letzeburgesch";
|
||||
case ("lez"): return "Lezghian";
|
||||
case ("lin"): return "Lingala";
|
||||
case ("lit"): return "Lithuanian";
|
||||
case ("loz"): return "Lozi";
|
||||
case ("lub"): return "Luba-Katanga";
|
||||
case ("lui"): return "Luiseno";
|
||||
case ("lun"): return "Lunda";
|
||||
case ("luo"): return "Luo (Kenya and Tanzania)";
|
||||
case ("mac"): return "Macedonian";
|
||||
case ("mad"): return "Madurese";
|
||||
case ("mag"): return "Magahi";
|
||||
case ("mai"): return "Maithili";
|
||||
case ("mak"): return "Makasar";
|
||||
case ("mlg"): return "Malagasy";
|
||||
case ("may"): return "Malay";
|
||||
case ("msa"): return "Malay";
|
||||
case ("mal"): return "Malayalam";
|
||||
case ("mlt"): return "Maltese";
|
||||
case ("man"): return "Mandingo";
|
||||
case ("mni"): return "Manipuri";
|
||||
case ("mno"): return "Manobo languages";
|
||||
case ("max"): return "Manx";
|
||||
case ("mao"): return "Maori";
|
||||
case ("mri"): return "Maori";
|
||||
case ("mar"): return "Marathi";
|
||||
case ("chm"): return "Mari";
|
||||
case ("mah"): return "Marshall";
|
||||
case ("mwr"): return "Marwari";
|
||||
case ("mas"): return "Masai";
|
||||
case ("myn"): return "Mayan languages";
|
||||
case ("men"): return "Mende";
|
||||
case ("mic"): return "Micmac";
|
||||
case ("min"): return "Minangkabau";
|
||||
case ("mis"): return "Miscellaneous (Other)";
|
||||
case ("moh"): return "Mohawk";
|
||||
case ("mol"): return "Moldavian";
|
||||
case ("mkh"): return "Mon-Kmer (Other)";
|
||||
case ("lol"): return "Mongo";
|
||||
case ("mon"): return "Mongolian";
|
||||
case ("mos"): return "Mossi";
|
||||
case ("mul"): return "Multiple languages";
|
||||
case ("mun"): return "Munda languages";
|
||||
case ("nau"): return "Nauru";
|
||||
case ("nav"): return "Navajo";
|
||||
case ("nde"): return "Ndebele, North";
|
||||
case ("nbl"): return "Ndebele, South";
|
||||
case ("ndo"): return "Ndongo";
|
||||
case ("nep"): return "Nepali";
|
||||
case ("new"): return "Newari";
|
||||
case ("nic"): return "Niger-Kordofanian (Other)";
|
||||
case ("ssa"): return "Nilo-Saharan (Other)";
|
||||
case ("niu"): return "Niuean";
|
||||
case ("non"): return "Norse, Old";
|
||||
case ("nai"): return "North American Indian (Other)";
|
||||
case ("nor"): return "Norwegian";
|
||||
case ("nob"): return "Norwegian (Bokmål)";
|
||||
case ("nno"): return "Norwegian (Nynorsk)";
|
||||
case ("nub"): return "Nubian languages";
|
||||
case ("nym"): return "Nyamwezi";
|
||||
case ("nya"): return "Nyanja";
|
||||
case ("nyn"): return "Nyankole";
|
||||
case ("nyo"): return "Nyoro";
|
||||
case ("nzi"): return "Nzima";
|
||||
case ("oji"): return "Ojibwa";
|
||||
case ("ori"): return "Oriya";
|
||||
case ("orm"): return "Oromo";
|
||||
case ("osa"): return "Osage";
|
||||
case ("oss"): return "Ossetic";
|
||||
case ("oto"): return "Otomian languages";
|
||||
case ("pal"): return "Pahlavi";
|
||||
case ("pau"): return "Palauan";
|
||||
case ("pli"): return "Pali";
|
||||
case ("pam"): return "Pampanga";
|
||||
case ("pag"): return "Pangasinan";
|
||||
case ("pan"): return "Panjabi";
|
||||
case ("pap"): return "Papiamento";
|
||||
case ("paa"): return "Papuan-Australian (Other)";
|
||||
case ("fas"): return "Persian";
|
||||
case ("per"): return "Persian";
|
||||
case ("peo"): return "Persian, Old (ca 600 - 400 B.C.)";
|
||||
case ("phn"): return "Phoenician";
|
||||
case ("pol"): return "Polish";
|
||||
case ("pon"): return "Ponape";
|
||||
case ("por"): return "Portuguese";
|
||||
case ("pra"): return "Prakrit languages";
|
||||
case ("pro"): return "Provencal, Old (to 1500)";
|
||||
case ("pus"): return "Pushto";
|
||||
case ("que"): return "Quechua";
|
||||
case ("roh"): return "Rhaeto-Romance";
|
||||
case ("raj"): return "Rajasthani";
|
||||
case ("rar"): return "Rarotongan";
|
||||
case ("roa"): return "Romance (Other)";
|
||||
case ("ron"): return "Romanian";
|
||||
case ("rum"): return "Romanian";
|
||||
case ("rom"): return "Romany";
|
||||
case ("run"): return "Rundi";
|
||||
case ("rus"): return "Russian";
|
||||
case ("sal"): return "Salishan languages";
|
||||
case ("sam"): return "Samaritan Aramaic";
|
||||
case ("smi"): return "Sami languages";
|
||||
case ("smo"): return "Samoan";
|
||||
case ("sad"): return "Sandawe";
|
||||
case ("sag"): return "Sango";
|
||||
case ("san"): return "Sanskrit";
|
||||
case ("srd"): return "Sardinian";
|
||||
case ("sco"): return "Scots";
|
||||
case ("sel"): return "Selkup";
|
||||
case ("sem"): return "Semitic (Other)";
|
||||
case ("srp"): return "Serbian";
|
||||
case ("scr"): return "Serbo-Croatian";
|
||||
case ("srr"): return "Serer";
|
||||
case ("shn"): return "Shan";
|
||||
case ("sna"): return "Shona";
|
||||
case ("sid"): return "Sidamo";
|
||||
case ("bla"): return "Siksika";
|
||||
case ("snd"): return "Sindhi";
|
||||
case ("sin"): return "Singhalese";
|
||||
case ("sit"): return "Sino-Tibetan (Other)";
|
||||
case ("sio"): return "Siouan languages";
|
||||
case ("sla"): return "Slavic (Other)";
|
||||
//case ("ssw"): return "Siswant";
|
||||
case ("slk"): return "Slovak";
|
||||
case ("slv"): return "Slovenian";
|
||||
case ("sog"): return "Sogdian";
|
||||
case ("som"): return "Somali";
|
||||
case ("son"): return "Songhai";
|
||||
case ("wen"): return "Sorbian languages";
|
||||
case ("nso"): return "Sotho, Northern";
|
||||
case ("sot"): return "Sotho, Southern";
|
||||
case ("sai"): return "South American Indian (Other)";
|
||||
case ("esl"): return "Spanish";
|
||||
case ("spa"): return "Spanish";
|
||||
case ("suk"): return "Sukuma";
|
||||
case ("sux"): return "Sumerian";
|
||||
case ("sun"): return "Sudanese";
|
||||
case ("sus"): return "Susu";
|
||||
case ("swa"): return "Swahili";
|
||||
case ("ssw"): return "Swazi";
|
||||
case ("sve"): return "Swedish";
|
||||
case ("swe"): return "Swedish";
|
||||
case ("syr"): return "Syriac";
|
||||
case ("tgl"): return "Tagalog";
|
||||
case ("tah"): return "Tahitian";
|
||||
case ("tgk"): return "Tajik";
|
||||
case ("tmh"): return "Tamashek";
|
||||
case ("tam"): return "Tamil";
|
||||
case ("tat"): return "Tatar";
|
||||
case ("tel"): return "Telugu";
|
||||
case ("ter"): return "Tereno";
|
||||
case ("tha"): return "Thai";
|
||||
case ("bod"): return "Tibetan";
|
||||
case ("tib"): return "Tibetan";
|
||||
case ("tig"): return "Tigre";
|
||||
case ("tir"): return "Tigrinya";
|
||||
case ("tem"): return "Timne";
|
||||
case ("tiv"): return "Tivi";
|
||||
case ("tli"): return "Tlingit";
|
||||
case ("tog"): return "Tonga (Nyasa)";
|
||||
case ("ton"): return "Tonga (Tonga Islands)";
|
||||
case ("tru"): return "Truk";
|
||||
case ("tsi"): return "Tsimshian";
|
||||
case ("tso"): return "Tsonga";
|
||||
case ("tsn"): return "Tswana";
|
||||
case ("tum"): return "Tumbuka";
|
||||
case ("tur"): return "Turkish";
|
||||
case ("ota"): return "Turkish, Ottoman (1500 - 1928)";
|
||||
case ("tuk"): return "Turkmen";
|
||||
case ("tyv"): return "Tuvinian";
|
||||
case ("twi"): return "Twi";
|
||||
case ("uga"): return "Ugaritic";
|
||||
case ("uig"): return "Uighur";
|
||||
case ("ukr"): return "Ukrainian";
|
||||
case ("umb"): return "Umbundu";
|
||||
case ("und"): return "Undetermined";
|
||||
case ("urd"): return "Urdu";
|
||||
case ("uzb"): return "Uzbek";
|
||||
case ("vai"): return "Vai";
|
||||
case ("ven"): return "Venda";
|
||||
case ("vie"): return "Vietnamese";
|
||||
case ("vol"): return "Volapük";
|
||||
case ("vot"): return "Votic";
|
||||
case ("wak"): return "Wakashan languages";
|
||||
case ("wal"): return "Walamo";
|
||||
case ("war"): return "Waray";
|
||||
case ("was"): return "Washo";
|
||||
case ("cym"): return "Welsh";
|
||||
case ("wel"): return "Welsh";
|
||||
case ("wol"): return "Wolof";
|
||||
case ("xho"): return "Xhosa";
|
||||
case ("sah"): return "Yakut";
|
||||
case ("yao"): return "Yao";
|
||||
case ("yap"): return "Yap";
|
||||
case ("yid"): return "Yiddish";
|
||||
case ("yor"): return "Yoruba";
|
||||
case ("zap"): return "Zapotec";
|
||||
case ("zen"): return "Zenaga";
|
||||
case ("zha"): return "Zhuang";
|
||||
case ("zul"): return "Zulu";
|
||||
case ("zun"): return "Zuni";
|
||||
}
|
||||
return "Any";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
50
src/Logic/Mp4/Boxes/Mdia.cs
Normal file
50
src/Logic/Mp4/Boxes/Mdia.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Nikse.SubtitleEdit.Logic.Mp4.Boxes
|
||||
{
|
||||
public class Mdia
|
||||
{
|
||||
|
||||
public Mdhd Mdhd { get; private set; }
|
||||
public Minf Minf { get; private set; }
|
||||
public readonly string HandlerType = null;
|
||||
|
||||
public Mdia(FileStream fs, long maximumLength)
|
||||
{
|
||||
var buffer = new byte[8];
|
||||
long pos = fs.Position;
|
||||
while (fs.Position < maximumLength)
|
||||
{
|
||||
fs.Seek(pos, SeekOrigin.Begin);
|
||||
int bytesRead = fs.Read(buffer, 0, 8);
|
||||
if (bytesRead < buffer.Length)
|
||||
return;
|
||||
var size = Helper.GetUInt(buffer, 0);
|
||||
string name = Helper.GetString(buffer, 4, 4);
|
||||
|
||||
pos = fs.Position + size - 8;
|
||||
if (name == "minf")
|
||||
{
|
||||
UInt32 timeScale = 90000;
|
||||
if (Mdhd != null)
|
||||
timeScale = Mdhd.TimeScale;
|
||||
Minf = new Minf(fs, pos, timeScale);
|
||||
}
|
||||
else if (name == "hdlr")
|
||||
{
|
||||
|
||||
byte[] b = new byte[size - 4];
|
||||
fs.Read(b, 0, b.Length);
|
||||
HandlerType = Helper.GetString(b, 8, 4);
|
||||
}
|
||||
else if (name == "mdhd")
|
||||
{
|
||||
Mdhd = new Mdhd(fs, size);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
33
src/Logic/Mp4/Boxes/Minf.cs
Normal file
33
src/Logic/Mp4/Boxes/Minf.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Nikse.SubtitleEdit.Logic.Mp4.Boxes
|
||||
{
|
||||
public class Minf
|
||||
{
|
||||
|
||||
public Stbl Stbl { get; private set; }
|
||||
|
||||
public Minf(FileStream fs, long maximumLength, UInt32 timeScale)
|
||||
{
|
||||
var buffer = new byte[8];
|
||||
long pos = fs.Position;
|
||||
while (fs.Position < maximumLength)
|
||||
{
|
||||
fs.Seek(pos, SeekOrigin.Begin);
|
||||
int bytesRead = fs.Read(buffer, 0, 8);
|
||||
if (bytesRead < buffer.Length)
|
||||
return;
|
||||
var size = Helper.GetUInt(buffer, 0);
|
||||
string name = Helper.GetString(buffer, 4, 4);
|
||||
|
||||
pos = fs.Position + size - 8;
|
||||
if (name == "stbl")
|
||||
{
|
||||
Stbl = new Stbl(fs, pos, timeScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
39
src/Logic/Mp4/Boxes/Moov.cs
Normal file
39
src/Logic/Mp4/Boxes/Moov.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Nikse.SubtitleEdit.Logic.Mp4.Boxes
|
||||
{
|
||||
public class Moov
|
||||
{
|
||||
public Mvhd Mvhd { get; private set; }
|
||||
public List<Trak> Tracks { get; private set; }
|
||||
|
||||
public Moov(FileStream fs, long maximumLength)
|
||||
{
|
||||
Tracks = new List<Trak>();
|
||||
|
||||
var buffer = new byte[8];
|
||||
long pos = fs.Position;
|
||||
while (fs.Position < maximumLength)
|
||||
{
|
||||
fs.Seek(pos, SeekOrigin.Begin);
|
||||
int bytesRead = fs.Read(buffer, 0, 8);
|
||||
if (bytesRead < buffer.Length)
|
||||
return;
|
||||
var size = Helper.GetUInt(buffer, 0);
|
||||
string name = Helper.GetString(buffer, 4, 4);
|
||||
|
||||
pos = fs.Position + size - 8;
|
||||
if (name == "trak")
|
||||
{
|
||||
Tracks.Add(new Trak(fs, pos));
|
||||
}
|
||||
else if (name == "mvhd")
|
||||
{
|
||||
Mvhd = new Mvhd(fs, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
src/Logic/Mp4/Boxes/Mvhd.cs
Normal file
18
src/Logic/Mp4/Boxes/Mvhd.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
|
||||
namespace Nikse.SubtitleEdit.Logic.Mp4.Boxes
|
||||
{
|
||||
public class Mvhd
|
||||
{
|
||||
|
||||
public Mvhd(FileStream fs, long maximumLength)
|
||||
{
|
||||
var buffer = new byte[8];
|
||||
long pos = fs.Position;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
108
src/Logic/Mp4/Boxes/Stbl.cs
Normal file
108
src/Logic/Mp4/Boxes/Stbl.cs
Normal file
@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Nikse.SubtitleEdit.Logic.Mp4.Boxes
|
||||
{
|
||||
public class Stbl
|
||||
{
|
||||
|
||||
public readonly List<string> Texts = new List<string>();
|
||||
public readonly List<double> StartTimeCodes = new List<double>();
|
||||
public readonly List<double> EndTimeCodes = new List<double>();
|
||||
|
||||
public Stbl(FileStream fs, long maximumLength, UInt32 timeScale)
|
||||
{
|
||||
var buffer = new byte[8];
|
||||
long pos = fs.Position;
|
||||
while (fs.Position < maximumLength)
|
||||
{
|
||||
fs.Seek(pos, SeekOrigin.Begin);
|
||||
int bytesRead = fs.Read(buffer, 0, 8);
|
||||
if (bytesRead < buffer.Length)
|
||||
return;
|
||||
var size = Helper.GetUInt(buffer, 0);
|
||||
string name = Helper.GetString(buffer, 4, 4);
|
||||
|
||||
pos = fs.Position + size - 8;
|
||||
|
||||
|
||||
if (name == "stco") // 32-bit
|
||||
{
|
||||
byte[] b = new byte[size - 4];
|
||||
fs.Read(b, 0, b.Length);
|
||||
int version = b[0];
|
||||
uint totalEntries = Helper.GetUInt(b, 4);
|
||||
|
||||
uint lastOffset = 0;
|
||||
for (int i = 0; i < totalEntries; i++)
|
||||
{
|
||||
uint offset = Helper.GetUInt(b, 8 + i * 4);
|
||||
if (lastOffset + 5 < offset)
|
||||
{
|
||||
fs.Seek(offset, SeekOrigin.Begin);
|
||||
byte[] data = new byte[150];
|
||||
fs.Read(data, 0, data.Length);
|
||||
uint textSize = Helper.GetUInt(data, 0);
|
||||
if (textSize < data.Length - 4)
|
||||
{
|
||||
string text = Helper.GetString(data, 4, (int)textSize - 1);
|
||||
Texts.Add(text);
|
||||
}
|
||||
}
|
||||
lastOffset = offset;
|
||||
}
|
||||
}
|
||||
else if (name == "co64") // 64-bit
|
||||
{
|
||||
// Console.WriteLine("UNABLE TO HANDLE 64 bit!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
|
||||
return;
|
||||
}
|
||||
else if (name == "stsz") // sample sizes
|
||||
{
|
||||
byte[] b = new byte[size - 4];
|
||||
fs.Read(b, 0, b.Length);
|
||||
int version = b[0];
|
||||
uint uniformSizeOfEachSample = Helper.GetUInt(b, 4);
|
||||
uint numberOfSampleSizes = Helper.GetUInt(b, 8);
|
||||
for (int i = 0; i < numberOfSampleSizes; i++)
|
||||
{
|
||||
uint sampleSize = Helper.GetUInt(b, 12 + i * 4);
|
||||
}
|
||||
}
|
||||
else if (name == "stts") // sample table time to sample map
|
||||
{
|
||||
byte[] b = new byte[size - 4];
|
||||
fs.Read(b, 0, b.Length);
|
||||
int version = b[0];
|
||||
uint numberOfSampleTimes = Helper.GetUInt(b, 4);
|
||||
double totalTime = 0;
|
||||
for (int i = 0; i < numberOfSampleTimes; i++)
|
||||
{
|
||||
uint sampleCount = Helper.GetUInt(b, 8 + i * 8);
|
||||
uint sampleDelta = Helper.GetUInt(b, 12 + i * 8);
|
||||
totalTime += (double)(sampleDelta / (double)timeScale);
|
||||
if (StartTimeCodes.Count <= EndTimeCodes.Count)
|
||||
StartTimeCodes.Add(totalTime);
|
||||
else
|
||||
EndTimeCodes.Add(totalTime);
|
||||
}
|
||||
}
|
||||
else if (name == "stsc") // sample table sample to chunk map
|
||||
{
|
||||
byte[] b = new byte[size - 4];
|
||||
fs.Read(b, 0, b.Length);
|
||||
int version = b[0];
|
||||
uint numberOfSampleTimes = Helper.GetUInt(b, 4);
|
||||
for (int i = 0; i < numberOfSampleTimes; i++)
|
||||
{
|
||||
uint firstChunk = Helper.GetUInt(b, 8 + i * 12);
|
||||
uint samplesPerChunk = Helper.GetUInt(b, 12 + i * 12);
|
||||
uint sampleDescriptionIndex = Helper.GetUInt(b, 16 + i * 12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
10
src/Logic/Mp4/Boxes/Tkhd.cs
Normal file
10
src/Logic/Mp4/Boxes/Tkhd.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Nikse.SubtitleEdit.Logic.Mp4.Boxes
|
||||
{
|
||||
public class Tkhd
|
||||
{
|
||||
}
|
||||
}
|
32
src/Logic/Mp4/Boxes/Trak.cs
Normal file
32
src/Logic/Mp4/Boxes/Trak.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Nikse.SubtitleEdit.Logic.Mp4.Boxes
|
||||
{
|
||||
public class Trak
|
||||
{
|
||||
|
||||
public Mdia Mdia { get; private set; }
|
||||
|
||||
public Trak(FileStream fs, long maximumLength)
|
||||
{
|
||||
var buffer = new byte[8];
|
||||
long pos = fs.Position;
|
||||
while (fs.Position < maximumLength)
|
||||
{
|
||||
fs.Seek(pos, SeekOrigin.Begin);
|
||||
int bytesRead = fs.Read(buffer, 0, 8);
|
||||
if (bytesRead < buffer.Length)
|
||||
return;
|
||||
var size = Helper.GetUInt(buffer, 0);
|
||||
string name = Helper.GetString(buffer, 4, 4);
|
||||
|
||||
pos = fs.Position + size - 8;
|
||||
if (name == "mdia")
|
||||
{
|
||||
Mdia = new Mdia(fs, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
77
src/Logic/Mp4/Mp4Parser.cs
Normal file
77
src/Logic/Mp4/Mp4Parser.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Nikse.SubtitleEdit.Logic.Mp4.Boxes;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Nikse.SubtitleEdit.Logic.Mp4
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// http://wiki.multimedia.cx/index.php?title=QuickTime_container
|
||||
/// </summary>
|
||||
public class Mp4Parser
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
public string FileName { get; private set; }
|
||||
public Moov Moov { get; private set; }
|
||||
|
||||
public List<Trak> GetSubtitleTracks()
|
||||
{
|
||||
var list = new List<Trak>();
|
||||
if (Moov != null && Moov.Tracks != null)
|
||||
{
|
||||
foreach (var trak in Moov.Tracks)
|
||||
{
|
||||
if (trak.Mdia != null && trak.Mdia.HandlerType == "sbtl" && trak.Mdia.Minf != null && trak.Mdia.Minf.Stbl != null)
|
||||
{
|
||||
list.Add(trak);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public Mp4Parser(string fileName)
|
||||
{
|
||||
FileName = fileName;
|
||||
FileStream fs = new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
ParseMp4(fs);
|
||||
fs.Close();
|
||||
}
|
||||
|
||||
public Mp4Parser(FileStream fs)
|
||||
{
|
||||
FileName = null;
|
||||
ParseMp4(fs);
|
||||
}
|
||||
|
||||
private void ParseMp4(FileStream fs)
|
||||
{
|
||||
int count = 0;
|
||||
var buffer = new byte[8];
|
||||
long pos = 0;
|
||||
int bytesRead = 8;
|
||||
while (bytesRead == 8)
|
||||
{
|
||||
fs.Seek(pos, SeekOrigin.Begin);
|
||||
bytesRead = fs.Read(buffer, 0, 8);
|
||||
var size = Helper.GetUInt(buffer, 0);
|
||||
string name = Helper.GetString(buffer, 4, 4);
|
||||
pos = fs.Position + size - 8;
|
||||
if (name == "moov")
|
||||
{
|
||||
Moov = new Moov(fs, pos);
|
||||
}
|
||||
count++;
|
||||
if (count > 100)
|
||||
break;
|
||||
}
|
||||
fs.Close();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -527,6 +527,16 @@
|
||||
<Compile Include="Logic\Fourier.cs" />
|
||||
<Compile Include="Logic\IfoParser.cs" />
|
||||
<Compile Include="Logic\Language.cs" />
|
||||
<Compile Include="Logic\Mp4\Boxes\Helper.cs" />
|
||||
<Compile Include="Logic\Mp4\Boxes\Mdhd.cs" />
|
||||
<Compile Include="Logic\Mp4\Boxes\Mdia.cs" />
|
||||
<Compile Include="Logic\Mp4\Boxes\Minf.cs" />
|
||||
<Compile Include="Logic\Mp4\Boxes\Moov.cs" />
|
||||
<Compile Include="Logic\Mp4\Boxes\Mvhd.cs" />
|
||||
<Compile Include="Logic\Mp4\Boxes\Stbl.cs" />
|
||||
<Compile Include="Logic\Mp4\Boxes\Tkhd.cs" />
|
||||
<Compile Include="Logic\Mp4\Boxes\Trak.cs" />
|
||||
<Compile Include="Logic\Mp4\Mp4Parser.cs" />
|
||||
<Compile Include="Logic\Networking\NikseWebServiceSession.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
|
Loading…
Reference in New Issue
Block a user