背景
「B-LIVE」というライブ配信サイトに、コメントビューアーが欲しいという要望があり、開発してみることに。
1から作り上げるのはしんどいので、ソースコードが公開されているコメントビューアーを改造する形にした。
選ばれたのはマルチコメントビューアーでした。
マルチコメントビューアーを選んだ理由
・ソースコードが公開されている
・利用ユーザーが多そう
・古代からある
・対応サイトも結構多い
結論から言うと、対応は出来た。
・開発時間:40時間
・ソースコード:こちらからダウンロードできます。
・問題点:開発元のRyu氏が公式に公開してくれるのか?
ソースコード解析
配布元
MultiCommentViewer
GitHub - CommentViewerCollection/MultiCommentViewer: いろんな配信サイトのコメントを表示できるコメビュです
いろんな配信サイトのコメントを表示できるコメビュです. Contribute to CommentViewerCollection/MultiCommentViewer development by creating an account ...
OPENREC のプラグインの解析
サイトごとの設定ファイルが複数あるが、とりあえずOPENRECのソースコードを解析していく
ファイル構成
まずは全体のファイル構成
※主要そうな部分のみ抜粋
MultiCommentViewer/
│
├MultiCommentViewer/
│ ├ViewModels/
│ ├ConnectionViewModel.cs
│ ├Views/
│ ├MainWindow.xaml
│
├OpenrecIF/
│ ├Message.cs/
│ └OpenrecIF.csproj
│
├OpenrecSitePlugin
│ ├API.cs
│ ├CommentPostPanel.xaml
│ ├CommentProvider.cs
│ ├IOpenrecWebsocket.cs
│ ├OpenrecOptionsPanel.xaml
│ ├OpenrecSitePlugin.csproj
│ ├OpenrecWebsocket.cs
│ ├Packet.cs
│ ├Tools.cs
│ └Websocket.cs
│
├OpenrecSitePluginTests
│
├MultiCommentViewer.sln
MultiCommentViewer.sln
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31825.309
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiCommentViewer", "MultiCommentViewer\MultiCommentViewer.csproj", "{1CAA4971-6CB9-4FB5-AC5E-BFC6BED83C87}"
ProjectSection(ProjectDependencies) = postProject
{88B68D39-41A4-4C10-B942-0F3BE976D7A3} = {88B68D39-41A4-4C10-B942-0F3BE976D7A3}
{A39D3546-FB5F-41FC-AAE0-2AC1B5C61ECF} = {A39D3546-FB5F-41FC-AAE0-2AC1B5C61ECF}
{5C5D8468-1FA6-44DB-B196-003164FBB91A} = {5C5D8468-1FA6-44DB-B196-003164FBB91A}
{EF2FABAC-9D97-4FFC-8FEE-B4FBDEA0CF34} = {EF2FABAC-9D97-4FFC-8FEE-B4FBDEA0CF34}
{A04C3DC9-78CB-4DB2-B8BA-0D462A64314F} = {A04C3DC9-78CB-4DB2-B8BA-0D462A64314F}
{4098E8D6-7954-4B24-96C9-1CFC6BC8A654} = {4098E8D6-7954-4B24-96C9-1CFC6BC8A654}
{13A909DD-3791-4539-9C04-F72D9E755DCF} = {13A909DD-3791-4539-9C04-F72D9E755DCF}
{8C6AB9F7-E1C9-4B2A-9F17-E51E32EA43CC} = {8C6AB9F7-E1C9-4B2A-9F17-E51E32EA43CC}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenrecSitePlugin", "OpenrecSitePlugin\OpenrecSitePlugin.csproj", "{B56F2F0D-197A-4A29-A29A-1F92ECD81902}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenrecSitePluginTests", "OpenrecSitePluginTests\OpenrecSitePluginTests.csproj", "{8C6AB9F7-E1C9-4B2A-9F17-E51E32EA43CC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenrecIF", "OpenrecIF\OpenrecIF.csproj", "{02A55625-E735-4909-A470-E8405EEE1A32}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Alpha|Any CPU = Alpha|Any CPU
Beta|Any CPU = Beta|Any CPU
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
EndGlobal
OpenrecIF.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net462</TargetFramework>
<LangVersion>8.0</LangVersion>
<Configurations>Release;Beta;Alpha;Debug</Configurations>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<WarningLevel>4</WarningLevel>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Beta|AnyCPU'">
<OutputPath>bin\Beta\</OutputPath>
<DefineConstants>TRACE;BETA</DefineConstants>
<WarningLevel>4</WarningLevel>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Alpha|AnyCPU'">
<DefineConstants>TRACE;DEBUG;ALPHA</DefineConstants>
<WarningLevel>4</WarningLevel>
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
<Optimize>false</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
<WarningLevel>4</WarningLevel>
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ISitePlugin\SitePlugin.csproj" />
</ItemGroup>
</Project>
OpenrecSitePlugin.csproj
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<UseWPF>true</UseWPF>
<TargetFramework>net462</TargetFramework>
<LangVersion>8.0</LangVersion>
<Configurations>Release;Beta;Alpha;Debug</Configurations>
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<WarningLevel>4</WarningLevel>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Beta|AnyCPU'">
<OutputPath>bin\Beta\</OutputPath>
<DefineConstants>TRACE;BETA</DefineConstants>
<WarningLevel>4</WarningLevel>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Alpha|AnyCPU'">
<DefineConstants>TRACE;DEBUG;ALPHA</DefineConstants>
<WarningLevel>4</WarningLevel>
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
<Optimize>false</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
<WarningLevel>4</WarningLevel>
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="System.Resources.Extensions" Version="4.6.0" />
<PackageReference Include="System.ServiceModel.Primitives" Version="4.8.1" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BrowserCookieInterfaces\BrowserCookieInterfaces.csproj" />
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\ISitePlugin\SitePlugin.csproj" />
<ProjectReference Include="..\OpenrecIF\OpenrecIF.csproj" />
<ProjectReference Include="..\SitePluginCommon\SitePluginCommon.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.CSharp" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Web" />
<Reference Include="System.Windows" />
<Reference Include="System.Xaml" />
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>
MainWindow.xaml
<Window x:Class="MultiCommentViewer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MultiCommentViewer"
xmlns:c="clr-namespace:Common;assembly=Common"
xmlns:w="clr-namespace:Common.Wpf;assembly=Common"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Platform"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance local:MainViewModel, IsDesignTimeCreatable=True}"
d:DesignWidth="1000"
Title="{Binding Title}" Topmost="{Binding Topmost, Mode=TwoWay}"
Height="{Binding MainViewHeight, Mode=TwoWay}" Width="{Binding MainViewWidth, Mode=TwoWay}"
Left="{Binding MainViewLeft, Mode=TwoWay}" Top="{Binding MainViewTop, Mode=TwoWay}"
>
<Grid>
<DataGridTemplateColumn
Header="サイト"
DisplayIndex="{Binding DataContext.ConnectionsViewSiteDisplayIndex, Mode=TwoWay, Source={x:Reference dummyElement}, FallbackValue=1}"
Width="{Binding DataContext.ConnectionsViewSiteWidth, Mode=TwoWay, Source={x:Reference dummyElement}, Converter={StaticResource dataGridLengthConverter}}"
Visibility="{Binding DataContext.IsShowConnectionsViewSite, Source={x:Reference dummyElement}, Converter={StaticResource booleanToVisibilityConverter}}"
>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox BorderBrush="Transparent" Margin="5,0" ItemsSource="{Binding Sites}" SelectedValue="{Binding SelectedSite, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="DisplayName" IsEnabled="{Binding CanConnect}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn
Header="名前"
DisplayIndex="{Binding DataContext.ConnectionsViewConnectionNameDisplayIndex, Mode=TwoWay, Source={x:Reference dummyElement}, FallbackValue=2}"
Width="{Binding DataContext.ConnectionsViewConnectionNameWidth, Mode=TwoWay, Source={x:Reference dummyElement}, Converter={StaticResource dataGridLengthConverter}}"
Visibility="{Binding DataContext.IsShowConnectionsViewConnectionName, Source={x:Reference dummyElement}, Converter={StaticResource booleanToVisibilityConverter}}"
>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox BorderBrush="Transparent" Margin="5,0" Text="{Binding Name, UpdateSourceTrigger=LostFocus}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn
Header="URL,放送ID等"
DisplayIndex="{Binding DataContext.ConnectionsViewInputDisplayIndex, Mode=TwoWay, Source={x:Reference dummyElement}, FallbackValue=3}"
Width="{Binding DataContext.ConnectionsViewInputWidth, Mode=TwoWay, Source={x:Reference dummyElement}, Converter={StaticResource dataGridLengthConverter}}"
Visibility="{Binding DataContext.IsShowConnectionsViewInput, Source={x:Reference dummyElement}, Converter={StaticResource booleanToVisibilityConverter}}"
>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Margin="5,0" Text="{Binding Input, UpdateSourceTrigger=PropertyChanged}">
<TextBox.ToolTip>
<TextBlock Text="{Binding Input}" />
</TextBox.ToolTip>
</TextBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn
Header="ブラウザ"
DisplayIndex="{Binding DataContext.ConnectionsViewBrowserDisplayIndex, Mode=TwoWay, Source={x:Reference dummyElement}, FallbackValue=4}"
Width="{Binding DataContext.ConnectionsViewBrowserWidth, Mode=TwoWay, Source={x:Reference dummyElement}, Converter={StaticResource dataGridLengthConverter}}"
Visibility="{Binding DataContext.IsShowConnectionsViewBrowser, Source={x:Reference dummyElement}, Converter={StaticResource booleanToVisibilityConverter}}"
>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox Margin="5,0" ItemsSource="{Binding Browsers}" SelectedValue="{Binding SelectedBrowser, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="DisplayName" IsEnabled="{Binding CanConnect}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn
Header="接続"
DisplayIndex="{Binding DataContext.ConnectionsViewConnectionDisplayIndex, Mode=TwoWay, Source={x:Reference dummyElement}, FallbackValue=5}"
Width="{Binding DataContext.ConnectionsViewConnectionWidth, Mode=TwoWay, Source={x:Reference dummyElement}, Converter={StaticResource dataGridLengthConverter}}"
Visibility="{Binding DataContext.IsShowConnectionsViewConnection, Source={x:Reference dummyElement}, Converter={StaticResource booleanToVisibilityConverter}}"
>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Margin="5,0" Content="接続" IsEnabled="{Binding CanConnect}" Command="{Binding ConnectCommand}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn
Header="切断"
DisplayIndex="{Binding DataContext.ConnectionsViewDisconnectionDisplayIndex, Mode=TwoWay, Source={x:Reference dummyElement}, FallbackValue=6}"
Width="{Binding DataContext.ConnectionsViewDisconnectionWidth, Mode=TwoWay, Source={x:Reference dummyElement}, Converter={StaticResource dataGridLengthConverter}}"
Visibility="{Binding DataContext.IsShowConnectionsViewDisconnection, Source={x:Reference dummyElement}, Converter={StaticResource booleanToVisibilityConverter}}"
>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Margin="5,0" Content="切断" IsEnabled="{Binding CanDisconnect}" Command="{Binding DisconnectCommand}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</Grid>
</Window>
CommentPostPanel.xaml
<UserControl x:Class="OpenrecSitePlugin.CommentPostPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:OpenrecSitePlugin"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance local:CommentPostPanelViewModel, IsDesignTimeCreatable=True}"
d:DesignHeight="300" d:DesignWidth="400"
Height="50" Width="400">
<Grid>
<TextBox Text="{Binding Input}" IsEnabled="{Binding CanPostComment}" HorizontalAlignment="Stretch" Height="23" Margin="5,0,45,5" TextWrapping="Wrap" VerticalAlignment="Bottom"/>
<Button Content="投稿" IsEnabled="{Binding CanPostComment}" HorizontalAlignment="Right" Margin="0,0,5,5" VerticalAlignment="Bottom" Width="35"/>
</Grid>
</UserControl>
OpenrecOptionsPanel.xaml
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:OpenrecSitePlugin"
xmlns:Common="clr-namespace:Common;assembly=Common" x:Class="OpenrecSitePlugin.OpenrecOptionsPanel"
mc:Ignorable="d" Height="290" Width="420">
<Grid>
<Common:NumericUpDown Value="{Binding StampSize,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Minimum="1" HorizontalAlignment="Left" Height="30" Margin="94,20,0,0" VerticalAlignment="Top" Width="90"/>
<Label Content="スタンプのサイズ" HorizontalAlignment="Left" Margin="10,22,0,0" VerticalAlignment="Top"/>
<CheckBox x:Name="checkBoxStampMusic" Content="スタンプが貼られた時に音を鳴らす(現状、waveファイルのみ対応)" IsChecked="{Binding IsPlayStampMusic}" HorizontalAlignment="Left" Margin="10,70,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.352,0.333"/>
<Button Content="開く" HorizontalAlignment="Left" IsEnabled="{Binding IsChecked, ElementName=checkBoxStampMusic}" Command="{Binding ShowOpenStampMusicSelectorCommand}" Margin="322,91,0,0" VerticalAlignment="Top" Width="53"/>
<TextBox HorizontalAlignment="Left" IsEnabled="{Binding IsChecked, ElementName=checkBoxStampMusic}" Height="23" Margin="56,91,0,0" TextWrapping="NoWrap" Text="{Binding StampMusicFilePath}" VerticalAlignment="Top" Width="261"/>
<CheckBox x:Name="checkBoxYellMusic" Content="エールが送られた時に音を鳴らす(現状、waveファイルのみ対応)" IsChecked="{Binding IsPlayYellMusic}" HorizontalAlignment="Left" Margin="10,119,0,0" VerticalAlignment="Top"/>
<Button Content="開く" HorizontalAlignment="Left" IsEnabled="{Binding IsChecked, ElementName=checkBoxYellMusic}" Command="{Binding ShowOpenYellMusicSelectorCommand}" Margin="322,142,0,0" VerticalAlignment="Top" Width="53" RenderTransformOrigin="0.453,1.4"/>
<TextBox HorizontalAlignment="Left" IsEnabled="{Binding IsChecked, ElementName=checkBoxYellMusic}" Height="23" Margin="56,139,0,0" TextWrapping="Wrap" Text="{Binding YellMusicFilePath}" VerticalAlignment="Top" Width="261"/>
<CheckBox Content="@のあとの文字列を自動的にコテハンとして登録する" IsChecked="{Binding IsAutoSetNickname}" HorizontalAlignment="Left" Margin="10,180,0,0" VerticalAlignment="Top"/>
</Grid>
</UserControl>
ConnectionViewModel.cs
namespace MultiCommentViewer
{
public class ConnectionContext
{
public ConnectionName ConnectionName { get; set; }
public ICommentProvider CommentProvider { get; set; }
public Guid SiteGuid { get; set; }
}
public class ConnectionViewModel : ViewModelBase, IConnectionStatus
{
public ObservableCollection<SiteViewModel> Sites { get; }
public ObservableCollection<BrowserViewModel> Browsers { get; }
private SiteViewModel _selectedSite;
private ICommentProvider _commentProvider = null;
private readonly ConnectionName _connectionName;
public ICommand ConnectCommand { get; }
public ICommand DisconnectCommand { get; }
public event EventHandler<SelectedSiteChangedEventArgs> SelectedSiteChanged;
private ConnectionContext _beforeContext;
private ConnectionContext _currentContext;
public SiteViewModel SelectedSite
{
get { return _selectedSite; }
set
{
if (_selectedSite == value)
return;
//一番最初は_commentProviderはnull
var before = _commentProvider;
if (before != null)
{
Debug.Assert(before.CanConnect, "接続中に変更はできない");
before.CanConnectChanged -= CommentProvider_CanConnectChanged;
before.CanDisconnectChanged -= CommentProvider_CanDisconnectChanged;
before.MessageReceived -= CommentProvider_MessageReceived;
before.MetadataUpdated -= CommentProvider_MetadataUpdated;
before.Connected -= CommentProvider_Connected;
}
_selectedSite = value;
var nextGuid = _selectedSite.Guid;
var next = _commentProvider = _sitePluginLoader.CreateCommentProvider(nextGuid);
next.CanConnectChanged += CommentProvider_CanConnectChanged;
next.CanDisconnectChanged += CommentProvider_CanDisconnectChanged;
next.MessageReceived += CommentProvider_MessageReceived;
next.MetadataUpdated += CommentProvider_MetadataUpdated;
next.Connected += CommentProvider_Connected;
UpdateLoggedInInfo();
System.Windows.Controls.UserControl commentPanel;
try
{
commentPanel = _sitePluginLoader.GetCommentPostPanel(nextGuid, next);
}
catch (Exception ex)
{
_logger.LogException(ex);
commentPanel = null;
}
CommentPostPanel = commentPanel;
_beforeContext = _currentContext;
_currentContext = new ConnectionContext
{
ConnectionName = this.ConnectionName,
CommentProvider = next,
SiteGuid = nextGuid,
//SiteContext = _selectedSite.Site,
};
RaisePropertyChanged();
SelectedSiteChanged?.Invoke(this, new SelectedSiteChangedEventArgs
{
ConnectionName = this.ConnectionName,
OldValue = _beforeContext,
NewValue = _currentContext
});
}
}
private async void Connect()
{
try
{
//接続中は削除できないように選択を外す
IsSelected = false;
var input = Input;
var browser = SelectedBrowser.Browser;
await _commentProvider.ConnectAsync(input, browser);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
_logger.LogException(ex);
}
}
public ConnectionViewModel(ConnectionName connectionName, IEnumerable<SiteViewModel> sites, IEnumerable<BrowserViewModel> browsers, ILogger logger, ISitePluginLoader sitePluginLoader, IOptions options)
{
_dispatcher = Dispatcher.CurrentDispatcher;
Guid = Guid.NewGuid();
_logger = logger;
_sitePluginLoader = sitePluginLoader;
_options = options;
_connectionName = connectionName ?? throw new ArgumentNullException(nameof(connectionName));
_beforeName = _connectionName.Name;
_connectionName.PropertyChanged += (s, e) =>
{
switch (e.PropertyName)
{
case nameof(_connectionName.Name):
var newName = _connectionName.Name;
Renamed?.Invoke(this, new RenamedEventArgs(_beforeName, newName));
_beforeName = newName;
RaisePropertyChanged(nameof(Name));
break;
}
};
options.PropertyChanged += (s, e) =>
{
switch (e.PropertyName)
{
case nameof(options.IsEnabledSiteConnectionColor):
RaisePropertyChanged(nameof(BackColor));
RaisePropertyChanged(nameof(ForeColor));
break;
case nameof(options.SiteConnectionColorType):
RaisePropertyChanged(nameof(BackColor));
RaisePropertyChanged(nameof(ForeColor));
break;
}
};
if (sites == null)
{
throw new ArgumentNullException(nameof(sites));
}
Sites = new ObservableCollection<SiteViewModel>();
foreach (var siteVm in sites)
{
_siteVmDict.Add(siteVm.Guid, siteVm);
Sites.Add(siteVm);
}
//Sites = new ObservableCollection<SiteViewModel>(sites);
if (Sites.Count > 0)
{
SelectedSite = Sites[0];
}
Browsers = new ObservableCollection<BrowserViewModel>(browsers);
if (Browsers.Count > 0)
{
SelectedBrowser = Browsers[0];
}
ConnectCommand = new RelayCommand(Connect);
DisconnectCommand = new RelayCommand(Disconnect);
}
}
}
CommentProvider.cs
namespace OpenrecSitePlugin
{
[Serializable]
public class InvalidInputException : Exception
{
public InvalidInputException() { }
}
class CommentProvider : ICommentProvider
{
string _liveId;
Context _context;
#region ICommentProvider
#region Events
public event EventHandler<IMetadata> MetadataUpdated;
public event EventHandler CanConnectChanged;
public event EventHandler CanDisconnectChanged;
public event EventHandler<ConnectedEventArgs> Connected;
public event EventHandler<IMessageContext> MessageReceived;
#endregion //Events
#region CanConnect
private bool _canConnect;
public bool CanConnect
{
get { return _canConnect; }
set
{
if (_canConnect == value)
return;
_canConnect = value;
CanConnectChanged?.Invoke(this, EventArgs.Empty);
}
}
protected virtual Task<string> GetLiveId(string input)
{
return Tools.GetLiveId(_dataSource, input);
}
//protected virtual Extract()
private async Task ConnectInternalAsync(string input, IBrowserProfile browserProfile)
{
if (_ws != null)
{
throw new InvalidOperationException("");
}
var cookies = GetCookies(browserProfile);
_cc = CreateCookieContainer(cookies);
_context = Tools.GetContext(cookies);
string liveId;
try
{
liveId = await GetLiveId(input);
_liveId = liveId;
}
catch (InvalidInputException ex)
{
_logger.LogException(ex, "無効な入力値", $"input={input}");
SendSystemInfo("無効な入力値です", InfoType.Error);
AfterDisconnected();
return;
}
var movieContext2 = await GetMovieInfo(liveId);
var movieId = movieContext2.MovieId;
if (movieId == 0)
{
SendSystemInfo("存在しないURLまたはIDです", InfoType.Error);
AfterDisconnected();
return;
}
if (movieContext2.OnairStatus == 2)
{
SendSystemInfo("この放送は終了しています", InfoType.Error);
AfterDisconnected();
return;
}
MetadataUpdated?.Invoke(this, new Metadata { Title = movieContext2.Title });
_startAt = movieContext2.StartedAt.DateTime;
_500msTimer.Enabled = true;
var (chats, raw) = await GetChats(movieContext2);
try
{
foreach (var item in chats)
{
var comment = Tools.Parse(item);
var commentData = Tools.CreateCommentData(comment, _startAt, _siteOptions);
var messageContext = CreateMessageContext(comment, commentData, true);
if (messageContext != null)
{
MessageReceived?.Invoke(this, messageContext);
}
}
}
catch (Exception ex)
{
_logger.LogException(ex, "", "raw=" + raw);
}
foreach (var user in _userStoreManager.GetAllUsers(SiteType.Openrec))
{
if (!(user is IUser2 user2)) continue;
_userDict.AddOrUpdate(user2.UserId, user2, (id, u) => u);
}
Reconnect:
_ws = CreateOpenrecWebsocket();
_ws.Received += WebSocket_Received;
var userAgent = GetUserAgent(browserProfile.Type);
var wsTask = _ws.ReceiveAsync(movieId.ToString(), userAgent, cookies);
var blackListProvider = CreateBlacklistProvider();
blackListProvider.Received += BlackListProvider_Received;
var blackTask = blackListProvider.ReceiveAsync(movieId.ToString(), _context);
var tasks = new List<Task>
{
wsTask,
blackTask
};
while (tasks.Count > 0)
{
var t = await Task.WhenAny(tasks);
if (t == blackTask)
{
try
{
await blackTask;
}
catch (Exception ex)
{
_logger.LogException(ex);
}
tasks.Remove(blackTask);
}
else
{
blackListProvider.Disconnect();
try
{
await blackTask;
}
catch (Exception ex)
{
_logger.LogException(ex);
}
tasks.Remove(blackTask);
SendSystemInfo("ブラックリストタスク終了", InfoType.Debug);
try
{
await wsTask;
}
catch (Exception ex)
{
_logger.LogException(ex);
}
tasks.Remove(wsTask);
SendSystemInfo("wsタスク終了", InfoType.Debug);
}
}
_ws.Received -= WebSocket_Received;
blackListProvider.Received -= BlackListProvider_Received;
//意図的な切断では無い場合、配信がまだ続いているか確認して、配信中だったら再接続する。
//2019/03/12 heartbeatを送っているのにも関わらずwebsocketが切断されてしまう場合を確認。ブラウザでも配信中に切断されて再接続するのを確認済み。
if (!_isExpectedDisconnect)
{
var movieInfo = await GetMovieInfo(liveId);
if (movieInfo.OnairStatus == 1)
{
goto Reconnect;
}
}
}
public async Task ConnectAsync(string input, IBrowserProfile browserProfile)
{
BeforeConnecting();
try
{
await ConnectInternalAsync(input, browserProfile);
}
finally
{
AfterDisconnected();
}
}
private void WebSocket_Received(object sender, IPacket e)
{
try
{
if (e is PacketMessageEventMessageChat chat)
{
var comment = Tools.Parse(chat.Comment);
var commentData = Tools.CreateCommentData(comment, _startAt, _siteOptions);
var messageContext = CreateMessageContext(comment, commentData, false);
if (messageContext != null)
{
MessageReceived?.Invoke(this, messageContext);
}
}
else if (e is PacketMessageEventMessageAudienceCount audienceCount)
{
var ac = audienceCount.AudienceCount;
MetadataUpdated?.Invoke(this, new Metadata
{
CurrentViewers = ac.live_viewers.ToString(),
TotalViewers = ac.viewers.ToString(),
});
}
else if (e is PacketMessageEventMessageLiveEnd liveEnd)
{
Disconnect();
}
}
catch (Exception ex)
{
_logger.LogException(ex);
}
}
}
}
Websocket.cs
using System;
using System.Threading.Tasks;
using System.Threading;
using WebSocket4Net;
using System.Diagnostics;
using System.Collections.Generic;
namespace OpenrecSitePlugin
{
class Websocket
{
public event EventHandler Opened;
public event EventHandler<string> Received;
WebSocket _ws;
private void Log(string str)
{
Debug.Write(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"));
Debug.WriteLine(str);
}
public Task ReceiveAsync(string url, List<KeyValuePair<string, string>> cookies, string userAgent, string origin)
{
if (_ws != null)
throw new InvalidOperationException("_ws is not null");
var tcs = new TaskCompletionSource<object>();
var subProtocol = "";
_ws = new WebSocket4Net.WebSocket(url, subProtocol, cookies, null, userAgent, origin, WebSocket4Net.WebSocketVersion.Rfc6455)
{
EnableAutoSendPing = false,
AutoSendPingInterval = 0,
ReceiveBufferSize = 8192,
NoDelay = true
};
_ws.MessageReceived += (s, e) =>
{
Log("_ws.MessageReceived: " + e.Message);
Received?.Invoke(this, e.Message);
};
_ws.DataReceived += (s, e) =>
{
//ここに来たことは今のところ一度もない。
Debug.WriteLine("Dataが送られてきた");
};
_ws.Opened += (s, e) =>
{
Log("_ws.Opend");
Opened?.Invoke(this, EventArgs.Empty);
};
_ws.Closed += (s, e) =>
{
Log("_ws.Closed");
try
{
tcs.TrySetResult(null);
}
finally
{
if (_ws != null)
{
_ws.Dispose();
_ws = null;
}
}
};
_ws.Error += (s, e) =>
{
Log("_ws.Error");
try
{
tcs.SetException(e.Exception);
}
finally
{
if (_ws != null)
{
_ws.Dispose();
_ws = null;
}
}
};
_ws.Open();
return tcs.Task;
}
public async Task SendAsync(string str)
{
await Task.Yield();
if (_ws != null)
{
_ws.Send(str);
Debug.WriteLine("websocket send:" + str);
}
await Task.CompletedTask;
}
public void Disconnect()
{
if (_ws != null)
{
_ws.Close();
}
}
public Websocket()
{
}
}
}
OpenrecWebsocket.cs
using System;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Collections.Generic;
using System.Net;
using Common;
namespace OpenrecSitePlugin
{
class OpenrecWebsocket : IOpenrecWebsocket
{
private Websocket _websocket;
private readonly ILogger _logger;
//public event EventHandler<IOpenrecCommentData> CommentReceived;
public event EventHandler<IPacket> Received;
public async Task ReceiveAsync(string movieId, string userAgent, List<Cookie> cookies)
{
var cookieList = new List<KeyValuePair<string, string>>();
foreach (Cookie cookie in cookies)
{
if (cookie.Name == "AWSALB")
{
cookieList.Add(new KeyValuePair<string, string>(cookie.Name, cookie.Value));
}
}
var origin = "https://www.openrec.tv";
_websocket = new Websocket();
_websocket.Received += Websocket_Received;
_websocket.Opened += Websocket_Opened;
var url = $"wss://chat.openrec.tv/socket.io/?movieId={movieId}&EIO=3&transport=websocket";
await _websocket.ReceiveAsync(url, cookieList, userAgent, origin);
//切断後処理
_heartbeatTimer.Enabled = false;
}
private void Websocket_Opened(object sender, EventArgs e)
{
_heartbeatTimer.Enabled = true;
}
private void Websocket_Received(object sender, string e)
{
Debug.WriteLine(e);
IPacket packet = null;
try
{
packet = Packet.Parse(e);
}
catch (ParseException ex)
{
_logger.LogException(ex);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
if (packet == null)
return;
Received?.Invoke(this, packet);
}
public async Task SendAsync(IPacket packet)
{
if (packet is PacketPing ping)
{
await SendAsync(ping.Raw);
}
else
{
throw new NotImplementedException();
}
}
public async Task SendAsync(string s)
{
await _websocket.SendAsync(s);
}
System.Timers.Timer _heartbeatTimer = new System.Timers.Timer();
public OpenrecWebsocket(ILogger logger)
{
_logger = logger;
_heartbeatTimer.Interval = 25 * 1000;
_heartbeatTimer.Elapsed += _heartBeatTimer_Elapsed;
_heartbeatTimer.AutoReset = true;
}
private async void _heartBeatTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
try
{
await SendAsync(new PacketPing());
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
public void Disconnect()
{
_websocket.Disconnect();
}
}
}
Tools.cs
結構ながくなるので割愛します。
ただ、これはサイトごとの設定ファイルで、結構重要なファイル。
開発手順
後日、余力があれば書きます。。。
コメント